Skip to content

fix: read the renamed Otari-Request-ID and Otari-Attempt-ID response headers - #100

Merged
peteski22 merged 3 commits into
mainfrom
fix/96-renamed-response-headers
Sep 25, 2026
Merged

peteski22 merged 3 commits into
mainfrom
fix/96-renamed-response-headers

Conversation

@peteski22

Copy link
Copy Markdown
Contributor

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_get returns None for a header that is not present, so with_response_metadata(...).request_id would simply be None on 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-ID in place of X-Otari-Request-ID at four call sites, covering the sync and async clients, streaming and non-streaming. Read Otari-Attempt-ID in place of X-Correlation-ID in the error mapper, and carry that value as attempt_id rather than correlation_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, while Otari-Request-ID names the whole resolve call containing it.

Both names are now module constants in _base.py, beside the existing GATEWAY_HEADER_NAME, rather than literals repeated at each call site. The README example names the new header.

No public surface changes. with_response_metadata and the streaming wrapper keep their current API, and correlation_id was never an exposed attribute here, only a local and a fragment of the error message text.

Notes

  • Tests: 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.
  • Checks: 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" and git grep -i "x-correlation-id" find nothing.
  • Test literals: the unit tests keep spelled-out header names rather than importing the new constants, so a typo in a constant still fails a test. One test on each client keeps a lowercase spelling, holding the case-insensitivity coverage that _header_get provides.
  • Release ordering: the issue requires this ship with or after the gateway release carrying Rename the released X-Otari-* headers to Otari-*, and write the header naming rule down otari#1485. That is v0.12.0, released 2026-09-25, so the ordering is satisfied.
  • Existing lockfile mismatch: main's uv.lock records the root package as 0.2.0 while pyproject.toml is 0.4.0. Runs used UV_FROZEN=true to avoid an unrelated lockfile edit, and test: cover container serialization in messages #94 already carries that sync.
  • Not covered here: Otari-Guardrails, Otari-Container-Id and Otari-Container-Expires-At were renamed in the same gateway change but are not read by this SDK.

Fixes #96

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.
@peteski22
peteski22 requested a review from tbille September 25, 2026 15:01
@peteski22
peteski22 merged commit be1c262 into main Sep 25, 2026
3 checks passed
@peteski22
peteski22 deleted the fix/96-renamed-response-headers branch September 25, 2026 15:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Read the renamed Otari-Request-ID and Otari-Attempt-ID headers; the X- prefixed names are going away

2 participants