Skip to content

Commit 078e390

Browse files
committed
fix: classify backend value errors as bad gateway
1 parent 3fc2f99 commit 078e390

File tree

2 files changed

+91
-30
lines changed

2 files changed

+91
-30
lines changed

examples/subscription_bridge/server.py

Lines changed: 42 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -589,7 +589,10 @@ def run_backend_structured(
589589
stderr = (result.stderr or result.stdout or "backend execution failed").strip()
590590
raise RuntimeError(stderr)
591591
raw = output_path.read_text() if output_path.exists() else result.stdout
592-
return _normalize_decision_payload(_load_json_output(raw))
592+
try:
593+
return _normalize_decision_payload(_load_json_output(raw))
594+
except ValueError as exc:
595+
raise RuntimeError(str(exc)) from exc
593596

594597
if backend == "claude":
595598
cmd = [
@@ -610,7 +613,10 @@ def run_backend_structured(
610613
if result.returncode != 0:
611614
stderr = (result.stderr or result.stdout or "backend execution failed").strip()
612615
raise RuntimeError(stderr)
613-
return _normalize_decision_payload(_load_json_output(result.stdout))
616+
try:
617+
return _normalize_decision_payload(_load_json_output(result.stdout))
618+
except ValueError as exc:
619+
raise RuntimeError(str(exc)) from exc
614620

615621
raise ValueError(f"Unsupported backend: {backend}")
616622

@@ -620,24 +626,27 @@ def _respond_for_chat_request(
620626
) -> dict[str, Any]:
621627
prompt = build_chat_prompt(payload)
622628
if _normalize_tools(payload.get("tools")):
623-
decision = run_backend_structured(
624-
backend=backend,
625-
prompt=_build_structured_decision_prompt(prompt, payload),
626-
model=model,
627-
workdir=workdir,
628-
schema=DecisionSchema,
629-
)
630-
if decision.get("type") == "tool_calls":
629+
try:
630+
decision = run_backend_structured(
631+
backend=backend,
632+
prompt=_build_structured_decision_prompt(prompt, payload),
633+
model=model,
634+
workdir=workdir,
635+
schema=DecisionSchema,
636+
)
637+
if decision.get("type") == "tool_calls":
638+
return build_chat_completion_response(
639+
model=model,
640+
request_id=request_id,
641+
tool_calls=decision.get("tool_calls"),
642+
)
631643
return build_chat_completion_response(
632644
model=model,
633645
request_id=request_id,
634-
tool_calls=decision.get("tool_calls"),
646+
content=str(decision.get("content") or ""),
635647
)
636-
return build_chat_completion_response(
637-
model=model,
638-
request_id=request_id,
639-
content=str(decision.get("content") or ""),
640-
)
648+
except ValueError as exc:
649+
raise RuntimeError(str(exc)) from exc
641650

642651
text = run_backend(backend=backend, prompt=prompt, model=model, workdir=workdir)
643652
return build_chat_completion_response(model=model, request_id=request_id, content=text)
@@ -648,24 +657,27 @@ def _respond_for_responses_request(
648657
) -> dict[str, Any]:
649658
prompt = build_responses_prompt(payload)
650659
if _normalize_tools(payload.get("tools")):
651-
decision = run_backend_structured(
652-
backend=backend,
653-
prompt=_build_structured_decision_prompt(prompt, payload),
654-
model=model,
655-
workdir=workdir,
656-
schema=DecisionSchema,
657-
)
658-
if decision.get("type") == "tool_calls":
660+
try:
661+
decision = run_backend_structured(
662+
backend=backend,
663+
prompt=_build_structured_decision_prompt(prompt, payload),
664+
model=model,
665+
workdir=workdir,
666+
schema=DecisionSchema,
667+
)
668+
if decision.get("type") == "tool_calls":
669+
return build_responses_api_response(
670+
model=model,
671+
request_id=request_id,
672+
tool_calls=decision.get("tool_calls"),
673+
)
659674
return build_responses_api_response(
660675
model=model,
661676
request_id=request_id,
662-
tool_calls=decision.get("tool_calls"),
677+
content=str(decision.get("content") or ""),
663678
)
664-
return build_responses_api_response(
665-
model=model,
666-
request_id=request_id,
667-
content=str(decision.get("content") or ""),
668-
)
679+
except ValueError as exc:
680+
raise RuntimeError(str(exc)) from exc
669681

670682
text = run_backend(backend=backend, prompt=prompt, model=model, workdir=workdir)
671683
return build_responses_api_response(model=model, request_id=request_id, content=text)

tests/examples/test_subscription_bridge.py

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,55 @@ def fake_run_backend(*, backend: str, prompt: str, model: str | None, workdir: P
5959
)
6060

6161

62+
def test_http_server_returns_502_for_backend_value_errors(
63+
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
64+
) -> None:
65+
def fake_run_backend_structured(
66+
*, backend: str, prompt: str, model: str | None, workdir: Path, schema: dict[str, Any]
67+
) -> dict[str, Any]:
68+
raise ValueError("backend emitted malformed JSON")
69+
70+
monkeypatch.setattr(server, "run_backend_structured", fake_run_backend_structured)
71+
72+
httpd = server.make_server("127.0.0.1", 0, default_backend="codex", workdir=tmp_path)
73+
thread = threading.Thread(target=httpd.serve_forever, daemon=True)
74+
thread.start()
75+
time.sleep(0.05)
76+
try:
77+
req = urllib.request.Request(
78+
f"http://127.0.0.1:{httpd.server_address[1]}/v1/chat/completions",
79+
data=json.dumps(
80+
{
81+
"messages": [{"role": "user", "content": "Use the weather tool."}],
82+
"tools": [
83+
{
84+
"type": "function",
85+
"function": {
86+
"name": "get_weather",
87+
"description": "Get the weather for a city.",
88+
"parameters": {
89+
"type": "object",
90+
"properties": {"city": {"type": "string"}},
91+
},
92+
},
93+
}
94+
],
95+
}
96+
).encode("utf-8"),
97+
headers={"Content-Type": "application/json"},
98+
)
99+
with pytest.raises(urllib.error.HTTPError) as exc_info:
100+
urllib.request.urlopen(req)
101+
finally:
102+
httpd.shutdown()
103+
httpd.server_close()
104+
thread.join(timeout=2)
105+
106+
assert exc_info.value.code == 502
107+
payload = json.loads(exc_info.value.read().decode("utf-8"))
108+
assert payload == {"error": {"message": "backend emitted malformed JSON"}}
109+
110+
62111
def test_build_chat_prompt_from_messages_preserves_roles() -> None:
63112
payload = {
64113
"messages": [

0 commit comments

Comments
 (0)