[NA] [CI] fix: serve both LLM wire protocols in the E2E mock - #28
Merged
Conversation
The nightly E2E started failing on 2026-08-01 without any change to this repo. A rolling nousresearch/hermes-agent:latest rebuild began honoring the openai-api provider's declared codex_responses transport, switching the wire from POST /v1/chat/completions to POST /v1/responses. The mock only spoke Chat Completions, so the stream never emitted a terminal event and every turn died after three retries with no tool call — surfacing as 'trace has no output' and 'no tool spans' across all three E2E jobs. Serve both protocols rather than pinning the image: the nightly cron exists to catch exactly this upstream drift, so pinning would silence the detector. The Chat Completions handler is unchanged, so older Hermes images keep passing. Also replace the catch-all 200 for unknown POST routes with a 501. The benign 200 let an unimplemented protocol masquerade as a hung stream, which is what made this break hard to diagnose. Verified against v2026.7.20, v2026.7.30 (chat_completions) and :latest (codex_responses): all three E2E jobs pass, 113 unit tests pass.
Review feedback: the global _STATE["calls"] counter was incremented unlocked and before the response was emitted. Two failure modes follow — ThreadingHTTPServer can serve requests concurrently, and an aborted or retried call consumed the tool stage, leaving the retry to receive the final message instead. Reproduced the latter: aborting call 1 mid-stream made the retry return output_text rather than re-emitting function_call, which would surface as a bare 'no tool spans' failure. The conversation already carries the stage, so read it instead of counting: turn 1 has only the user message, turn 2 carries the tool result (function_call_output on the Responses wire, role: "tool" on Chat Completions). That is idempotent — a retry resends the same body and gets the same stage — so the mutable global goes away entirely rather than being guarded by a lock. Verified: the retry now re-emits function_call; both wires stage correctly on turns 1 and 2; mock E2E passes on latest, v2026.7.30 and v2026.7.20; wheel and real-Opik E2E pass; 113 unit tests pass.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
User description
Details
The nightly E2E went red on 2026-08-01 with no change to this repo (last commit
f701092, 2026-07-10). All three jobs failed together withtrace has no output (finalize re-send did not land)andno tool spans.Root cause — upstream drift, not a plugin regression. A rolling
nousresearch/hermes-agent:latestrebuild began honoring theopenai-apiprovider's declaredcodex_responsestransport, switching the chat wire fromPOST /v1/chat/completionstoPOST /v1/responses. Our mock only spoke Chat Completions, so/v1/responsesfell through to the catch-all{"ok": true}, the stream never emitted a terminal event, and every turn died after 3 retries having made 0 tool calls:Both reported assertion failures trace back to that single cause: no tool span (the call never happened) and no trace output (the turn never reached finalize).
Note the version tags are misleading here —
:latestandv2026.7.30are both version0.19.1but different digests, so a version bump was never going to signal this.Fix — serve both wire protocols, keep tracking
latest./v1/responses: typed SSE events (response.output_item.added→.done→response.completed). Bothaddedanddoneare required — Hermes setshas_tool_callsfrom the first and collects the executable item from the second (codex_runtime.py:1078,1116), and the absent terminal event is what raised at:1213./v1/chat/completionsbyte-for-byte, so older Hermes images keep passing.200for unknown POST routes with a501. The benign 200 let an unimplemented protocol masquerade as a hung stream — that disguise is what made this expensive to diagnose. The next protocol change now names itself in the first log line.Deliberately not pinning
HERMES_IMAGE. This cron exists to catch upstream drift (e2e.yml:5). Pinning would have turned the suite green while the incompatibility it was built to find sat undetected — a silenced detector, not a stable one.Change checklist
Issues
Testing
Reproduced the failure locally against
:latest, then verified the fix across all three images and all three CI jobs:v2026.7.20chat_completionsv2026.7.30chat_completions:latest(Aug 2 rebuild)codex_responsesrun_e2e.sh(mock) —llm spans: 2 \| tool spans: 1 \| NA: 0run_e2e_wheel.sh(pip entry point) — PASSEDrun_e2e_real_opik.sh(full Opik stack) —spans=3 llm=2 tool=1 NA=0ruff checkcleanConfirmed the fix restores real coverage rather than just satisfying assertions: on the
codex_responseswire the trace recordsapi_mode=codex_responseswith correctly-named spans, incrementingLLM call 1/2, genuine tool output (1024), usage, and provider mapped toopenai. The plugin was already correct on the new wire — the E2E just couldn't prove it. The OPIK-7280 pretty-mode output shape holds there too.Documentation
N/A — no user-facing or API changes. The module docstring in
e2e/mock_llm_server.pydocuments both protocols, why both are kept, and why unknown routes fail loudly.Generated description
Below is a concise technical summary of the changes proposed in this PR:
graph LR do_POST_("do_POST"):::modified already_ran_tool_("_already_ran_tool"):::added responses_tool_events_("_responses_tool_events"):::added responses_final_events_("_responses_final_events"):::added sse_typed_("_sse_typed"):::added OPENAI_RESPONSES_API_("OPENAI_RESPONSES_API"):::added OPENAI_CHAT_COMPLETIONS_API_("OPENAI_CHAT_COMPLETIONS_API"):::modified do_POST_ -- "Decides tool stage via request body, not call counter." --> already_ran_tool_ do_POST_ -- "Emits function_call added/done and response.completed for tool turn." --> responses_tool_events_ do_POST_ -- "Streams assistant text delta, then response.completed after tool output." --> responses_final_events_ do_POST_ -- "Streams event-typed SSE frames ending at response.completed." --> sse_typed_ do_POST_ -- "Adds /v1/responses mock with typed SSE and two-step tool cycle." --> OPENAI_RESPONSES_API_ do_POST_ -- "Chat completions stage now derived from body, not global state." --> OPENAI_CHAT_COMPLETIONS_API_ classDef added stroke:#15AA7A classDef removed stroke:#CD5270 classDef modified stroke:#EDAC4C linkStyle default stroke:#CBD5E1,font-size:13pxExtend
Handlerine2e/mock_llm_server.pyto serve both Chat Completions and Responses traffic for the Hermes openai-api mock, so the nightly E2E keeps working across old and new upstream images. Update the mock server’s turn detection and SSE responses to stay deterministic and surface unsupported inference routes with a 501 instead of a misleading 200.Modified files (1)
Latest Contributors(1)
Modified files (1)
Latest Contributors(1)