Skip to content

fix(core): enforce tool result adjacency#4638

Closed
he-yufeng wants to merge 1 commit into
QwenLM:mainfrom
he-yufeng:fix/openai-tool-result-adjacency
Closed

fix(core): enforce tool result adjacency#4638
he-yufeng wants to merge 1 commit into
QwenLM:mainfrom
he-yufeng:fix/openai-tool-result-adjacency

Conversation

@he-yufeng

Copy link
Copy Markdown
Contributor

Summary

Fixes #4619.

cleanOrphanedToolCalls() was checking tool call IDs globally, so it could keep a tool result even when another user turn had broken the required assistant-tool adjacency. Anthropic-compatible APIs reject that shape because each tool_result must answer the immediately previous assistant tool_use turn.

This changes the cleanup pass to keep only tool responses that directly follow the assistant message that declared their tool call. It also removes the matching assistant tool call when its response is no longer adjacent.

Tests

  • npm exec -- vitest run packages/core/src/core/openaiContentGenerator/converter.test.ts
  • npm run typecheck --workspace=packages/core
  • npm exec -- eslint packages/core/src/core/openaiContentGenerator/converter.ts packages/core/src/core/openaiContentGenerator/converter.test.ts
  • npm run build --workspace=packages/core

// Filter out tool calls that don't have corresponding responses
const validToolCalls = message.tool_calls.filter(
(toolCall) => toolCall.id && toolResponseIds.has(toolCall.id),
(toolCall) =>

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

[Suggestion] When adjacency enforcement orphans ALL tool calls on an assistant message that has content: '' and reasoning_content: 'thinking...', the fallback check at line 1453 (message.content.trim()) evaluates to falsy and the entire message — including reasoning_content — is silently dropped.

processContent produces content: '' for thinking models (line 677: assistantTextContent || (reasoningParts.length > 0 ? '' : null)). Providers like DeepSeek, Qwen3, and Mimo all consume reasoning_content from the outgoing history. Losing it means the model loses its own chain-of-thought from prior turns.

This is more likely to trigger now than before the PR, because the old global-match approach would have preserved the message.

Suggested change
(toolCall) =>
} else if (
(typeof message.content === 'string' && message.content.trim()) ||
('reasoning_content' in message &&
typeof (message as OpenAI.Chat.ChatCompletionAssistantMessageParam & { reasoning_content?: string }).reasoning_content === 'string' &&
((message as OpenAI.Chat.ChatCompletionAssistantMessageParam & { reasoning_content?: string }).reasoning_content?.length ?? 0) > 0)
) {

— qwen3.7-max via Qwen Code /review

@he-yufeng

Copy link
Copy Markdown
Contributor Author

Closing this duplicate in favor of #4622, which already has the review thread and the same #4619 fix path. Keeping one branch alive should make the follow-up easier to review.

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.

fix(core): validate tool_result adjacency in cleanOrphanedToolCalls to prevent Anthropic API errors

2 participants