[BUG] OpenAIResponsesModel reports tool_use instead of max_tokens when a function call is cut off by max_output_tokens · Issue #4135 · strands-agents/harness-sdk · GitHub
You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[BUG] OpenAIResponsesModel reports tool_use instead of max_tokens when a function call is cut off by max_output_tokens #4135
When OpenAIResponsesModel receives response.incomplete with incomplete_details.reason == "max_output_tokens"after a function_call output item has started, the stream reports stopReason: "tool_use" instead of "max_tokens". The event loop then executes the truncated tool call (with input: {} because the partial JSON does not parse) instead of running the max-tokens recovery path.
Minimal reproduction (no network; the OpenAI client is mocked the same way tests/strands/models/test_openai_responses.py does):
messageStop should carry stopReason: "max_tokens", so the event loop replaces the truncated tool use via recover_message_on_max_tokens_reached and raises MaxTokensReachedException, exactly as it does for the other providers in the same situation.
Basis:
OpenAIModel (Chat Completions) reports max_tokens for the same scenario, because it forwards the API's finish_reason == "length" regardless of whether tool-call deltas were accumulated (src/strands/models/openai.py, message_stop mapping). Running the same truncated-tool-call stream through OpenAIModel gives stop_reason = max_tokens.
The event loop's contract is that max_tokens means "tool use blocks may be incomplete or unreliable" and must not be executed (src/strands/event_loop/_recover_message_on_max_tokens_reached.py, and the stop_reason == "max_tokens" branches in event_loop.py).
The OpenAI SDK documents incomplete_details.reason as "max_output_tokens" | "content_filter" — "the reason why the response is incomplete" (openai.types.responses.response.IncompleteDetails). A function call whose arguments were cut off is by definition incomplete.
The existing test test_stream_response_incomplete already asserts max_tokens for the text-only case; the tool-call case is just not covered.
Actual Behavior
Output on main @ e13bfcd (identical with strands-agents==1.54.0):
WARNING strands.event_loop.streaming | tool_name=<write_file>, raw_input=<{"path": "notes.md", "content": "Lorem ipsum dol> | failed to parse tool input json, defaulting to empty dict
messageStop: [{'messageStop': {'stopReason': 'tool_use'}}]
stop_reason: end_turn
user [{'text': 'save my notes'}]
assistant [{'toolUse': {'toolUseId': 'call_1', 'name': 'write_file', 'input': {}}}]
user [{'toolResult': {'toolUseId': 'call_1', 'status': 'error', 'content': [{'text': 'Error: Validation failed for input parameters: 2 validation errors for Write_fileTool\npath\n Field required [type=missing, input_value={}, input_type=dict]\n ...'}]}}]
assistant [{'text': 'Done.'}]
So the truncation is never surfaced: the tool is invoked with empty input, the model gets a validation error as the tool result, and the run ends with end_turn. For a tool with all-optional parameters the tool would actually execute with defaults.
Root cause: src/strands/models/openai_responses.py lines 486–492 give tool_calls precedence over the length stop reason:
tool_calls is populated on response.output_item.added, i.e. before any arguments have streamed, so any function call that is in flight when the response is cut off wins over length.
Additional Context
The TypeScript Responses adapter has the same precedence (strands-ts/src/models/openai/responses-adapter.ts, if (state.toolCalls.size > 0) stopReason = 'toolUse' after maxTokens was set from incomplete_details), so the fix probably wants to be ported.
I'm happy to work on this once a maintainer confirms; the change I verified locally is to let stop_reason == "length" take precedence over tool_calls (existing test_openai_responses.py still passes) plus a test for the truncated-tool-call case.
Possible Solution
See above: check stop_reason == "length" before tool_calls when computing finish_reason.
Related Issues
#4113 (the "failed to parse tool input json" warning) is adjacent but about zero-argument tools; this issue is about the stop reason.
Since the change is two lines plus a regression test, I went ahead and opened #4139 for review rather than waiting. Happy to adjust if you'd prefer a different approach.
Activity
BlueX888 commentedon Sep 3, 2026
Since the change is two lines plus a regression test, I went ahead and opened #4139 for review rather than waiting. Happy to adjust if you'd prefer a different approach.
fix(llamacpp): report max_tokens when a streamed tool call is cut off