From 05e35daa574a8080334520b5dc4cb0714a2e058c Mon Sep 17 00:00:00 2001 From: Robert Benko Date: Sat, 29 Nov 2025 01:01:10 +0100 Subject: [PATCH 1/5] fix(openai): responses function_call mapping * to handle function_call messages loaded from session file * use call_id as item id if response id is not available * ensure function_call item id is prefixed with fc --- core/llm/openaiTypeConverters.ts | 38 ++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/core/llm/openaiTypeConverters.ts b/core/llm/openaiTypeConverters.ts index a7e3d6bacd8..b35c437a4ab 100644 --- a/core/llm/openaiTypeConverters.ts +++ b/core/llm/openaiTypeConverters.ts @@ -818,20 +818,34 @@ export function toResponsesInput(messages: ChatMessage[]): ResponseInput { | undefined; const toolCalls = msg.toolCalls as ToolCallDelta[] | undefined; - if (respId && Array.isArray(toolCalls) && toolCalls.length > 0) { - // Emit full function_call output item + if (Array.isArray(toolCalls) && toolCalls.length > 0) { const tc = toolCalls[0]; - const name = tc?.function?.name as string | undefined; - const args = tc?.function?.arguments as string | undefined; const call_id = tc?.id as string | undefined; - const functionCallItem: ResponseFunctionToolCall = { - id: respId, - type: "function_call", - name: name || "", - arguments: typeof args === "string" ? args : "{}", - call_id: call_id || respId, - }; - input.push(functionCallItem); + + // Only emit function_call if we have an id to use (either respId or call_id) + const rawItemId = respId || call_id; + if (rawItemId) { + // Ensure the Responses item id uses the required prefix (e.g., 'fc') + const itemId = rawItemId.startsWith("fc") + ? rawItemId + : `fc_${rawItemId}`; + + const name = tc?.function?.name as string | undefined; + const args = tc?.function?.arguments as string | undefined; + + const functionCallItem: ResponseFunctionToolCall = { + id: itemId, + type: "function_call", + name: name || "", + arguments: typeof args === "string" ? args : "{}", + call_id: call_id || respId || "", + }; + + input.push(functionCallItem); + } else { + // No IDs available, fallback to EasyInput assistant message to avoid emitting an invalid function_call + pushMessage("assistant", text || ""); + } } else if (respId) { // Emit full assistant output message item const outputMessageItem: ResponseOutputMessage = { From 7ba2d465dc343ced88ed08af5077872d2f0d4500 Mon Sep 17 00:00:00 2001 From: Robert Benko Date: Tue, 2 Dec 2025 04:17:29 +0100 Subject: [PATCH 2/5] fix(openai): reasoning multi tool function_call mapping * synthesize a single orchestrator function_call when multiple tool calls follow a reasoning (thinking) message to avoid protocol violations --- core/llm/openaiTypeConverters.ts | 129 +++++++++++++++++++++++++------ 1 file changed, 104 insertions(+), 25 deletions(-) diff --git a/core/llm/openaiTypeConverters.ts b/core/llm/openaiTypeConverters.ts index b35c437a4ab..cdab4f1a1f5 100644 --- a/core/llm/openaiTypeConverters.ts +++ b/core/llm/openaiTypeConverters.ts @@ -774,6 +774,90 @@ function toResponseInputContentList( return list; } +// Synthesize an orchestrator wrapper +function synthesizeOrchestratorCall( + toolCalls: ToolCallDelta[], + input: ResponseInput, + respId?: string, + prevReasoningId?: string, +) { + // Build subcalls array for the orchestrator from the expanded toolCalls + const subcalls = toolCalls.map((tc) => { + const fname = tc?.function?.name as string | undefined; + const rawArgs = tc?.function?.arguments as string | undefined; + let parsedArgs: any = {}; + try { + parsedArgs = rawArgs && rawArgs.length ? JSON.parse(rawArgs) : {}; + } catch { + parsedArgs = rawArgs ?? {}; + } + + return { + // use a single canonical field for the function name + function_name: fname || "", + parameters: parsedArgs, + // preserve original call id when present + call_id: tc?.id || undefined, + original_arguments: rawArgs || undefined, + }; + }); + + const wrapperArgs = { tool_uses: subcalls }; + + // Derive a single Responses item id for the orchestrator + const rawWrapperId = respId || prevReasoningId; + const wrapperItemId = + rawWrapperId && rawWrapperId.startsWith("fc") + ? rawWrapperId + : `fc_${rawWrapperId}`; + + const wrapperCallId = wrapperItemId; + + const functionCallItem: ResponseFunctionToolCall = { + id: wrapperItemId, + type: "function_call", + name: "multi_tool_use", + arguments: JSON.stringify(wrapperArgs), + call_id: wrapperCallId, + }; + + input.push(functionCallItem); +} + +function emitFunctionCallsFor( + toolCallsArr: ToolCallDelta[], + input: ResponseInput, + respId?: string, + prevReasoningId?: string, +) { + for (let j = 0; j < toolCallsArr.length; j++) { + const tc = toolCallsArr[j]; + const call_id = tc?.id as string | undefined; + + // Prefer respId or prevReasoningId. + let rawItemId = respId || prevReasoningId || call_id; + if ((respId || prevReasoningId) && toolCallsArr.length > 1) { + rawItemId = `${rawItemId}_${j}`; + } + if (!rawItemId) { + // Can't create a valid Responses item id for this call; skip it + continue; + } + + const itemId = rawItemId.startsWith("fc") ? rawItemId : `fc_${rawItemId}`; + + const functionCallItem: ResponseFunctionToolCall = { + id: itemId, + type: "function_call", + name: tc.function?.name || "", + arguments: tc.function?.arguments || "", + call_id: call_id || respId || prevReasoningId || "", + }; + + input.push(functionCallItem); + } +} + export function toResponsesInput(messages: ChatMessage[]): ResponseInput { const input: ResponseInput = []; @@ -816,36 +900,31 @@ export function toResponsesInput(messages: ChatMessage[]): ResponseInput { const respId = msg.metadata?.responsesOutputItemId as | string | undefined; + const prevMsgForThis = messages[i - 1] as ChatMessage | undefined; + const prevReasoningId = prevMsgForThis?.metadata?.reasoningId as + | string + | undefined; const toolCalls = msg.toolCalls as ToolCallDelta[] | undefined; if (Array.isArray(toolCalls) && toolCalls.length > 0) { - const tc = toolCalls[0]; - const call_id = tc?.id as string | undefined; - - // Only emit function_call if we have an id to use (either respId or call_id) - const rawItemId = respId || call_id; - if (rawItemId) { - // Ensure the Responses item id uses the required prefix (e.g., 'fc') - const itemId = rawItemId.startsWith("fc") - ? rawItemId - : `fc_${rawItemId}`; - - const name = tc?.function?.name as string | undefined; - const args = tc?.function?.arguments as string | undefined; - - const functionCallItem: ResponseFunctionToolCall = { - id: itemId, - type: "function_call", - name: name || "", - arguments: typeof args === "string" ? args : "{}", - call_id: call_id || respId || "", - }; + // If multiple tool calls immediately follow a reasoning (thinking) + // message, synthesize a single orchestrator function_call so the reasoning + // item is followed by exactly one action. This avoids protocol violations + // where a reasoning item is followed by multiple function_call items. + const shouldSynthesizeOrchestrator = + toolCalls.length > 1 && !!prevReasoningId; - input.push(functionCallItem); - } else { - // No IDs available, fallback to EasyInput assistant message to avoid emitting an invalid function_call - pushMessage("assistant", text || ""); + if (shouldSynthesizeOrchestrator) { + synthesizeOrchestratorCall( + toolCalls, + input, + respId, + prevReasoningId, + ); } + + // Emit function_call items directly from toolCalls + emitFunctionCallsFor(toolCalls, input, respId, prevReasoningId); } else if (respId) { // Emit full assistant output message item const outputMessageItem: ResponseOutputMessage = { From 57889a750ecf3e28ae9b15eee490a4ff596d306a Mon Sep 17 00:00:00 2001 From: Robert Benko Date: Sat, 6 Dec 2025 16:42:38 +0100 Subject: [PATCH 3/5] fix(openai-vscode): use function_call original item id * (vscode) ensure tool calls stored in chat "assistant" message store original item id their function_calls arrived with * (openai) handle function_call messages loaded from old session file by creating item id from call id if there is no original item id * (openai) emit all function_call items when sending response --- core/config/types.ts | 2 + core/index.d.ts | 6 +- core/llm/openaiTypeConverters.ts | 130 ++++++--------------------- gui/src/redux/slices/sessionSlice.ts | 20 +++-- gui/src/util/toolCallState.ts | 2 + 5 files changed, 44 insertions(+), 116 deletions(-) diff --git a/core/config/types.ts b/core/config/types.ts index d9f58aca6e2..9e72ec1ce01 100644 --- a/core/config/types.ts +++ b/core/config/types.ts @@ -302,6 +302,7 @@ declare global { name: string; arguments: string; }; + responsesOutputItemId?: string; } export interface ToolCallDelta { @@ -311,6 +312,7 @@ declare global { name?: string; arguments?: string; }; + responsesOutputItemId?: string; } export interface ToolResultChatMessage { diff --git a/core/index.d.ts b/core/index.d.ts index f31b62ed7d6..90f944d909f 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -272,10 +272,6 @@ export interface Session { title: string; workspaceDirectory: string; history: ChatHistoryItem[]; - /** Optional: per-session UI mode (chat/agent/plan/background) */ - mode?: MessageModes; - /** Optional: title of the selected chat model for this session */ - chatModelTitle?: string | null; } export interface BaseSessionMetadata { @@ -348,6 +344,7 @@ export interface ToolCall { name: string; arguments: string; }; + responsesOutputItemId?: string; } export interface ToolCallDelta { @@ -357,6 +354,7 @@ export interface ToolCallDelta { name?: string; arguments?: string; }; + responsesOutputItemId?: string; } export interface ToolResultChatMessage { diff --git a/core/llm/openaiTypeConverters.ts b/core/llm/openaiTypeConverters.ts index cdab4f1a1f5..48119a64051 100644 --- a/core/llm/openaiTypeConverters.ts +++ b/core/llm/openaiTypeConverters.ts @@ -774,90 +774,7 @@ function toResponseInputContentList( return list; } -// Synthesize an orchestrator wrapper -function synthesizeOrchestratorCall( - toolCalls: ToolCallDelta[], - input: ResponseInput, - respId?: string, - prevReasoningId?: string, -) { - // Build subcalls array for the orchestrator from the expanded toolCalls - const subcalls = toolCalls.map((tc) => { - const fname = tc?.function?.name as string | undefined; - const rawArgs = tc?.function?.arguments as string | undefined; - let parsedArgs: any = {}; - try { - parsedArgs = rawArgs && rawArgs.length ? JSON.parse(rawArgs) : {}; - } catch { - parsedArgs = rawArgs ?? {}; - } - - return { - // use a single canonical field for the function name - function_name: fname || "", - parameters: parsedArgs, - // preserve original call id when present - call_id: tc?.id || undefined, - original_arguments: rawArgs || undefined, - }; - }); - - const wrapperArgs = { tool_uses: subcalls }; - - // Derive a single Responses item id for the orchestrator - const rawWrapperId = respId || prevReasoningId; - const wrapperItemId = - rawWrapperId && rawWrapperId.startsWith("fc") - ? rawWrapperId - : `fc_${rawWrapperId}`; - - const wrapperCallId = wrapperItemId; - - const functionCallItem: ResponseFunctionToolCall = { - id: wrapperItemId, - type: "function_call", - name: "multi_tool_use", - arguments: JSON.stringify(wrapperArgs), - call_id: wrapperCallId, - }; - - input.push(functionCallItem); -} - -function emitFunctionCallsFor( - toolCallsArr: ToolCallDelta[], - input: ResponseInput, - respId?: string, - prevReasoningId?: string, -) { - for (let j = 0; j < toolCallsArr.length; j++) { - const tc = toolCallsArr[j]; - const call_id = tc?.id as string | undefined; - - // Prefer respId or prevReasoningId. - let rawItemId = respId || prevReasoningId || call_id; - if ((respId || prevReasoningId) && toolCallsArr.length > 1) { - rawItemId = `${rawItemId}_${j}`; - } - if (!rawItemId) { - // Can't create a valid Responses item id for this call; skip it - continue; - } - - const itemId = rawItemId.startsWith("fc") ? rawItemId : `fc_${rawItemId}`; - - const functionCallItem: ResponseFunctionToolCall = { - id: itemId, - type: "function_call", - name: tc.function?.name || "", - arguments: tc.function?.arguments || "", - call_id: call_id || respId || prevReasoningId || "", - }; - - input.push(functionCallItem); - } -} - +// eslint-disable-next-line complexity export function toResponsesInput(messages: ChatMessage[]): ResponseInput { const input: ResponseInput = []; @@ -900,31 +817,34 @@ export function toResponsesInput(messages: ChatMessage[]): ResponseInput { const respId = msg.metadata?.responsesOutputItemId as | string | undefined; - const prevMsgForThis = messages[i - 1] as ChatMessage | undefined; - const prevReasoningId = prevMsgForThis?.metadata?.reasoningId as - | string - | undefined; + const toolCalls = msg.toolCalls as ToolCallDelta[] | undefined; if (Array.isArray(toolCalls) && toolCalls.length > 0) { - // If multiple tool calls immediately follow a reasoning (thinking) - // message, synthesize a single orchestrator function_call so the reasoning - // item is followed by exactly one action. This avoids protocol violations - // where a reasoning item is followed by multiple function_call items. - const shouldSynthesizeOrchestrator = - toolCalls.length > 1 && !!prevReasoningId; - - if (shouldSynthesizeOrchestrator) { - synthesizeOrchestratorCall( - toolCalls, - input, - respId, - prevReasoningId, - ); + // Emit one function_call item per recorded tool call, + // prefer per-tool responsesOutputItemId when available + for (const tc of toolCalls) { + const name = tc?.function?.name as string | undefined; + const args = tc?.function?.arguments as string | undefined; + const tcRespId = (tc as any).responsesOutputItemId as + | string + | undefined; + const callId = tc?.id as string | undefined; + const rawItemId = tcRespId || respId || callId; + const itemId = rawItemId + ? rawItemId.startsWith("fc") + ? rawItemId + : `fc_${rawItemId}` + : undefined; + const functionCallItem: ResponseFunctionToolCall = { + id: itemId, + type: "function_call", + name: name || "", + arguments: typeof args === "string" ? args : "{}", + call_id: callId || tcRespId || respId || "", + }; + input.push(functionCallItem); } - - // Emit function_call items directly from toolCalls - emitFunctionCallsFor(toolCalls, input, respId, prevReasoningId); } else if (respId) { // Emit full assistant output message item const outputMessageItem: ResponseOutputMessage = { diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 3656af0c185..30b9537e0ea 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -87,9 +87,12 @@ export function handleToolCallsInMessage( // Initialize tool call states for each filtered tool call in the message // Each tool call gets its own state to track generation/execution progress - lastItem.toolCallStates = filteredToolCalls.map((toolCallDelta) => - addToolCallDeltaToState(toolCallDelta, undefined), - ); + lastItem.toolCallStates = filteredToolCalls.map((toolCallDelta) => { + const state = addToolCallDeltaToState(toolCallDelta, undefined); + state.toolCall.responsesOutputItemId = + (message.metadata?.responsesOutputItemId as string) ?? undefined; + return state; + }); // Update the message's toolCalls array to reflect the processed tool calls // We can safely cast because we verified the role above @@ -116,6 +119,7 @@ export function handleToolCallsInMessage( function applyToolCallDelta( toolCallDelta: ToolCallDelta, toolCallStates: ToolCallState[], + responsesOutputItemId: string, ): void { // Find existing state by matching toolCallId - this ensures we update // the correct tool call even when multiple tool calls are being streamed @@ -150,6 +154,7 @@ function applyToolCallDelta( toolCallStates[existingStateIndex] = updatedState; } else { // Add new tool call state for a newly discovered tool call + updatedState.toolCall.responsesOutputItemId = responsesOutputItemId; toolCallStates.push(updatedState); } } @@ -181,7 +186,11 @@ export function handleStreamingToolCallUpdates( // Process each filtered tool call delta, matching by ID to update the correct state filteredToolCalls.forEach((toolCallDelta) => { - applyToolCallDelta(toolCallDelta, updatedToolCallStates); + applyToolCallDelta( + toolCallDelta, + updatedToolCallStates, + message.metadata?.responsesOutputItemId as string, + ); }); // Replace the entire tool call states array with the updated version @@ -693,9 +702,6 @@ export const sessionSlice = createSlice({ state.history = payload.history as any; state.title = payload.title; state.id = payload.sessionId; - if (payload.mode) { - state.mode = payload.mode; - } } else { state.history = []; state.title = NEW_SESSION_TITLE; diff --git a/gui/src/util/toolCallState.ts b/gui/src/util/toolCallState.ts index c7b9bcbd7df..91da2189983 100644 --- a/gui/src/util/toolCallState.ts +++ b/gui/src/util/toolCallState.ts @@ -63,6 +63,8 @@ export function addToolCallDeltaToState( name: mergedName, arguments: mergedArgs, }, + responsesOutputItemId: + currentCall?.responsesOutputItemId, }, toolCallId: callId, parsedArgs, From 8c2b5c14d231becfaa9ecd8b196bd9526d1e5117 Mon Sep 17 00:00:00 2001 From: Robert Benko Date: Mon, 8 Dec 2025 20:02:06 +0100 Subject: [PATCH 4/5] fix: restore mistakenly deleted MessageModes --- core/index.d.ts | 4 ++++ gui/src/redux/slices/sessionSlice.ts | 3 +++ 2 files changed, 7 insertions(+) diff --git a/core/index.d.ts b/core/index.d.ts index 90f944d909f..02577388f65 100644 --- a/core/index.d.ts +++ b/core/index.d.ts @@ -272,6 +272,10 @@ export interface Session { title: string; workspaceDirectory: string; history: ChatHistoryItem[]; + /** Optional: per-session UI mode (chat/agent/plan/background) */ + mode?: MessageModes; + /** Optional: title of the selected chat model for this session */ + chatModelTitle?: string | null; } export interface BaseSessionMetadata { diff --git a/gui/src/redux/slices/sessionSlice.ts b/gui/src/redux/slices/sessionSlice.ts index 30b9537e0ea..f7697e60dfc 100644 --- a/gui/src/redux/slices/sessionSlice.ts +++ b/gui/src/redux/slices/sessionSlice.ts @@ -702,6 +702,9 @@ export const sessionSlice = createSlice({ state.history = payload.history as any; state.title = payload.title; state.id = payload.sessionId; + if (payload.mode) { + state.mode = payload.mode; + } } else { state.history = []; state.title = NEW_SESSION_TITLE; From c4ed9e4100ab646608ee9cb1de024143983831b0 Mon Sep 17 00:00:00 2001 From: Robert Benko Date: Mon, 8 Dec 2025 20:27:12 +0100 Subject: [PATCH 5/5] format: make it prettier --- gui/src/util/toolCallState.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/src/util/toolCallState.ts b/gui/src/util/toolCallState.ts index 91da2189983..5c990f20fd5 100644 --- a/gui/src/util/toolCallState.ts +++ b/gui/src/util/toolCallState.ts @@ -63,8 +63,7 @@ export function addToolCallDeltaToState( name: mergedName, arguments: mergedArgs, }, - responsesOutputItemId: - currentCall?.responsesOutputItemId, + responsesOutputItemId: currentCall?.responsesOutputItemId, }, toolCallId: callId, parsedArgs,