Skip to content

fix(openai): keep maxTokens when a Responses function call is cut off (TypeScript) - #4159

Closed
BlueX888 wants to merge 2 commits into
strands-agents:mainfrom
BlueX888:fix/ts-responses-incomplete-tool-call
Closed

BlueX888 wants to merge 2 commits into
strands-agents:mainfrom
BlueX888:fix/ts-responses-incomplete-tool-call

Conversation

@BlueX888

@BlueX888 BlueX888 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Description

TypeScript port of #4139.

When the Responses API stream emits response.incomplete with incomplete_details.reason === 'max_output_tokens' after a function_call output item has started, finalizeResponsesStream reported stopReason: 'toolUse' instead of 'maxTokens'. Model.streamAggregated then either threw a generic ModelError('unable to parse tool input JSON') (partial arguments) or executed the tool with input: {} (cut off before any argument bytes), instead of throwing MaxTokensError.

Root cause: state.toolCalls is populated on response.output_item.added, i.e. before any arguments have streamed, and the finalizer unconditionally overrode the stop reason with 'toolUse' whenever that map was non-empty. So any function call in flight when the response was cut off won over the 'maxTokens' set by the response.incomplete handler.

Changes:

  • responses-adapter.ts: only override with 'toolUse' when the stop reason is not already 'maxTokens'. This matches the chat adapter, which maps finish_reason: 'length' to 'maxTokens' regardless of accumulated tool-call deltas, and the Python Responses model after fix(openai): report max_tokens when a Responses function call is cut off #4139.
  • responses.test.ts: add a regression test for a function call cut off by max_output_tokens, asserting stopReason === 'maxTokens'.

Related Issues

Fixes #4158

Documentation PR

No documentation change needed.

Type of Change

Bug fix

Testing

  • npx vitest run --project unit-node src/models/openai/__tests__/responses.test.ts: 61 passed (60 existing + 1 new). The new test fails on main without the source change (expected 'toolUse' to be 'maxTokens') and passes with it.

  • npx vitest run --project unit-node (full strands-ts unit suite): 165 files, 4560 tests passed, no type errors.

  • eslint src/models/openai, prettier --check on the two changed files, tsc --noEmit --project src/tsconfig.json: clean.

  • Repro from [BUG] OpenAIModel (Responses, TypeScript) reports toolUse instead of maxTokens when a function call is cut off by max_output_tokens #4158 now prints stopReason: 'maxTokens' at the model level, and the agent throws MaxTokensError for both the partial-arguments and no-arguments-yet cases instead of parsing or executing the truncated tool call.

  • I ran hatch run prepare (this is a strands-ts-only change; I ran the corresponding npm run lint / format / type-check / test commands above)

Checklist

  • I have read the CONTRIBUTING document
  • I have reviewed and understand every line of code in this PR, including any generated by AI tools, and I can explain why it works
  • My change is focused and reasonably small; I have split unrelated work into separate PRs
  • I have added any necessary tests that prove my fix is effective or my feature works
  • I have updated the documentation accordingly
  • I have added an appropriate example to the documentation to outline the feature, or no new docs are needed
  • My changes generate no new warnings
  • Any dependent changes have been merged and published

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

… (TypeScript)

finalizeResponsesStream let an in-flight function_call override the
maxTokens stop reason set from response.incomplete, so a truncated tool
call surfaced as toolUse and was parsed/executed instead of raising
MaxTokensError. Port of strands-agents#4139 to strands-ts.

Fixes strands-agents#4158
@BlueX888
BlueX888 requested a review from a team as a code owner September 4, 2026 02:10
@BlueX888
BlueX888 requested a review from lizradway September 4, 2026 02:10
@github-actions github-actions Bot added area-model Related to models or model providers typescript Pull requests that update typescript code bug Something isn't working labels Sep 4, 2026
@github-actions github-actions Bot added complexity/low Touched functions have low cognitive complexity (<=10) size/xs strands-running labels Sep 14, 2026
})
const model = new OpenAIModel({ api: 'responses', client })
const events = await collectIterator(model.stream([new Message({ role: 'user', content: [new TextBlock('x')] })]))
const stop = events.find((e: any) => e.type === 'modelMessageStopEvent') as any

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Issue: The regression test covers the partial-arguments case, but issue #4158 describes a second distinct scenario — the function call cut off before any argument bytes stream (TRUNCATED_NO_ARGS: output_item.added then response.incomplete, with no function_call_arguments.delta). That path previously executed the tool with input: {} and ended with endTurn, which is arguably the more dangerous of the two regressions.

Suggestion: Add a second test case (or a parameterized case) for the no-argument-bytes variant asserting stopReason === 'maxTokens'. Both variants share the same finalizer code path, so this is non-blocking, but it locks in the behavior for the silent-execution case the issue specifically called out.

@github-actions

Copy link
Copy Markdown
Contributor

Assessment: Approve

Minimal, correct, well-scoped bug fix. The one-line precedence change makes the Responses adapter agree with the chat adapter (finish_reason: 'length' → maxTokens regardless of accumulated tool-call deltas) and mirrors the Python fix in #4139. I confirmed in model.ts that streamAggregated checks maxTokens (L613) before the tool-JSON-parse error (L620), so this correctly surfaces MaxTokensError for both truncation scenarios described in #4158.

Review Categories
  • Correctness: Fix resolves the stop-reason precedence bug and restores cross-adapter parity.
  • Maintainability: Evergreen comment clearly documents why maxTokens must win over toolUse.
  • Testing: New regression test follows existing file conventions; one non-blocking suggestion to also cover the no-argument-bytes variant explicitly called out in the issue.

Clean, focused fix that closes a real parity gap between the two OpenAI adapters.

@lizradway
lizradway enabled auto-merge (squash) September 14, 2026 17:51
@BlueX888

Copy link
Copy Markdown
Contributor Author

Approval noted on a0b5774; no changes were requested, so none were made. gh pr checks: 5 pass (2 Approvers for Bots, Check access, Flag editorial catalog fields, check-api-review-label). "Trigger Strands Review" (run 34919657464) is status=waiting on the first-time-contributor workflow-approval gate and needs a maintainer to approve the run — nothing to fix on my side. The non-blocking inline note on strands-ts/src/models/openai/__tests__/responses.test.ts (no-argument-bytes variant) is left unapplied unless you want it covered.

@BlueX888

Copy link
Copy Markdown
Contributor Author

State check, then one question.

  • lizradway's APPROVED review is recorded against a0b5774, which is still the branch head.
  • That commit's committer date is 2026-09-15T02:02Z, about 6h52m after the approval at 2026-09-14T19:10Z.
  • All nine TypeScript jobs pass, including the six-job test matrix. CI Gate failed its first attempt; the later CI Gate run on this head passed.
  • Trigger Strands Review is the only check still red: environment-gated, no steps ran, recorded failed 30 days after queuing.

Since main requires approval of the last push, does the existing approval still satisfy the ruleset, or would you prefer a fresh approval after the last push? Nothing needs re-pushing on our side; we can re-run the gate if that is simpler.

@yonib05

yonib05 commented Nov 10, 2026

Copy link
Copy Markdown
Member

Sorry, this one is on us. Your fix was approved in September and we never merged it, then shipped the same change in #5749 (released in typescript/v1.24.0) without linking back here. Closing as superseded. Thank you for the clear repro and the fix.

@yonib05 yonib05 closed this Nov 10, 2026
auto-merge was automatically disabled November 10, 2026 14:19

Pull request was closed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-model Related to models or model providers bug Something isn't working complexity/low Touched functions have low cognitive complexity (<=10) size/xs typescript Pull requests that update typescript code

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[BUG] OpenAIModel (Responses, TypeScript) reports toolUse instead of maxTokens when a function call is cut off by max_output_tokens

3 participants