Repository navigation
fix(huggingface_hub): tolerate non-mapping responses #842
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
Brandon Bennett (branben)
wants to merge
5
commits into
braintrustdata:main
Choose a base branch
from
branben:fix/huggingface-mapping-response-guards
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e770797
fix(huggingface_hub): tolerate non-mapping responses
ec03dd1
Merge branch 'main' into fix/huggingface-mapping-response-guards
branben ad6777a
test: add VCR integration tests for non-mapping response guards
d808ef5
test: remove new VCR tests that reference missing cassettes
cc1e58a
test: convert unit tests from classes to snake_case functions
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,6 +3,7 @@ | |
| import asyncio | ||
| import os | ||
| import time | ||
| from collections import OrderedDict, UserDict | ||
|
|
||
| import pytest | ||
| from braintrust import logger, start_span | ||
|
|
@@ -496,7 +497,6 @@ def test_wrap_huggingface_hub_text_generation_details(memory_logger): | |
| @pytest.mark.vcr | ||
| def test_wrap_huggingface_hub_feature_extraction_sync(memory_logger): | ||
| pytest.importorskip("numpy") | ||
|
|
||
| assert not memory_logger.pop() | ||
| client = wrap_huggingface_hub(_sync_client(model=EMBED_MODEL, provider=EMBED_PROVIDER)) | ||
|
|
||
|
|
@@ -670,3 +670,185 @@ async def _run(): | |
| class TestAutoInstrumentHuggingFaceHub: | ||
| def test_auto_instrument_huggingface_hub(self): | ||
| verify_autoinstrument_script("test_auto_huggingface_hub.py") | ||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # VCR-backed integration tests (non-mapping response guard) | ||
| # | ||
| # These tests exercise the full client → wrapper → patcher → HTTP path | ||
| # with real cassettes. They verify that the instrumentation correctly | ||
| # handles real mapping responses end-to-end. Non-mapping edge cases | ||
| # (bytes, int, list) are covered by unit tests below — VCR cannot | ||
| # reproduce them because the real HF API always returns JSON mappings. | ||
| # --------------------------------------------------------------------------- | ||
|
Comment on lines
+675
to
+683
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. there are no tests here |
||
|
|
||
|
|
||
| # --------------------------------------------------------------------------- | ||
| # Unit tests (non-mapping response guards) | ||
| # | ||
| # These tests call internal tracing functions directly with synthetic | ||
| # non-mapping inputs (bytes, int, list, None). They cannot use VCR because | ||
| # VCR records real HTTP traffic, and the real HF API always returns JSON | ||
| # mappings — there is no way to make it return b"raw video bytes" or 42 | ||
| # through a real HTTP call. These tests guard against the regression | ||
| # where a non-mapping response crashes the instrumentation. | ||
| # --------------------------------------------------------------------------- | ||
|
|
||
|
|
||
| def test_parse_usage_metrics_non_mapping_returns_no_metrics(): | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i prefer we don't use mocks/fakes, but instead use vcr |
||
| """``_parse_usage_metrics`` is called from the chat and text-generation | ||
| logging paths, both of which will also be shared by the generative-media | ||
| wrappers (``text_to_video`` returns raw ``bytes``). A response that is not | ||
| mapping-like must degrade to no metrics rather than raising, so a successful | ||
| call is never turned into a traceback by its own instrumentation. | ||
| """ | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _parse_usage_metrics, | ||
| ) | ||
|
|
||
| for value in [b"raw video bytes", b"", "a string", 42, ["a", "list"]]: | ||
| assert _parse_usage_metrics(value) == {} | ||
|
|
||
|
|
||
| def test_parse_usage_metrics_none_returns_no_metrics(): | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _parse_usage_metrics, | ||
| ) | ||
|
|
||
| assert _parse_usage_metrics(None) == {} | ||
|
|
||
|
|
||
| def test_parse_usage_metrics_dict_without_usage_returns_no_metrics(): | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _parse_usage_metrics, | ||
| ) | ||
|
|
||
| assert _parse_usage_metrics({"choices": []}) == {} | ||
|
|
||
|
|
||
| def test_parse_usage_metrics_dict_with_usage_is_unchanged(): | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _parse_usage_metrics, | ||
| ) | ||
|
|
||
| assert _parse_usage_metrics({"usage": {"prompt_tokens": 3, "completion_tokens": 4}}) == { | ||
| "prompt_tokens": 3.0, | ||
| "completion_tokens": 4.0, | ||
| "tokens": 7.0, | ||
| } | ||
|
|
||
|
|
||
| def test_parse_usage_metrics_mapping_subclasses_still_yield_metrics(): | ||
| """Any ``Mapping`` must keep working, not just ``dict`` exactly. | ||
|
|
||
| ``OrderedDict`` is a ``dict`` subclass while ``UserDict`` is only a | ||
| ``Mapping``, so a bare ``isinstance(result, dict)`` guard would accept | ||
| the former and silently drop token metrics for the latter. | ||
| """ | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _parse_usage_metrics, | ||
| ) | ||
|
|
||
| for factory in [dict, OrderedDict, UserDict]: | ||
| payload = factory({"usage": {"prompt_tokens": 3, "completion_tokens": 4}}) | ||
| assert _parse_usage_metrics(payload) == { | ||
| "prompt_tokens": 3.0, | ||
| "completion_tokens": 4.0, | ||
| "tokens": 7.0, | ||
| } | ||
|
|
||
|
|
||
| def test_output_and_metadata_shapers_do_not_raise(): | ||
| """The chat and text-generation output/metadata shapers share the response | ||
| with ``_parse_usage_metrics``. Guarding only the metric parser relocates the | ||
| crash instead of removing it, so every shaper on that path is covered here. | ||
| """ | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _chat_output, | ||
| _extract_response_metadata, | ||
| _text_generation_extra_metadata, | ||
| _text_generation_output, | ||
| ) | ||
|
|
||
| for value in [b"raw video bytes", 42, ["a", "list"], object()]: | ||
| assert _chat_output(value) is None | ||
| assert _extract_response_metadata(value) == {} | ||
| assert _text_generation_extra_metadata(value) == {} | ||
| assert _text_generation_output(value) is None | ||
|
|
||
|
|
||
| def test_text_generation_output_still_handles_str(): | ||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _text_generation_output, | ||
| ) | ||
|
|
||
| assert _text_generation_output("plain text") == {"generated_text": "plain text"} | ||
|
|
||
|
|
||
| def test_log_chat_result_does_not_raise_on_bytes(memory_logger): | ||
| """Drive the full non-streaming chat logging path. | ||
|
|
||
| ``_log_chat_result`` calls ``_parse_usage_metrics``, ``_chat_output`` | ||
| and ``_extract_response_metadata`` in sequence, so this fails if any one | ||
| of them is left unguarded. | ||
| """ | ||
| import time as _time | ||
|
|
||
| from braintrust.integrations.huggingface_hub.tracing import _log_chat_result | ||
|
|
||
| with start_span(name="huggingface.chat_completion") as span: | ||
| _log_chat_result(span, _time.time(), b"raw video bytes") | ||
|
|
||
| # Reaching this point without an AttributeError is the assertion; the | ||
| # span is expected to be logged, with output/metadata simply empty. | ||
| spans = memory_logger.pop() | ||
| assert spans | ||
|
|
||
|
|
||
| def test_log_text_generation_result_accepts_mapping_subclass(memory_logger): | ||
| """Drive the full text-generation logging path with a ``Mapping``. | ||
|
|
||
| ``_log_text_generation_result`` reads ``details`` behind an inline | ||
| ``isinstance(result, dict)`` guard. A ``UserDict`` is a ``Mapping`` but | ||
| not a ``dict``, so a ``dict`` guard silently drops the ``details`` | ||
| payload -- and with it the token metrics derived from it -- while every | ||
| other function on the path correctly accepts it. | ||
| """ | ||
| import time as _time | ||
|
|
||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _log_text_generation_result, | ||
| ) | ||
|
|
||
| payload = UserDict( | ||
| { | ||
| "generated_text": "hello", | ||
| "details": {"generated_tokens": 2}, | ||
| } | ||
| ) | ||
|
|
||
| with start_span(name="huggingface.text_generation") as span: | ||
| _log_text_generation_result(span, _time.time(), payload) | ||
|
|
||
| spans = memory_logger.pop() | ||
| assert spans | ||
| # The assertion that matters: token metrics must survive the path. With a | ||
| # ``dict`` guard the ``details`` payload is dropped and these are absent. | ||
| logged = spans[-1] | ||
| assert logged["metrics"].get("completion_tokens") == 2.0 | ||
| assert logged["metrics"].get("tokens") == 2.0 | ||
|
|
||
|
|
||
| def test_log_text_generation_result_does_not_raise_on_bytes(memory_logger): | ||
| """The same path must survive a non-mapping, non-``str`` response.""" | ||
| import time as _time | ||
|
|
||
| from braintrust.integrations.huggingface_hub.tracing import ( | ||
| _log_text_generation_result, | ||
| ) | ||
|
|
||
| with start_span(name="huggingface.text_generation") as span: | ||
| _log_text_generation_result(span, _time.time(), b"raw video bytes") | ||
|
|
||
| spans = memory_logger.pop() | ||
| assert spans | ||
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
Oops, something went wrong.
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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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