hermes: preserve Switchyard response content
All checks were successful
Tests / Declarative: Post Actions passed: 244
All checks were successful
Tests / Declarative: Post Actions passed: 244
This commit is contained in:
parent
678db67776
commit
c8409b8f3a
@ -25,7 +25,7 @@ spec:
|
|||||||
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
ai.bstein.dev/execution: Hermes Kanban with durable direct Codex and Claude Code CLI workers
|
||||||
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
ai.bstein.dev/model-policy: Jetson-assisted AUTO routing, low through xhigh, cross-provider fallback
|
||||||
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
|
ai.bstein.dev/placement: rpi5 preferred; Jetson deferred until state storage is available
|
||||||
ai.bstein.dev/config-rev: "20260811-switchyard-responses-adapter"
|
ai.bstein.dev/config-rev: "20260811-switchyard-responses-output"
|
||||||
vault.hashicorp.com/agent-inject: "true"
|
vault.hashicorp.com/agent-inject: "true"
|
||||||
vault.hashicorp.com/role: hermes-agent
|
vault.hashicorp.com/role: hermes-agent
|
||||||
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
vault.hashicorp.com/agent-inject-secret-anthropic-token: kv/data/atlas/hermes/agent-tokens
|
||||||
|
|||||||
@ -73,16 +73,13 @@ data:
|
|||||||
# unambiguous and safe default.
|
# unambiguous and safe default.
|
||||||
payload["model"] = "qwen2.5:14b-instruct-q4_0"
|
payload["model"] = "qwen2.5:14b-instruct-q4_0"
|
||||||
changed = True
|
changed = True
|
||||||
for key in ("reasoning_effort", "reasoning"):
|
# This non-thinking Qwen build rejects hosted-provider reasoning fields.
|
||||||
|
# Effort remains a Switchyard routing concern, not an Ollama request.
|
||||||
|
for key in ("reasoning_effort", "reasoning", "thinking", "output_config"):
|
||||||
value = payload.get(key)
|
value = payload.get(key)
|
||||||
if isinstance(value, str) and value.lower() in {"xhigh", "max"}:
|
if value is not None:
|
||||||
payload[key] = "high"
|
payload.pop(key, None)
|
||||||
changed = True
|
changed = True
|
||||||
elif isinstance(value, dict):
|
|
||||||
effort = value.get("effort")
|
|
||||||
if isinstance(effort, str) and effort.lower() in {"xhigh", "max"}:
|
|
||||||
value["effort"] = "high"
|
|
||||||
changed = True
|
|
||||||
if not changed:
|
if not changed:
|
||||||
return body
|
return body
|
||||||
return json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
return json.dumps(payload, separators=(",", ":")).encode("utf-8")
|
||||||
|
|||||||
@ -15,7 +15,7 @@ spec:
|
|||||||
template:
|
template:
|
||||||
metadata:
|
metadata:
|
||||||
annotations:
|
annotations:
|
||||||
ai.bstein.dev/config-rev: "20260811-switchyard-model-default"
|
ai.bstein.dev/config-rev: "20260811-switchyard-model-fields"
|
||||||
labels:
|
labels:
|
||||||
app: hermes-model-gate
|
app: hermes-model-gate
|
||||||
spec:
|
spec:
|
||||||
|
|||||||
@ -133,6 +133,7 @@ def _validate_payload(payload: Any) -> dict[str, Any]:
|
|||||||
def _completed_response(lines: Iterable[str]) -> dict[str, Any]:
|
def _completed_response(lines: Iterable[str]) -> dict[str, Any]:
|
||||||
"""Collapse a Codex SSE stream for a non-streaming Responses caller."""
|
"""Collapse a Codex SSE stream for a non-streaming Responses caller."""
|
||||||
terminal_response: dict[str, Any] | None = None
|
terminal_response: dict[str, Any] | None = None
|
||||||
|
output_items: dict[int, dict[str, Any]] = {}
|
||||||
upstream_error = ""
|
upstream_error = ""
|
||||||
for line in lines:
|
for line in lines:
|
||||||
if not line.startswith("data:"):
|
if not line.startswith("data:"):
|
||||||
@ -148,6 +149,11 @@ def _completed_response(lines: Iterable[str]) -> dict[str, Any]:
|
|||||||
continue
|
continue
|
||||||
event_type = str(event.get("type") or "")
|
event_type = str(event.get("type") or "")
|
||||||
response = event.get("response")
|
response = event.get("response")
|
||||||
|
if event_type == "response.output_item.done":
|
||||||
|
item = event.get("item")
|
||||||
|
output_index = event.get("output_index")
|
||||||
|
if isinstance(item, dict) and isinstance(output_index, int):
|
||||||
|
output_items[output_index] = item
|
||||||
if event_type in {
|
if event_type in {
|
||||||
"response.completed",
|
"response.completed",
|
||||||
"response.failed",
|
"response.failed",
|
||||||
@ -161,6 +167,13 @@ def _completed_response(lines: Iterable[str]) -> dict[str, Any]:
|
|||||||
else:
|
else:
|
||||||
upstream_error = str(error or "")
|
upstream_error = str(error or "")
|
||||||
if terminal_response is not None:
|
if terminal_response is not None:
|
||||||
|
# The subscription Codex endpoint streams complete output items but
|
||||||
|
# currently leaves the terminal response's output array empty. Public
|
||||||
|
# Responses clients, including Switchyard, expect those items there.
|
||||||
|
if output_items and not terminal_response.get("output"):
|
||||||
|
terminal_response["output"] = [
|
||||||
|
output_items[index] for index in sorted(output_items)
|
||||||
|
]
|
||||||
return terminal_response
|
return terminal_response
|
||||||
raise RuntimeError(upstream_error or "Codex stream ended without a terminal response")
|
raise RuntimeError(upstream_error or "Codex stream ended without a terminal response")
|
||||||
|
|
||||||
|
|||||||
@ -85,12 +85,10 @@ data:
|
|||||||
[targets.local_qwen_low]
|
[targets.local_qwen_low]
|
||||||
id = "route/local/qwen2.5-14b/low"
|
id = "route/local/qwen2.5-14b/low"
|
||||||
llm_client = "local_low"
|
llm_client = "local_low"
|
||||||
extra_body = { reasoning_effort = "low" }
|
|
||||||
|
|
||||||
[targets.local_qwen_medium]
|
[targets.local_qwen_medium]
|
||||||
id = "route/local/qwen2.5-14b/medium"
|
id = "route/local/qwen2.5-14b/medium"
|
||||||
llm_client = "local_medium"
|
llm_client = "local_medium"
|
||||||
extra_body = { reasoning_effort = "medium" }
|
|
||||||
|
|
||||||
[targets.codex_luna_low]
|
[targets.codex_luna_low]
|
||||||
id = "route/codex/luna/low"
|
id = "route/codex/luna/low"
|
||||||
|
|||||||
@ -19,7 +19,7 @@ spec:
|
|||||||
labels:
|
labels:
|
||||||
app: hermes-switchyard
|
app: hermes-switchyard
|
||||||
annotations:
|
annotations:
|
||||||
ai.bstein.dev/config-rev: "20260811-switchyard-authority-v6"
|
ai.bstein.dev/config-rev: "20260811-switchyard-authority-v7"
|
||||||
prometheus.io/scrape: "true"
|
prometheus.io/scrape: "true"
|
||||||
prometheus.io/port: "9005"
|
prometheus.io/port: "9005"
|
||||||
prometheus.io/path: /metrics
|
prometheus.io/path: /metrics
|
||||||
|
|||||||
@ -548,16 +548,31 @@ def test_codex_broker_auth_and_request_contract(tmp_path: Path, monkeypatch):
|
|||||||
"status": "completed",
|
"status": "completed",
|
||||||
"output": [],
|
"output": [],
|
||||||
}
|
}
|
||||||
|
completed_item = {
|
||||||
|
"type": "message",
|
||||||
|
"role": "assistant",
|
||||||
|
"status": "completed",
|
||||||
|
"content": [{"type": "output_text", "text": "done"}],
|
||||||
|
}
|
||||||
assert module._completed_response(
|
assert module._completed_response(
|
||||||
[
|
[
|
||||||
"event: response.created",
|
"event: response.created",
|
||||||
'data: {"type":"response.created","response":{}}',
|
'data: {"type":"response.created","response":{}}',
|
||||||
|
"event: response.output_item.done",
|
||||||
|
"data: "
|
||||||
|
+ json.dumps(
|
||||||
|
{
|
||||||
|
"type": "response.output_item.done",
|
||||||
|
"output_index": 0,
|
||||||
|
"item": completed_item,
|
||||||
|
}
|
||||||
|
),
|
||||||
"event: response.completed",
|
"event: response.completed",
|
||||||
"data: "
|
"data: "
|
||||||
+ json.dumps({"type": "response.completed", "response": completed}),
|
+ json.dumps({"type": "response.completed", "response": completed}),
|
||||||
"data: [DONE]",
|
"data: [DONE]",
|
||||||
]
|
]
|
||||||
) == completed
|
)["output"] == [completed_item]
|
||||||
with pytest.raises(RuntimeError, match="provider unavailable"):
|
with pytest.raises(RuntimeError, match="provider unavailable"):
|
||||||
module._completed_response(
|
module._completed_response(
|
||||||
[
|
[
|
||||||
|
|||||||
@ -18,24 +18,21 @@ def _model_gate_namespace() -> dict:
|
|||||||
return namespace
|
return namespace
|
||||||
|
|
||||||
|
|
||||||
def test_model_gate_clamps_hosted_only_reasoning_efforts():
|
def test_model_gate_removes_hosted_only_reasoning_fields():
|
||||||
normalize = _model_gate_namespace()["_normalize_reasoning"]
|
normalize = _model_gate_namespace()["_normalize_reasoning"]
|
||||||
|
|
||||||
top_level = json.loads(normalize(b'{"reasoning_effort":"xhigh"}'))
|
top_level = json.loads(normalize(b'{"reasoning_effort":"xhigh"}'))
|
||||||
string_reasoning = json.loads(normalize(b'{"reasoning":"max"}'))
|
string_reasoning = json.loads(normalize(b'{"reasoning":"max"}'))
|
||||||
nested = json.loads(normalize(b'{"reasoning":{"effort":"xhigh"}}'))
|
nested = json.loads(normalize(b'{"reasoning":{"effort":"xhigh"}}'))
|
||||||
|
|
||||||
assert top_level["reasoning_effort"] == "high"
|
assert "reasoning_effort" not in top_level
|
||||||
assert string_reasoning["reasoning"] == "high"
|
assert "reasoning" not in string_reasoning
|
||||||
assert nested["reasoning"]["effort"] == "high"
|
assert "reasoning" not in nested
|
||||||
|
|
||||||
|
|
||||||
def test_model_gate_preserves_supported_and_non_json_requests():
|
def test_model_gate_preserves_supported_and_non_json_requests():
|
||||||
normalize = _model_gate_namespace()["_normalize_reasoning"]
|
normalize = _model_gate_namespace()["_normalize_reasoning"]
|
||||||
supported = (
|
supported = b'{"model":"qwen2.5:14b-instruct-q4_0","messages":[]}'
|
||||||
b'{"model":"qwen2.5:14b-instruct-q4_0",'
|
|
||||||
b'"reasoning_effort":"medium","messages":[]}'
|
|
||||||
)
|
|
||||||
non_json = b"streamed-body"
|
non_json = b"streamed-body"
|
||||||
|
|
||||||
assert normalize(supported) == supported
|
assert normalize(supported) == supported
|
||||||
@ -53,7 +50,7 @@ def test_model_gate_translates_switchyard_local_target_alias():
|
|||||||
)
|
)
|
||||||
|
|
||||||
assert routed["model"] == "qwen2.5:14b-instruct-q4_0"
|
assert routed["model"] == "qwen2.5:14b-instruct-q4_0"
|
||||||
assert routed["reasoning_effort"] == "medium"
|
assert "reasoning_effort" not in routed
|
||||||
|
|
||||||
|
|
||||||
def test_model_gate_supplies_its_single_model_when_switchyard_omits_it():
|
def test_model_gate_supplies_its_single_model_when_switchyard_omits_it():
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user