Skip to content

Commit cbd93b1

Browse files
authored
Merge pull request #3650 from verifywise-ai/fix/ai-gateway-prompt-test-streaming
fix(ai-gateway): implement prompt test streaming and fix Compare panel
2 parents 0400355 + b392324 commit cbd93b1

File tree

3 files changed

+49
-15
lines changed

3 files changed

+49
-15
lines changed

AIGateway/src/routers/prompts.py

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
1+
import json as _json
12
import re
23
from typing import Any, Optional
34

45
from fastapi import APIRouter, HTTPException, Request
6+
from fastapi.responses import StreamingResponse
57
from pydantic import BaseModel, field_validator
68

79
from crud import prompts as crud
810
from middlewares.auth import verify_internal_key
11+
from services.llm_service import stream_chat_completion
12+
from services.proxy_service import resolve_endpoint_for_key
913
from utils.auth import get_org_id, get_user_id, require_admin
1014
from utils.notifications import notify_config_change
1115

@@ -377,27 +381,42 @@ async def delete_test_dataset(
377381
return {"deleted": True}
378382

379383

380-
@router.post("/test", status_code=501)
384+
@router.post("/test")
381385
async def test_prompt(request: Request, body: TestPromptRequest):
382386
"""
383-
Test a prompt by resolving variables and proxying to the LLM completion
384-
endpoint. The LLM proxy integration lives in the Express backend for now;
385-
this stub returns 501 until the proxy is co-located in the AIGateway
386-
service.
387+
Test a prompt by resolving variables and streaming the LLM response.
388+
Returns an SSE stream compatible with the frontend's streamPromptTest().
387389
"""
388390
verify_internal_key(request)
391+
org_id = get_org_id(request)
389392

390-
# Resolve variables so callers can at least validate substitution locally
393+
# Resolve variables in the prompt content
391394
resolved_content = crud.resolve_variables(
392395
body.content,
393396
body.variables or {},
394397
)
395398

396-
raise HTTPException(
397-
status_code=501,
398-
detail=(
399-
"test-prompt requires proxy integration — "
400-
"LLM proxy currently runs in the Express backend. "
401-
"Resolved content is available but cannot be forwarded yet."
402-
),
403-
)
399+
# Resolve the endpoint to get provider, model, and API key
400+
try:
401+
endpoint = await resolve_endpoint_for_key(
402+
organization_id=org_id,
403+
endpoint_slug=body.endpoint_slug,
404+
allowed_endpoint_ids=[],
405+
)
406+
except ValueError as e:
407+
raise HTTPException(status_code=404, detail=str(e))
408+
409+
# Stream the LLM response
410+
async def _stream():
411+
try:
412+
async for chunk_str in stream_chat_completion(
413+
model=endpoint["model"],
414+
messages=resolved_content,
415+
api_key=endpoint["decrypted_key"],
416+
):
417+
yield chunk_str
418+
except Exception as e:
419+
yield f"data: {_json.dumps({'error': str(e)})}\n\n"
420+
yield "data: [DONE]\n\n"
421+
422+
return StreamingResponse(_stream(), media_type="text/event-stream")

Clients/src/presentation/pages/AIGateway/Prompts/ComparePanel.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export default function ComparePanel({
116116
items={versionItems}
117117
placeholder="Select version"
118118
sx={{ flex: 1 }}
119+
getOptionValue={(item) => item._id}
119120
/>
120121
<Select
121122
id="compare-select-right"
@@ -125,6 +126,7 @@ export default function ComparePanel({
125126
items={versionItems}
126127
placeholder="Select version"
127128
sx={{ flex: 1 }}
129+
getOptionValue={(item) => item._id}
128130
/>
129131
</Box>
130132
<Typography fontSize={13} fontWeight={500} color="text.secondary" mt="8px">Endpoint</Typography>
@@ -135,6 +137,7 @@ export default function ComparePanel({
135137
items={endpoints.map((e) => ({ _id: e.slug, name: `${e.display_name} (${e.slug})` }))}
136138
placeholder="Select endpoint"
137139
sx={{ width: "100%" }}
140+
getOptionValue={(item) => item._id}
138141
/>
139142
<Typography fontSize={11} color="text.disabled" mt="4px">
140143
{detectedVars.length > 0

Clients/src/presentation/pages/AIGateway/shared.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,7 @@ export async function streamPromptTest(
245245
): Promise<StreamPromptTestResult> {
246246
const startTime = Date.now();
247247

248-
const response = await fetch(`${GATEWAY_API_URL}/ai-gateway/prompts/test`, {
248+
const response = await fetch(`/api/ai-gateway/prompts/test`, {
249249
method: "POST",
250250
headers: {
251251
"Content-Type": "application/json",
@@ -274,6 +274,7 @@ export async function streamPromptTest(
274274
let content = "";
275275
let tokens = 0;
276276
let cost = 0;
277+
let streamError = "";
277278

278279
try {
279280
while (true) {
@@ -285,6 +286,9 @@ export async function streamPromptTest(
285286
if (line.startsWith("data: ") && line !== "data: [DONE]") {
286287
try {
287288
const chunk = JSON.parse(line.slice(6));
289+
if (chunk.error) {
290+
streamError = chunk.error;
291+
}
288292
const delta = chunk.choices?.[0]?.delta?.content;
289293
if (delta) {
290294
content += delta;
@@ -300,5 +304,13 @@ export async function streamPromptTest(
300304
reader.releaseLock();
301305
}
302306

307+
if (!content && streamError) {
308+
content = `Error: ${streamError}`;
309+
opts.onDelta(content);
310+
} else if (!content) {
311+
content = "No response received from the model. Check that the endpoint has a valid API key configured.";
312+
opts.onDelta(content);
313+
}
314+
303315
return { content, tokens, cost, latency: Date.now() - startTime };
304316
}

0 commit comments

Comments
 (0)