OPE-101: extract token usage in the OpenAI Responses provider

Any model routed through /v1/responses (gpt-5.6+ with tools, i.e. the
default gpt-5.6-sol) reported 0 tokens for every call: both
AssistantTurn constructions omitted usage while every sibling adapter
populated TokenUsage. First consumer to notice was the Auto-Approve
reviewer metering; the 2026-08-13 eval report printed 'Tokens: 0 in /
0 out' across ~40 live calls.

_usage_from maps the Responses shape like the Chat Completions adapter:
fresh input = input_tokens - cached_tokens (input_tokens is INCLUSIVE
of the cached share), cached share -> cache_read, output_tokens as-is
(reasoning already included). Defensive reads: compat servers may omit
input_tokens_details; a missing usage object stays None, never a fake
zero. Wired in _parse_response, which both complete() and the stream's
terminal response.completed event flow through - one extraction, both
paths.

Tests: cache split, partial/missing degradation, stream terminal event,
stream-without-terminal stays None. Verified live against gpt-5.6-sol:
call 1 'in=1820 cache_read=0', call 2 'in=3 cache_read=1817' - the
0-token era ends and the cache split reports correctly.
This commit is contained in:
Devika Verma
2026-08-17 22:57:18 +05:30
parent 9702c86c7f
commit a990f6872f
2 changed files with 75 additions and 0 deletions
+55
View File
@@ -273,6 +273,39 @@ def test_complete_text_turn_and_request_shape():
assert fake.kwargs["reasoning"] == {"summary": "auto"}
def test_complete_extracts_usage_with_cache_split():
# OPE-101: the Responses API reports `input_tokens` INCLUSIVE of the cached share;
# normalized like every other adapter — fresh input = input cached, cache_read
# carries the cached share. Before this, the field was dropped entirely and every
# Responses-routed model metered as 0 tokens.
resp = _response([_message_item("hello")])
resp.usage = SimpleNamespace(
input_tokens=1500,
output_tokens=80,
input_tokens_details=SimpleNamespace(cached_tokens=1400),
)
provider = OpenAIResponsesProvider(client=_FakeClient(response=resp))
turn = provider.complete(model="m", messages=[{"role": "user", "content": "hi"}])
assert turn.usage is not None
assert (turn.usage.input, turn.usage.output, turn.usage.cache_read) == (100, 80, 1400)
def test_complete_usage_degrades_on_partial_or_missing_fields():
# Compat/older servers may omit `input_tokens_details` or the whole usage object —
# never a crash, and absence stays None (not a fake zero-usage).
resp = _response([_message_item("x")])
resp.usage = SimpleNamespace(input_tokens=500, output_tokens=20) # no details
provider = OpenAIResponsesProvider(client=_FakeClient(response=resp))
turn = provider.complete(model="m", messages=[{"role": "user", "content": "hi"}])
assert (turn.usage.input, turn.usage.output, turn.usage.cache_read) == (500, 20, 0)
bare = _response([_message_item("y")]) # SimpleNamespace without a usage attr at all
turn2 = OpenAIResponsesProvider(client=_FakeClient(response=bare)).complete(
model="m", messages=[{"role": "user", "content": "hi"}]
)
assert turn2.usage is None
def test_complete_parses_function_calls_with_call_ids():
fake = _FakeClient(
response=_response(
@@ -498,6 +531,28 @@ def test_stream_without_terminal_event_keeps_accumulated_text():
provider.stream(model="m", messages=[{"role": "user", "content": "x"}])
)[-1].turn
assert turn.text == "partial" and turn.finish_reason is None
assert turn.usage is None # nothing terminal arrived — no usage to invent
def test_stream_terminal_event_carries_usage():
# OPE-101 streaming path: usage rides the terminal `response.completed` event's full
# response object, which the stream parses whole — same extraction as complete().
final = _response([_message_item("done")])
final.usage = SimpleNamespace(
input_tokens=1430,
output_tokens=65,
input_tokens_details=SimpleNamespace(cached_tokens=1408),
)
events = [
SimpleNamespace(type="response.output_text.delta", delta="done"),
SimpleNamespace(type="response.completed", response=final),
]
provider = OpenAIResponsesProvider(client=_FakeClient(events=events))
turn = list(
provider.stream(model="m", messages=[{"role": "user", "content": "x"}])
)[-1].turn
assert turn.usage is not None
assert (turn.usage.input, turn.usage.output, turn.usage.cache_read) == (22, 65, 1408)
def test_stream_requests_stream_flag():