fix: read the renamed Otari-Request-ID and Otari-Attempt-ID response headers - #100
Merged
Merged
Conversation
Otari renamed its X- prefixed response headers to follow RFC 6648, with no deprecation window, so X-Otari-Request-ID stops being sent as of gateway v0.12.0. This SDK read the old name on four paths: the sync and async with_response_metadata calls, and the sync and async streaming wrappers. The break would have been silent. _header_get returns None for a header that is not present, so request_id would simply be None on every response, with nothing raised for a caller to follow. Header lookup is case-insensitive, so only the looked-up strings change. One test on each client keeps a lowercase spelling to hold that coverage.
X-Correlation-ID became Otari-Attempt-ID in gateway v0.12.0. That rename changes more than the prefix. The header names the single provider attempt that served the request, while Otari-Request-ID names the whole resolve call containing it. "Correlation" said nothing about which of the two granularities a reader was holding. The error mapper now looks up the new name and carries the value as attempt_id, including in the error message a caller reads. That value was never a public attribute here, only a local and a fragment of the message text, so no typed surface changes for callers. As with the request ID, an unread header raised nothing: the detail line simply lost its trailing identifier.
The request-ID header name was repeated at four call sites across the sync and async clients, and the attempt-ID name sat inline in the error mapper. Any future rename means finding every literal again. _base.py already names the request header it sends as GATEWAY_HEADER_NAME, so the two response headers it reads now sit beside it. The attempt-ID constant has a single call site; it is named anyway so both halves of the pair read alike. The tests keep their header literals on purpose. Asserting against the constant would let a typo in the constant pass.
tbille
approved these changes
Sep 25, 2026
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.
Why
Otari renamed its
X-prefixed response headers to follow RFC 6648 (BCP 178), with no deprecation window, and that shipped in gateway v0.12.0 via mozilla-ai/otari#1667. This SDK read two of the old names, so both values come back empty against a current gateway.The break is silent, which is what makes it worth fixing ahead of a report.
_header_getreturnsNonefor a header that is not present, sowith_response_metadata(...).request_idwould simply beNoneon every response, and a failed call would lose its attempt identifier, with nothing raised for a caller to follow.Codegen cannot carry this. The codegen workflow regenerates
src/otari/_client/only, and these header names appear in no generated artifact at all, which is tracked upstream as mozilla-ai/otari#1656.What changed
Read
Otari-Request-IDin place ofX-Otari-Request-IDat four call sites, covering the sync and async clients, streaming and non-streaming. ReadOtari-Attempt-IDin place ofX-Correlation-IDin the error mapper, and carry that value asattempt_idrather thancorrelation_id, including in the error message a caller reads. That rename is more than the prefix: the header names the single provider attempt that served the request, whileOtari-Request-IDnames the whole resolve call containing it.Both names are now module constants in
_base.py, beside the existingGATEWAY_HEADER_NAME, rather than literals repeated at each call site. The README example names the new header.No public surface changes.
with_response_metadataand the streaming wrapper keep their current API, andcorrelation_idwas never an exposed attribute here, only a local and a fragment of the error message text.Notes
UV_FROZEN=true uv run pytest: 139 passed, 7 integration tests skipped (no gateway). Each rename was driven test-first, and the red runs showed the exact silent failure:assert None == 'req-stream-123'across nine cases for the request ID, and a message of'[gateway] no funds'with the identifier absent for the attempt ID.UV_FROZEN=true uv run ruff check .passed;UV_FROZEN=true uv run mypy src/passed (9 files). Both acceptance greps from the issue are clean:git grep -i "x-otari"andgit grep -i "x-correlation-id"find nothing._header_getprovides.uv.lockrecords the root package as 0.2.0 whilepyproject.tomlis 0.4.0. Runs usedUV_FROZEN=trueto avoid an unrelated lockfile edit, and test: cover container serialization in messages #94 already carries that sync.Otari-Guardrails,Otari-Container-IdandOtari-Container-Expires-Atwere renamed in the same gateway change but are not read by this SDK.Fixes #96