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 is 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 (for example checking for "context length" to trigger a retry-with-truncation) silently breaks on this shape.
Both sibling SDKs already handle it.
otari-sdk-tsfixed it inmapError.tsdetailFromObject()(#41, closing issue #40, "Error messages are[object Object]for OpenAI-style error bodies").otari-sdk-rust'score.rsextract_detail()documents 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: if the resolveddetailvalue is a dict, pull itsmessagekey. If that is absent or not a 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 reprWrote the tests first and confirmed they fail before touching the source:
With the fix applied:
Reverting the source fix (tests kept) reproduced the same two failures with the identical dict-repr output, then the fix was restored. Full unit suite after that:
ruff checkandmypyare clean on the touched files.Note
The same gap exists in
otari-sdk-go'serrors.goerrorDetail()(around lines 283-303): it tries to unmarshal the"error"value into a string, fails silently on the nested object, and returns the raw JSON bytes. Happy to file that as a separate PR if wanted; it is not bundled here since it is a different repo.