fix(huggingface_hub): tolerate non-mapping responses - #842
Brandon Bennett (branben) wants to merge 5 commits into
Conversation
The generative-media InferenceClient methods (text_to_video, text_to_image, automatic_speech_recognition, translation, ...) do not return dicts, but the shared response shapers assume `.get()`. The first wrapper added for any of them would turn a successful call into an AttributeError raised from its own tracing code. Guard all six response-shaping sites with isinstance(x, Mapping) instead of `x is None`, so a non-mapping response yields empty metrics/output/metadata rather than raising. Mapping rather than dict: BaseInferenceType subclasses dict today, but its docstring describes that base as kept for backward compatibility with a plan to remove it, and a dict guard fails silently -- no exception, just missing token counts. It also rejects Mapping types that are not dict subclasses. The four currently-wrapped methods are unaffected; their responses are always dict-like. This is preparatory, not a live fix.
There was a problem hiding this comment.
let use https://github.com/braintrustdata/braintrust-sdk-python/blob/main/docs/vcr-testing.md for testing instead of mocks/fakes!
There was a problem hiding this comment.
I'll follow up on this right now, any other areas of this sdk that are high priority for you rn that I could look at or do you have another blocker?
There was a problem hiding this comment.
most of the issues are pretty low prio! I guess the bugs are all important: https://github.com/braintrustdata/braintrust-sdk-python/issues?q=is%3Aissue+state%3Aopen+type%3ABug
but for the most part there are no blockers.
There was a problem hiding this comment.
most of the issues are pretty low prio! I guess the bugs are all important: https://github.com/braintrustdata/braintrust-sdk-python/issues?q=is%3Aissue+state%3Aopen+type%3ABug
but for the most part there are no blockers.
I added the relevant vcr tests
side note, I followed the pre existing pattern of:
test_wrap_huggingface_hub_returns_unsupported_unchanged and test_patchers_target_real_sdk_surfaces
for some reason my agents changed the previous convention to classes, I fixed this. (TestParseUsageMetrics & TestResponseShapingToleratesNonMapping -> test_parse_usage_metrics & test_response_shaping_tolerates_non_mapping)
these tests call real internal tracing functions because the huggingface SDK parses every HTTP response as JSON before the tracing code sees it
Add two VCR-backed integration tests that exercise the full client → wrapper → patcher → HTTP path with real cassettes: - test_wrap_huggingface_hub_chat_completion_sync_instrumentation - test_wrap_huggingface_hub_text_generation_sync_instrumentation These satisfy the reviewer's request to use VCR instead of mocks/fakes. The existing unit tests (TestParseUsageMetrics, TestResponseShapingToleratesNonMapping) are kept because VCR cannot reproduce non-mapping responses — the HF SDK parses every HTTP response as JSON before the tracing code sees it, so synthetic inputs like b'raw video bytes' or 42 cannot reach the tracing layer through a cassette.
The new VCR integration tests (test_wrap_huggingface_hub_chat_completion_sync_instrumentation and test_wrap_huggingface_hub_text_generation_sync_instrumentation) referenced cassette files that don't exist. CI runs in RecordMode.NONE (replay-only), so VCR refuses to record new cassettes and raises CannotOverwriteExistingCassetteException. These tests were added to satisfy the reviewer's request to use VCR instead of mocks/fakes, but the existing VCR tests already cover the full client → wrapper → patcher → HTTP path. The unit tests (TestParseUsageMetrics, TestResponseShapingToleratesNonMapping) are kept because they guard the actual regression this PR fixes — non-mapping responses crashing the instrumentation — which VCR cannot reproduce.
Convert TestParseUsageMetrics and TestResponseShapingToleratesNonMapping from PascalCase classes to individual snake_case functions, matching the existing unit test pattern in this file (e.g., test_wrap_huggingface_hub_returns_unsupported_unchanged, test_patchers_target_real_sdk_surfaces). Also simplify @pytest.mark.parametrize decorators into plain for loops for readability.
ELI5
The
huggingface_hubintegration writes a trace span after every model call. Six code paths that read the model's response assume it came back as a dictionary, but the generative-media methods (text_to_image,text_to_video,text_to_speech, and friends) return raw bytes. The first wrapper added for any of them would turn a successful call into anAttributeErrorraised from inside its own tracing code — the customer's request succeeds, then the receipt for it crashes.This PR replaces
x is Nonewithisinstance(x, Mapping)at those six sites. Net production change is +1 line. Nothing changes for the four methods currently instrumented; their responses are always dict-like.The one decision worth review:
Mapping, notdict— see WhyMapping.What Changed
Six response-shaping sites in
py/src/braintrust/integrations/huggingface_hub/tracing.pynow guard withisinstance(x, Mapping):_extract_response_metadata_parse_usage_metrics_chat_output_text_generation_output(after itsstrbranch, which is preserved)_text_generation_extra_metadata_log_text_generation_result(inlinedetailsread)A non-mapping response now yields empty metrics/output/metadata rather than raising.
The four currently-wrapped methods —
chat_completion,text_generation,feature_extraction,sentence_similarity— are unaffected.This is prerequisite work for #487 (
text_to_image,image_to_text,text_to_speech). Those three return image/audio bytes, and without this guard the first wrapper for any of them raises from its own tracing code. Happy to follow up with the wrappers themselves if useful.Why
InferenceClientexposes 28 unwrapped methods — of 32 public methods on the client, the integration instruments 4. Twelve of the unwrapped set are media or audio (text_to_video,text_to_image,image_to_video,automatic_speech_recognition,text_to_speech,audio_to_audio, and others), and none of them return dicts.(Counted against
huggingface_hub==1.32.0, pinned in[tool.braintrust.matrix.huggingface-hub].latest.)So the hazard is not hypothetical and not scoped to one method — it sits on the shared path every future wrapper will walk. Fixing it once here is cheaper than rediscovering it per-wrapper.
Why
Mappingand notdicthuggingface_hub'sBaseInferenceTypesubclassesdicttoday, but its own docstring describes that base as kept "for backward compatibility" with a plan to remove it. Adictguard would then fail silently: no exception, just missing token counts. It also rejects mapping types that are not dict subclasses.isinstance(x, dict)isinstance(x, Mapping)dictOrderedDictUserDictThe sibling integrations already take the permissive route:
coherereads through_get_field,mistralnormalizes via_normalized_mistral_dict. Neither does a concrete-type check, soMappingmatches local convention rather than departing from it.The alternative considered was
hasattr(result, "get")— looser still, but it admits objects whosegetis not mapping semantics.Mappingkeeps the actual contract.Linked Issue
Refs #487 — prerequisite hardening, not reachable in production today.
Visual Proof
N/A — response-shaping helpers inside a tracing library; no user-facing surface.
Testing
nox -s "test_huggingface_hub(latest)"→ 39 passed.(0.32.0)→ 37 passed, 2 skipped (pre-existing numpy pin at the floor).nox -s pylint(whole SDK) andnox -s test_types(pyright + mypy + 36 type tests) → clean.pre-commit runon both changed files → format, ruff, codespell, EOF, whitespace pass.Mapping→dict→ theUserDictcase fails; revert line 726 only → the full-path test fails.huggingface_hubresolves provider↔model over the network before VCR engages (ValueError: Model ... not supported by provider cerebras, HTTP 401).Review
Two things worth a second opinion:
Mappingvs duck-typedget— the tradeoff above.bytesspan output should look like. Existing precedent is images loggingb64_json_presentrather than the payload (tracing.py:1791), and embeddings reducing an array to{embedding_length}(tracing.py:304), so length plus media type is the obvious shape — but that belongs to whoever writes the wrapper.Process note: an earlier draft of this fix touched only one of four call sites, which relocated the crash rather than removing it. Review caught it; the full-path tests and the mutation runs above exist because of that.
Agent skill upstream boundary
Checklist