fix: unwrap nested OpenAI-style error envelope in extract_detail - #42
Open
shoemoney wants to merge 1 commit into
Open
fix: unwrap nested OpenAI-style error envelope in extract_detail#42shoemoney wants to merge 1 commit into
shoemoney wants to merge 1 commit into
Conversation
The gateway is OpenAI-compatible, so upstream provider errors arrive as
{"error": {"message": "..."}}. extract_detail() pulled the "error" key but
never unwrapped the nested dict, so callers got a Python repr string like
"{'message': 'context length exceeded'}" instead of the actual message.
Mirrors the fix already shipped in the sibling SDKs: otari-sdk-ts's
detailFromObject() (PR mozilla-ai#41, closing mozilla-ai#40) and otari-sdk-rust's
extract_detail(), which already documents this exact shape. Brings Python
in line with both.
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.
What
extract_detail()insrc/otari/_base.pypulls the gateway'sdetail/message/errorkey out of the error body, but never unwrapped theerrorvalue when it's itself a dict. The gateway is OpenAI-compatible, so upstream provider errors arrive as{"error": {"message": "..."}}. That nested dict fell through tostr(detail), which stringifies it as a Python repr instead of the actual message.Anything that logs, displays, or string-matches the error message (e.g. checking for "context length" to trigger a retry-with-truncation) silently breaks on this shape.
Why this is worth fixing now
Both sibling SDKs already handle this correctly, so this brings Python in line with a decision that's already been made twice:
otari-sdk-ts'smapError.tsdetailFromObject()— fixed by Regenerate SDK client core from Otari OpenAPI spec #41, which closed issue Regenerate SDK client core from Otari OpenAPI spec #40 ("Error messages are[object Object]for OpenAI-style error bodies").otari-sdk-rust'score.rsextract_detail(), whose docstring literally names both shapes it recognizes: "the FastAPI/gateway{\"detail\": \"...\"}shape and the OpenAI-style{\"error\": {\"message\": \"...\"}}/{\"error\": \"...\"}shapes."Python was the one SDK still missing it.
The fix
Mirrors the TS
detailFromObject()precedence exactly: if the resolveddetailvalue is itself a dict, pull itsmessagekey; if that's absent or non-string, fall back tojson.dumps(detail)instead ofstr(detail)so the fallback is valid JSON rather than a Python repr.Tests
Added
TestExtractDetailOpenAIEnvelopetotests/unit/test_errors.py, mirroring the TS test cases inclient.test.ts("extracts a useful message from error envelopes"):{"error": {"message": "..."}}-> unwraps to the message{"detail": "..."}shape unchanged{"error": "..."}string unchanged{"message": "..."}unchanged{"error": {"code": 400}}with nomessage-> valid JSON fallback, not a Python reprWhat I ran
Wrote the tests first and confirmed RED before touching the source:
Applied the fix, same file went GREEN:
Then reverted the source fix (kept the tests) to confirm the tests actually catch the regression — same two tests failed with the identical dict-repr output — before restoring the fix.
Full unit suite after restoring the fix:
Also ran
ruff check(clean) andmypy(clean) on the touched files.One more thing
The same gap exists in
otari-sdk-go'serrors.goerrorDetail()(around line 283-303) — it tries to unmarshal the"error"value into a string, fails silently on the nested object, and falls through to returning the raw JSON bytes. Happy to file that as a separate PR if maintainers want it — didn't want to bundle an unrelated repo's fix into this one.