fix(service): honour x-routerly-no-trace header in streaming sse output#111
fix(service): honour x-routerly-no-trace header in streaming sse output#111AmSach wants to merge 1 commit into
Conversation
|
|
the x-routerly-no-trace request header was documented but never read.
emit() in packages/service/src/routes/openai.ts unconditionally wrote
{"type":"trace",...} sse frames into the wire stream, breaking strict
openai-compatible clients (@ai-sdk/openai-compatible, openai strict mode, etc.)
that validate the first data: line as a ChatCompletionChunk with a choices[] array.
this affected every stream request through opencode, vercel ai sdk, and any
sdk that does schema validation on sse frames — see issue Inebrio#110.
fix: read the header near the other header reads and gate the
reply.raw.write call. trace entries are still recorded in the in-memory
traceStore via appendTrace(traceId, [entry]) so the dashboard / debug
surface is unaffected.
closes Inebrio#110
Signed-off-by: Aman Sachan <amansachan92905@gmail.com>
2fd05cf to
441a591
Compare
carlosatta
left a comment
There was a problem hiding this comment.
Good catch, and thanks for the thorough PR description.
Fix is correct and well-scoped.
- The
suppressTraceflag is read once from the header, placed correctly next to the existingx-routerly-conversation-idread — consistent with the pattern already in the file. appendTrace(traceId, [entry])is still called unconditionally, so the in-memory trace store is unaffected. Dashboard and debug surfaces work as before.- Both streaming endpoints (
/v1/chat/completionsand/v1/responses) go through the samehandleOpenAICompletionfunction and share the singleemitclosure, so the fix covers both. - The non-streaming
emitat the bottom of the function does not callreply.raw.write— not a concern.
Tests are well-designed. The all-candidates-exhausted path (via mockLlmStream.mockRejectedValue) is a reliable way to guarantee emit() fires on the wire path. The companion test confirming traces ARE emitted by default is the right pairing — it keeps the fix from silently becoming unconditional suppression in a future refactor.
Minor notes (non-blocking):
- The PR only adds the test against the error path. A test against a successful stream would give complete coverage of the guard, but it would require a more complex mock setup and is not strictly necessary since the guard is inside the closure and applies to all callers.
- The PR description mentions the Anthropic route potentially being affected — it is not. The
emitinanthropic.tsonly callsappendTrace, neverreply.raw.write, so no SSE frames leak there.
Looks good to merge. I am targeting this for the 0.3.0 release.
|
Thank you for this contribution, Aman — you correctly identified and fixed the issue. After reviewing the diff, we found that the same fix had already been applied to Your root cause analysis and the learning document in |
Summary
Fixes #110 — the documented
x-routerly-no-tracerequest header was never read, soemit()inpackages/service/src/routes/openai.tsunconditionally wrote{"type":"trace",...}SSE frames into the wire stream. Strict OpenAI-compatible clients (@ai-sdk/openai-compatibleused by opencode, openai npm package in strict mode, etc.) validate the firstdata:line as aChatCompletionChunkwith achoices[]array and abort the stream as soon as they hit a trace event.Root cause
The header check was completely missing, despite the source code having been documented to support it.
Fix
traceStoreviaappendTrace(...)— dashboard and debug surface unaffected.x-routerly-conversation-idread at line 119), matching the existing header-parsing pattern in the file.traceIdand the in-memory trace history remain accessible via thex-routerly-trace-idresponse header.Tests
Two new tests added in
packages/service/src/routes/openai.test.ts:suppresses trace events from sse stream when x-routerly-no-trace: 1 is sent— sends the header, triggers the all-candidates-exhausted path (which callsemit()directly via the route handler), and asserts the SSE body does NOT contain"type":"trace".emits trace events on sse stream when x-routerly-no-trace header is absent— companion test that confirms trace events ARE still written to the wire by default, so the suppression is header-gated (not unconditional).Both tests were verified to:
expected 'data: {"type":"trace",... not to contain '"type":"trace"'when the fix is reverted — confirming it actually catches the bug.Verification
npm run typecheck --workspace=packages/service— cleannpm test --workspace=packages/service— 50/50 test files, 1079/1079 tests pass (was 1077, +2 new tests, no regressions)Files changed
packages/service/src/routes/openai.ts(+4 / −1): one header read, oneif (!suppressTrace)guard around the SSE write.packages/service/src/routes/openai.test.ts(+39): two new regression tests.ai/learnings/ERRORS.md: ERR-20260628-001 entry.Diff summary
Fixes #110