Skip to content

fix(lm): handle None usage from Responses API on truncated responses#25

Closed
isaacbmiller wants to merge 1 commit into
mainfrom
isaac/responses-usage-error
Closed

fix(lm): handle None usage from Responses API on truncated responses#25
isaacbmiller wants to merge 1 commit into
mainfrom
isaac/responses-usage-error

Conversation

@isaacbmiller
Copy link
Copy Markdown

Problem

OpenAI's Responses API returns usage=None for incomplete/truncated responses (e.g. when max_output_tokens is 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 explicitly None. Since getattr returns the attribute value when it exists (even if None), this becomes dict(None) and raises TypeError: 'NoneType' object is not iterable.

Fix

Add or {} to coerce None to an empty dict at all 3 call sites:

  • dspy/clients/base_lm.py:109 — history entry construction
  • dspy/clients/lm.py:197 — sync forward() usage tracking
  • dspy/clients/lm.py:238 — async aforward() usage tracking

Tests

Added two new tests (test_responses_api_with_none_usage and test_responses_api_with_none_usage_async) that mock a truncated Responses API response with usage=None and verify the sync and async paths handle it gracefully.

@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Apr 23, 2026

Greptile Summary

This PR fixes a TypeError crash when OpenAI's Responses API returns usage=None for truncated responses (e.g. max_output_tokens exceeded or content filtering). It adds or {} at the three call sites where getattr(response, "usage", {}) is used, so an explicit None value is safely coerced to an empty dict. The PR also bundles a secondary fix in alitellm_completion: the async streaming path now correctly applies _add_dspy_identifier_to_headers before calling _get_stream_completion_fn, matching the existing sync path's behavior.

Confidence Score: 5/5

Safe to merge — targeted fix with no logic regressions and solid test coverage for both sync and async paths.

All three None-usage sites are fixed consistently. Tests cover the sync and async paths end-to-end with realistic mocked truncated responses. The secondary alitellm_completion header fix brings async streaming in line with the sync path. No P0/P1 findings remain.

No files require special attention.

Important Files Changed

Filename Overview
dspy/clients/base_lm.py Single-line fix: adds or {} to coerce None usage to an empty dict in the history entry, matching the fix in lm.py.
dspy/clients/lm.py Applies the or {} guard at two usage-tracker call sites (sync and async forward), and separately fixes async streaming to pass the DSPy identifier headers — bringing it in line with the existing sync path.
tests/clients/test_lm.py Adds two targeted tests (sync and async) that mock a truncated Responses API response with usage=None and assert the fix behaves correctly end-to-end.

Sequence Diagram

sequenceDiagram
    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
Loading

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>
@isaacbmiller isaacbmiller force-pushed the isaac/responses-usage-error branch from 357da6d to 3ef3f17 Compare April 24, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant