Skip to content

Commit cda147d

Browse files
committed
Return automation errors before streaming
1 parent 59e18ba commit cda147d

2 files changed

Lines changed: 70 additions & 85 deletions

File tree

‎app2api/api.py‎

Lines changed: 40 additions & 85 deletions
Original file line numberDiff line numberDiff line change
@@ -220,35 +220,6 @@ async def create_chat_completion(payload: ChatCompletionRequest):
220220
status_code=400,
221221
)
222222

223-
if payload.stream:
224-
225-
async def generate_chat_stream():
226-
job = await execute_protocol(
227-
target,
228-
question,
229-
payload.user,
230-
"openai-chat-completions",
231-
)
232-
if job.status != JobStatus.SUCCEEDED or job.answer is None:
233-
yield sse_data(
234-
error_body(
235-
job.error or "application automation failed",
236-
"server_error",
237-
"automation_failed",
238-
)
239-
)
240-
yield "data: [DONE]\n\n"
241-
return
242-
for chunk in chat_completion_chunks(job, payload.model):
243-
yield sse_data(chunk)
244-
yield "data: [DONE]\n\n"
245-
246-
return StreamingResponse(
247-
generate_chat_stream(),
248-
media_type="text/event-stream",
249-
headers={"Cache-Control": "no-cache"},
250-
)
251-
252223
job = await execute_protocol(
253224
target,
254225
question,
@@ -264,6 +235,20 @@ async def generate_chat_stream():
264235
),
265236
status_code=502,
266237
)
238+
239+
if payload.stream:
240+
241+
async def generate_chat_stream():
242+
for chunk in chat_completion_chunks(job, payload.model):
243+
yield sse_data(chunk)
244+
yield "data: [DONE]\n\n"
245+
246+
return StreamingResponse(
247+
generate_chat_stream(),
248+
media_type="text/event-stream",
249+
headers={"Cache-Control": "no-cache"},
250+
)
251+
267252
return chat_completion(job, payload.model)
268253

269254
@app.post("/v1/responses")
@@ -292,34 +277,6 @@ async def create_response(payload: ResponsesRequest):
292277
status_code=400,
293278
)
294279

295-
if payload.stream:
296-
297-
async def generate_response_stream():
298-
job = await execute_protocol(
299-
target,
300-
question,
301-
payload.user,
302-
"openai-responses",
303-
)
304-
if job.status != JobStatus.SUCCEEDED or job.answer is None:
305-
yield response_sse(
306-
"error",
307-
error_body(
308-
job.error or "application automation failed",
309-
"server_error",
310-
"automation_failed",
311-
),
312-
)
313-
return
314-
for event, value in responses_stream_events(job, payload):
315-
yield response_sse(event, value)
316-
317-
return StreamingResponse(
318-
generate_response_stream(),
319-
media_type="text/event-stream",
320-
headers={"Cache-Control": "no-cache"},
321-
)
322-
323280
job = await execute_protocol(
324281
target,
325282
question,
@@ -335,6 +292,19 @@ async def generate_response_stream():
335292
),
336293
status_code=502,
337294
)
295+
296+
if payload.stream:
297+
298+
async def generate_response_stream():
299+
for event, value in responses_stream_events(job, payload):
300+
yield response_sse(event, value)
301+
302+
return StreamingResponse(
303+
generate_response_stream(),
304+
media_type="text/event-stream",
305+
headers={"Cache-Control": "no-cache"},
306+
)
307+
338308
return response_object(job, payload)
339309

340310
@app.post("/v1/messages")
@@ -365,26 +335,23 @@ async def create_anthropic_message(payload: AnthropicMessagesRequest):
365335
if user is not None and not isinstance(user, str):
366336
user = str(user)
367337

338+
job = await execute_protocol(
339+
target,
340+
question,
341+
user,
342+
"anthropic-messages",
343+
)
344+
if job.status != JobStatus.SUCCEEDED or job.answer is None:
345+
return anthropic_json_error(
346+
job.error or "application automation failed",
347+
502,
348+
"api_error",
349+
)
350+
368351
if payload.stream:
369352
request_id = anthropic_request_id()
370353

371354
async def generate_anthropic_stream():
372-
job = await execute_protocol(
373-
target,
374-
question,
375-
user,
376-
"anthropic-messages",
377-
)
378-
if job.status != JobStatus.SUCCEEDED or job.answer is None:
379-
yield anthropic_sse(
380-
"error",
381-
anthropic_error_body(
382-
job.error or "application automation failed",
383-
"api_error",
384-
request_id,
385-
),
386-
)
387-
return
388355
for event, value in anthropic_stream_events(job, payload):
389356
yield anthropic_sse(event, value)
390357

@@ -397,18 +364,6 @@ async def generate_anthropic_stream():
397364
},
398365
)
399366

400-
job = await execute_protocol(
401-
target,
402-
question,
403-
user,
404-
"anthropic-messages",
405-
)
406-
if job.status != JobStatus.SUCCEEDED or job.answer is None:
407-
return anthropic_json_error(
408-
job.error or "application automation failed",
409-
502,
410-
"api_error",
411-
)
412367
request_id = anthropic_request_id()
413368
return JSONResponse(
414369
anthropic_message_object(job, payload),

‎tests/test_openai_compat.py‎

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,3 +207,33 @@ def test_xiaohuoren_is_a_separate_openai_model(tmp_path):
207207
item for item in models["models"] if item["slug"] == "douyin_xiaohuoren"
208208
)
209209
assert catalog["display_name"] == "Douyin Xiaohuoren"
210+
211+
212+
def test_responses_stream_returns_http_error_before_sse_on_automation_failure(
213+
tmp_path,
214+
):
215+
app = create_app(
216+
Settings(
217+
_env_file=None,
218+
driver="mock",
219+
enabled_targets="meituan",
220+
config_dir=tmp_path / "missing-config",
221+
artifact_dir=tmp_path / "artifacts",
222+
recording_enabled=False,
223+
job_timeout_seconds=5,
224+
)
225+
)
226+
with TestClient(app) as client:
227+
response = client.post(
228+
"/v1/responses",
229+
json={
230+
"model": "meituan_xiaotuan",
231+
"input": "这个请求不应开启不完整的 SSE 流",
232+
"stream": True,
233+
},
234+
)
235+
236+
assert response.status_code == 502
237+
assert response.headers["content-type"].startswith("application/json")
238+
assert response.json()["error"]["code"] == "automation_failed"
239+
assert "缺少目标流程配置" in response.json()["error"]["message"]

0 commit comments

Comments
 (0)