fix(proxy): coerce non-str x-litellm-* header values to avoid httpx TypeError (#27458) - #27504
Conversation
…headers (BerriAI#27458) StandardLoggingUserAPIKeyMetadata mixes typed fields (user_api_key_spend / user_api_key_max_budget are floats, user_api_key_auth_metadata is a dict) and these were dropped into outgoing request headers without coercion. httpx then raises 'Header value must be str or bytes, not <class 'float'>' when add_user_information_to_llm_headers=True. Coerce numbers/bools via str(); JSON-encode dict/list. Existing test now also asserts every emitted header value is str/bytes.
Greptile SummaryThis PR fixes a
Confidence Score: 4/5Safe to merge — the coercion logic is correct and targeted, the test change strengthens rather than weakens coverage, and the fix addresses a real crash in an opt-in feature path. The production change is minimal and correct: a well-ordered type dispatch replaces a raw assignment. The test update properly reflects the new coerced output and adds a type guard that would have caught this regression earlier. The only loose end is that litellm.add_user_information_to_llm_headers is left set globally after the test, which could silently affect other tests sharing the same process state. No files require special attention beyond the minor test teardown gap in test_proxy_utils.py.
|
| Filename | Overview |
|---|---|
| litellm/proxy/litellm_pre_call_utils.py | Adds type-coercion logic before assigning x-litellm-* header values — dicts/lists → JSON string, str/bytes → unchanged, everything else → str(v). Fixes the httpx TypeError for float and dict fields. |
| tests/proxy_unit_tests/test_proxy_utils.py | Test updated to assert all emitted header values are str/bytes (new regression guard) and expected map updated to coerced shapes. Global test state litellm.add_user_information_to_llm_headers is set but not restored after the test. |
Comments Outside Diff (1)
-
tests/proxy_unit_tests/test_proxy_utils.py, line 573-576 (link)The test sets
litellm.add_user_information_to_llm_headers = Trueglobally but never resets it. If a later test in the same session expects the default (FalseorNone), it can pick up this state and produce unexpected results. Wrapping the assignment in atry/finally(or using a fixture) keeps tests isolated.
Reviews (1): Last reviewed commit: "fix(proxy): coerce non-str values when i..." | Re-trigger Greptile
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
8044529
into
BerriAI:shin_agent_oss_staging_05_09_2026
|
🤖 litellm-agent: Squash-merged into staging branch Triage Summary 25 lines across 2 files (+22 / -3) Merge Confidence: 5/5 ✅ READY All checks green. Greptile 4/5, no blocking pattern findings, no CircleCI runs (OSS-typical). |
…ypeError (BerriAI#27458) (BerriAI#27504) Squash-merged by litellm-agent from Anai-Guo's PR.
Summary
Fixes #27458 — when
add_user_information_to_llm_headers: true, the proxy injectsx-litellm-*headers fromget_sanitized_user_information_from_key(), but several fields inStandardLoggingUserAPIKeyMetadataare non-string by definition:user_api_key_spendOptional[float]user_api_key_max_budgetOptional[float]user_api_key_auth_metadataOptional[Dict[str, str]]These got assigned directly into
returned_headersand then passed tohttpx.AsyncClient.build_request(), which raises:The reporter's traceback hits
httpx/_models.py:_normalize_header_valueexactly at thefloatbranch, which matchesuser_api_key_spend = 0.0even on a fresh key.Fix
In
add_headers_to_llm_call, coerce values before assigning into headers:dict/list→json.dumps(v)str/bytes→ unchangedfloat,int,bool, …) →str(v)Test
tests/proxy_unit_tests/test_proxy_utils.py::test_foward_litellm_user_info_to_backend_llm_callpreviously expected a float and a dict in the output map (it never actually fed the result to httpx, so the regression slipped through). Updated to:str/bytes— would have caught this regression."0.0","{}").🤖 Generated with Claude Code