Skip to content

Commit 247bb33

Browse files
committed
fix(openai-base): align Responses API tool-call event IDs with main
Main's ai-openai emits TOOL_CALL_START/ARGS/END with the internal function_call item id (item.id) as toolCallId, and includes both toolCallName and toolName fields. The PR's openai-base extraction tried to use the Responses API call_id instead and dropped toolCallName, which broke the agent loop + tool-call E2E suite for openai, grok, and openrouter. - Use item.id uniformly for toolCallId (agent loop carries it back as toolCallId on the tool ModelMessage, which the Responses API accepts as call_id for function_call_output). - Emit both toolCallName and toolName on TOOL_CALL_START/END events. - Do the same for the Chat Completions adapter. - Update the openai-base test that pinned the old call_id design.
1 parent 04a9a1c commit 247bb33

3 files changed

Lines changed: 25 additions & 29 deletions

File tree

packages/typescript/openai-base/src/adapters/chat-completions-text.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -334,6 +334,7 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
334334
yield asChunk({
335335
type: 'TOOL_CALL_START',
336336
toolCallId: toolCall.id,
337+
toolCallName: toolCall.name,
337338
toolName: toolCall.name,
338339
model: chunk.model || options.model,
339340
timestamp,
@@ -376,6 +377,7 @@ export class OpenAICompatibleChatCompletionsTextAdapter<
376377
yield asChunk({
377378
type: 'TOOL_CALL_END',
378379
toolCallId: toolCall.id,
380+
toolCallName: toolCall.name,
379381
toolName: toolCall.name,
380382
model: chunk.model || options.model,
381383
timestamp,

packages/typescript/openai-base/src/adapters/responses-text.ts

Lines changed: 15 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ export class OpenAICompatibleResponsesTextAdapter<
8585
// We assign our own indices as we encounter unique tool call IDs.
8686
const toolCallMetadata = new Map<
8787
string,
88-
{ index: number; name: string; callId: string; started: boolean }
88+
{ index: number; name: string; started: boolean }
8989
>()
9090
const requestParams = this.mapOptionsToRequest(options)
9191
const timestamp = Date.now()
@@ -281,7 +281,7 @@ export class OpenAICompatibleResponsesTextAdapter<
281281
stream: AsyncIterable<OpenAI_SDK.Responses.ResponseStreamEvent>,
282282
toolCallMetadata: Map<
283283
string,
284-
{ index: number; name: string; callId: string; started: boolean }
284+
{ index: number; name: string; started: boolean }
285285
>,
286286
options: TextOptions,
287287
aguiState: {
@@ -563,33 +563,27 @@ export class OpenAICompatibleResponsesTextAdapter<
563563
// handle output_item.added to capture function call metadata (name)
564564
if (chunk.type === 'response.output_item.added') {
565565
const item = chunk.item
566-
if (item.type === 'function_call') {
567-
// call_id is the required correlation ID for function_call_output.
568-
// id is the internal item ID used by delta/done events (item_id).
569-
// Use id when available (for the metadata map keyed by item_id),
570-
// falling back to call_id for providers that omit id.
571-
const itemId = item.id || item.call_id
572-
const callId = item.call_id || item.id || ''
573-
574-
// Store the function name for later use
575-
if (!toolCallMetadata.has(itemId)) {
576-
toolCallMetadata.set(itemId, {
566+
if (item.type === 'function_call' && item.id) {
567+
// Store the function name for later use, keyed by the item id that
568+
// subsequent delta/done events reference via `item_id`.
569+
if (!toolCallMetadata.has(item.id)) {
570+
toolCallMetadata.set(item.id, {
577571
index: chunk.output_index,
578572
name: item.name || '',
579-
callId,
580573
started: false,
581574
})
582575
}
583576
// Emit TOOL_CALL_START
584577
yield asChunk({
585578
type: 'TOOL_CALL_START',
586-
toolCallId: callId,
579+
toolCallId: item.id,
580+
toolCallName: item.name || '',
587581
toolName: item.name || '',
588582
model: model || options.model,
589583
timestamp,
590584
index: chunk.output_index,
591585
})
592-
toolCallMetadata.get(itemId)!.started = true
586+
toolCallMetadata.get(item.id)!.started = true
593587
}
594588
}
595589

@@ -601,7 +595,7 @@ export class OpenAICompatibleResponsesTextAdapter<
601595
const metadata = toolCallMetadata.get(chunk.item_id)
602596
yield asChunk({
603597
type: 'TOOL_CALL_ARGS',
604-
toolCallId: metadata?.callId || chunk.item_id,
598+
toolCallId: chunk.item_id,
605599
model: model || options.model,
606600
timestamp,
607601
delta: chunk.delta,
@@ -615,19 +609,20 @@ export class OpenAICompatibleResponsesTextAdapter<
615609
// Get the function name from metadata (captured in output_item.added)
616610
const metadata = toolCallMetadata.get(item_id)
617611
const name = metadata?.name || ''
618-
const callId = metadata?.callId || item_id
619612

620613
// Parse arguments
621614
let parsedInput: unknown = {}
622615
try {
623-
parsedInput = chunk.arguments ? JSON.parse(chunk.arguments) : {}
616+
const parsed = chunk.arguments ? JSON.parse(chunk.arguments) : {}
617+
parsedInput = parsed && typeof parsed === 'object' ? parsed : {}
624618
} catch {
625619
parsedInput = {}
626620
}
627621

628622
yield asChunk({
629623
type: 'TOOL_CALL_END',
630-
toolCallId: callId,
624+
toolCallId: item_id,
625+
toolCallName: name,
631626
toolName: name,
632627
model: model || options.model,
633628
timestamp,

packages/typescript/openai-base/tests/responses-text.test.ts

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -785,7 +785,7 @@ describe('OpenAICompatibleResponsesTextAdapter', () => {
785785
}
786786
})
787787

788-
it('uses call_id instead of internal id for tool call correlation', async () => {
788+
it('uses the internal function_call item id for tool call correlation', async () => {
789789
const streamChunks = [
790790
{
791791
type: 'response.created',
@@ -854,27 +854,26 @@ describe('OpenAICompatibleResponsesTextAdapter', () => {
854854
chunks.push(chunk)
855855
}
856856

857-
// TOOL_CALL_START should use call_id, not internal id
857+
// TOOL_CALL_* events should use the internal function_call item id
858+
// (matches main's OpenAI adapter behavior; the agent loop carries this
859+
// id back as `toolCallId` on the tool ModelMessage, which the Responses
860+
// API accepts as `call_id` for function_call_output).
858861
const toolStart = chunks.find((c) => c.type === 'TOOL_CALL_START')
859862
expect(toolStart).toBeDefined()
860863
if (toolStart?.type === 'TOOL_CALL_START') {
861-
expect(toolStart.toolCallId).toBe('call_api_abc123')
862-
expect(toolStart.toolCallId).not.toBe('fc_internal_001')
864+
expect(toolStart.toolCallId).toBe('fc_internal_001')
863865
}
864866

865-
// TOOL_CALL_ARGS should also use call_id
866867
const toolArgs = chunks.filter((c) => c.type === 'TOOL_CALL_ARGS')
867868
expect(toolArgs.length).toBeGreaterThan(0)
868869
if (toolArgs[0]?.type === 'TOOL_CALL_ARGS') {
869-
expect(toolArgs[0].toolCallId).toBe('call_api_abc123')
870+
expect(toolArgs[0].toolCallId).toBe('fc_internal_001')
870871
}
871872

872-
// TOOL_CALL_END should also use call_id
873873
const toolEnd = chunks.find((c) => c.type === 'TOOL_CALL_END')
874874
expect(toolEnd).toBeDefined()
875875
if (toolEnd?.type === 'TOOL_CALL_END') {
876-
expect(toolEnd.toolCallId).toBe('call_api_abc123')
877-
expect(toolEnd.toolCallId).not.toBe('fc_internal_001')
876+
expect(toolEnd.toolCallId).toBe('fc_internal_001')
878877
}
879878
})
880879
})

0 commit comments

Comments
 (0)