-
Notifications
You must be signed in to change notification settings - Fork 216
fix(ai): strip openai itemID from tool call metadata #889
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@workflow/ai": patch | ||
| --- | ||
|
|
||
| strip OpenAI itemId from providerMetadata to fix Responses API tool call errors | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -300,18 +300,23 @@ export async function* streamTextIterator({ | |||||||
| // Note: providerMetadata from the tool call is mapped to providerOptions | ||||||||
| // in the prompt format, following the AI SDK convention. This is critical | ||||||||
| // for providers like Gemini that require thoughtSignature to be preserved | ||||||||
| // across multi-turn tool calls. | ||||||||
| // across multi-turn tool calls. Some fields are sanitized before mapping. | ||||||||
| conversationPrompt.push({ | ||||||||
| role: 'assistant', | ||||||||
| content: toolCalls.map((toolCall) => ({ | ||||||||
| type: 'tool-call', | ||||||||
| toolCallId: toolCall.toolCallId, | ||||||||
| toolName: toolCall.toolName, | ||||||||
| input: JSON.parse(toolCall.input), | ||||||||
| ...(toolCall.providerMetadata != null | ||||||||
| ? { providerOptions: toolCall.providerMetadata } | ||||||||
| : {}), | ||||||||
| })), | ||||||||
| content: toolCalls.map((toolCall) => { | ||||||||
| const sanitizedMetadata = sanitizeProviderMetadataForToolCall( | ||||||||
| toolCall.providerMetadata | ||||||||
| ); | ||||||||
| return { | ||||||||
| type: 'tool-call', | ||||||||
| toolCallId: toolCall.toolCallId, | ||||||||
| toolName: toolCall.toolName, | ||||||||
| input: JSON.parse(toolCall.input), | ||||||||
| ...(sanitizedMetadata != null | ||||||||
| ? { providerOptions: sanitizedMetadata } | ||||||||
| : {}), | ||||||||
| }; | ||||||||
| }) as typeof toolCalls, | ||||||||
| }); | ||||||||
|
|
||||||||
| // Yield the tool calls along with the current conversation messages | ||||||||
|
|
@@ -474,3 +479,39 @@ function normalizeFinishReason(raw: unknown): FinishReason | undefined { | |||||||
| } | ||||||||
| return undefined; | ||||||||
| } | ||||||||
|
|
||||||||
| /** | ||||||||
| * Strip OpenAI's itemId from providerMetadata (requires reasoning items we don't preserve). | ||||||||
| * Preserves all other provider metadata (e.g., Gemini's thoughtSignature). | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||
| */ | ||||||||
| function sanitizeProviderMetadataForToolCall( | ||||||||
| metadata: unknown | ||||||||
| ): Record<string, unknown> | undefined { | ||||||||
| if (metadata == null) return undefined; | ||||||||
|
|
||||||||
| const meta = metadata as Record<string, unknown>; | ||||||||
|
|
||||||||
| // Check if OpenAI metadata exists and needs sanitization | ||||||||
| if ('openai' in meta && meta.openai != null) { | ||||||||
| const { openai, ...restProviders } = meta; | ||||||||
| const openaiMeta = openai as Record<string, unknown>; | ||||||||
|
|
||||||||
| // Remove itemId from OpenAI metadata - it requires reasoning items we don't preserve | ||||||||
| const { itemId: _itemId, ...restOpenai } = openaiMeta; | ||||||||
|
|
||||||||
| // Reconstruct metadata without itemId | ||||||||
| const hasOtherOpenaiFields = Object.keys(restOpenai).length > 0; | ||||||||
| const hasOtherProviders = Object.keys(restProviders).length > 0; | ||||||||
|
|
||||||||
| if (hasOtherOpenaiFields && hasOtherProviders) { | ||||||||
| return { ...restProviders, openai: restOpenai }; | ||||||||
| } else if (hasOtherOpenaiFields) { | ||||||||
| return { openai: restOpenai }; | ||||||||
| } else if (hasOtherProviders) { | ||||||||
| return restProviders; | ||||||||
| } | ||||||||
| return undefined; | ||||||||
| } | ||||||||
|
|
||||||||
| return meta; | ||||||||
| } | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @michael-han-dev — My initial approach followed this pattern, where I did not provide (itemId + reasoningItem) or previousResponseId. However, I later updated the implementation to include previousResponseId and raised PR #886 to address this. I wanted to confirm my understanding:
Could you help clarify whether this assumption is correct, or if I might be overlooking something in how the reasoning context is preserved?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hi @bhuvaneshprasad thanks for bringing this up! adding previousresponseID would definitely add a bonus of giving the model access to prior reasoning during tool calls. I didn't add it here since it would create a dependency on openai's server-side state, which breaks the self-contained design of durableagent (among other things). wanted to keep this pr minimal and match the existing behaviour.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. cc: @pranaygp |
||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.