mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(models): tolerate null Codex response usage (#6335)
This commit is contained in:
parent
2a81f913a5
commit
d8bdd7552c
@ -309,6 +309,7 @@ For Google's official Gemini OpenAI-compatible endpoint, use the
|
||||
```
|
||||
|
||||
- Codex CLI reads `~/.codex/auth.json`
|
||||
- Completed Codex responses still return their text and tool calls when token usage is null, omitted, or empty; usage metadata remains unavailable.
|
||||
- The Codex model provider returns completed responses without waiting for the SSE connection to close. Failed or incomplete responses report the provider's error or reason; partial output is not returned as a successful answer. Non-object error details are reported as text.
|
||||
- Claude Code accepts `CLAUDE_CODE_OAUTH_TOKEN`, `ANTHROPIC_AUTH_TOKEN`, `CLAUDE_CODE_CREDENTIALS_PATH`, or `~/.claude/.credentials.json`
|
||||
- `CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR` accepts a UTF-8 token handoff and reuses it for later model instances in the same process. Undecodable handoffs are skipped so Claude Code can still try its override or default credentials file.
|
||||
|
||||
@ -78,6 +78,12 @@ Coverage: `tests/test_codex_provider.py`.
|
||||
|
||||
### Codex SSE termination (`packages/harness/deerflow/models/openai_codex_provider.py`)
|
||||
|
||||
Completed responses with null, omitted, or empty `usage` retain their text,
|
||||
reasoning, and tool calls. Normalize unavailable usage to the existing empty
|
||||
mapping fallback while keeping `AIMessage.usage_metadata` as `None`; populated
|
||||
usage mappings, including zero counts and cached/reasoning token details, remain
|
||||
unchanged.
|
||||
|
||||
`response.completed` ends stream consumption immediately, before transport EOF;
|
||||
retain the output-item recovery path for empty completed output. Terminal
|
||||
`response.failed`, `response.incomplete`, and `error` events raise with their
|
||||
|
||||
@ -421,7 +421,7 @@ class CodexChatModel(BaseChatModel):
|
||||
}
|
||||
)
|
||||
|
||||
usage = response.get("usage", {})
|
||||
usage = response.get("usage") or {}
|
||||
usage_metadata = _build_usage_metadata(usage) if usage else None
|
||||
additional_kwargs = {}
|
||||
if reasoning_content:
|
||||
|
||||
@ -211,3 +211,65 @@ def test_completed_response_recovers_tool_calls_and_reasoning_before_closing(mod
|
||||
assert result.additional_kwargs["reasoning_content"] == "Save the completed report"
|
||||
assert stream.closed
|
||||
assert not stream.read_past_events
|
||||
|
||||
|
||||
@pytest.mark.parametrize("usage_shape", ["null", "omitted", "empty"])
|
||||
@pytest.mark.parametrize("output_kind", ["text", "tool", "mixed"])
|
||||
@pytest.mark.parametrize("recover_streamed_output", [False, True])
|
||||
def test_completed_response_without_usage_preserves_output(model, serve_events, usage_shape, output_kind, recover_streamed_output):
|
||||
tool_call = {"type": "function_call", "name": "save_report", "call_id": "call-report", "arguments": '{"name": "report.md"}'}
|
||||
reasoning = {"type": "reasoning", "summary": [{"type": "summary_text", "text": "Save the completed report"}]}
|
||||
items = {
|
||||
"text": [message_item("Report ready")],
|
||||
"tool": [tool_call],
|
||||
"mixed": [reasoning, message_item("Report ready"), tool_call],
|
||||
}[output_kind]
|
||||
response = {"id": "resp-no-usage", "status": "completed", "model": "gpt-5.4", "output": [] if recover_streamed_output else items}
|
||||
if usage_shape != "omitted":
|
||||
response["usage"] = None if usage_shape == "null" else {}
|
||||
events = [{"type": "response.output_item.done", "output_index": index, "item": item} for index, item in enumerate(items)] if recover_streamed_output else []
|
||||
events.append({"type": "response.completed", "response": response})
|
||||
stream, requests = serve_events(events, tail_error=httpx.ReadTimeout("Connection stayed open after completion"))
|
||||
|
||||
result = model.invoke([HumanMessage(content="Prepare a report")])
|
||||
|
||||
assert result.content == ("Report ready" if output_kind != "tool" else "")
|
||||
expected_calls = [{"type": "tool_call", "name": "save_report", "id": "call-report", "args": {"name": "report.md"}}] if output_kind != "text" else []
|
||||
assert result.tool_calls == expected_calls
|
||||
assert result.invalid_tool_calls == []
|
||||
assert result.additional_kwargs == ({"reasoning_content": "Save the completed report"} if output_kind == "mixed" else {})
|
||||
assert result.usage_metadata is None
|
||||
assert result.response_metadata["usage"] == {}
|
||||
assert result.response_metadata["model"] == "gpt-5.4"
|
||||
assert result.response_metadata["token_usage"] == {"prompt_tokens": 0, "completion_tokens": 0, "total_tokens": 0}
|
||||
assert len(requests) == 1
|
||||
assert stream.closed
|
||||
assert not stream.read_past_events
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_async_completed_response_without_usage_preserves_output(model, serve_events):
|
||||
stream, requests = serve_events([{"type": "response.completed", "response": {"status": "completed", "output": [message_item("Report ready")], "usage": None}}])
|
||||
|
||||
result = await model.ainvoke([HumanMessage(content="Prepare a report")])
|
||||
|
||||
assert result.content == "Report ready"
|
||||
assert result.usage_metadata is None
|
||||
assert result.response_metadata["usage"] == {}
|
||||
assert len(requests) == 1
|
||||
assert stream.closed
|
||||
assert not stream.read_past_events
|
||||
|
||||
|
||||
@pytest.mark.parametrize("tokens", [0, 7])
|
||||
def test_completed_response_usage_mapping_preserves_counts(model, serve_events, tokens):
|
||||
usage = {"input_tokens": tokens, "output_tokens": tokens, "total_tokens": 2 * tokens, "input_tokens_details": {"cached_tokens": tokens}, "output_tokens_details": {"reasoning_tokens": tokens}}
|
||||
stream, _ = serve_events([{"type": "response.completed", "response": {"status": "completed", "output": [message_item("Report ready")], "usage": usage}}])
|
||||
|
||||
result = model.invoke([HumanMessage(content="Prepare a report")])
|
||||
|
||||
assert result.usage_metadata == {"input_tokens": tokens, "output_tokens": tokens, "total_tokens": 2 * tokens, "input_token_details": {"cache_read": tokens}, "output_token_details": {"reasoning": tokens}}
|
||||
assert result.response_metadata["usage"] == usage
|
||||
assert result.response_metadata["token_usage"] == {"prompt_tokens": tokens, "completion_tokens": tokens, "total_tokens": 2 * tokens}
|
||||
assert stream.closed
|
||||
assert not stream.read_past_events
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user