fix(lm): handle None usage from Responses API on truncated responses#25
fix(lm): handle None usage from Responses API on truncated responses#25isaacbmiller wants to merge 1 commit into
Conversation
Greptile SummaryThis PR fixes a Confidence Score: 5/5Safe to merge — targeted fix with no logic regressions and solid test coverage for both sync and async paths. All three No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant Caller
participant LM
participant LiteLLM
participant OpenAI Responses API
Caller->>LM: forward() / aforward()
LM->>LiteLLM: litellm.responses / litellm.aresponses
LiteLLM->>OpenAI Responses API: POST /responses
OpenAI Responses API-->>LiteLLM: response (usage=None on truncation)
LiteLLM-->>LM: ResponsesAPIResponse(usage=None)
Note over LM: getattr(response, "usage", {}) → None<br/>None or {} → {}<br/>dict({}) → {} ✅
LM->>LM: history entry: usage = {}
LM->>LM: usage_tracker.add_usage(model, {})
LM-->>Caller: results
Reviews (1): Last reviewed commit: "fix(lm): handle None usage from Response..." | Re-trigger Greptile |
OpenAI Responses API returns usage=None for incomplete/truncated
responses (e.g. max_output_tokens hit or content filter). getattr
returns None when the attribute exists but is None, so dict(None)
raises TypeError. Adding 'or {}' coerces None to an empty dict.
Co-authored-by: factory-droid[bot] <138933559+factory-droid[bot]@users.noreply.github.com>
357da6d to
3ef3f17
Compare
Problem
OpenAI's Responses API returns
usage=Nonefor incomplete/truncated responses (e.g. whenmax_output_tokensis hit or content filtering triggers). LiteLLM passes this through as-is across all supported versions (1.64.0–1.82.6).The existing code
dict(getattr(response, "usage", {}))only guards against the attribute being absent — not against it being explicitlyNone. Sincegetattrreturns the attribute value when it exists (even ifNone), this becomesdict(None)and raisesTypeError: 'NoneType' object is not iterable.Fix
Add
or {}to coerceNoneto an empty dict at all 3 call sites:dspy/clients/base_lm.py:109— history entry constructiondspy/clients/lm.py:197— syncforward()usage trackingdspy/clients/lm.py:238— asyncaforward()usage trackingTests
Added two new tests (
test_responses_api_with_none_usageandtest_responses_api_with_none_usage_async) that mock a truncated Responses API response withusage=Noneand verify the sync and async paths handle it gracefully.