From 6ad251c636b568bd74de9b8a8b650d876227f122 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Tue, 13 Jan 2026 23:19:05 -0700 Subject: [PATCH 1/5] fix: handle missing tool identity in OpenAI Native streams --- .../__tests__/openai-native-tools.spec.ts | 79 +++++++++++++++++++ src/api/providers/openai-native.ts | 44 ++++++++--- 2 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/api/providers/__tests__/openai-native-tools.spec.ts b/src/api/providers/__tests__/openai-native-tools.spec.ts index 4ce6afd3202..b3c0ae0dfe5 100644 --- a/src/api/providers/__tests__/openai-native-tools.spec.ts +++ b/src/api/providers/__tests__/openai-native-tools.spec.ts @@ -296,4 +296,83 @@ describe("OpenAiNativeHandler MCP tool schema handling", () => { expect(tool.parameters.properties.metadata.additionalProperties).toBe(false) // Nested object expect(tool.parameters.properties.metadata.properties.labels.items.additionalProperties).toBe(false) // Array items }) + + it("should handle missing call_id and name in tool_call_arguments.delta by using pending tool identity", async () => { + const handler = new OpenAiNativeHandler({ + openAiNativeApiKey: "test-key", + apiModelId: "gpt-4o", + } as ApiHandlerOptions) + + const mockClient = { + responses: { + create: vi.fn().mockImplementation(() => { + return { + [Symbol.asyncIterator]: async function* () { + // 1. Emit output_item.added with tool identity + yield { + type: "response.output_item.added", + item: { + type: "function_call", + call_id: "call_123", + name: "read_file", + arguments: "", + }, + } + + // 2. Emit tool_call_arguments.delta WITHOUT identity (just args) + yield { + type: "response.function_call_arguments.delta", + delta: '{"path":', + } + + // 3. Emit another delta + yield { + type: "response.function_call_arguments.delta", + delta: '"/tmp/test.txt"}', + } + + // 4. Emit output_item.done + yield { + type: "response.output_item.done", + item: { + type: "function_call", + call_id: "call_123", + name: "read_file", + arguments: '{"path":"/tmp/test.txt"}', + }, + } + }, + } + }), + }, + } + ;(handler as any).client = mockClient + + const stream = handler.createMessage("system prompt", [], { + taskId: "test-task-id", + }) + + const chunks: any[] = [] + for await (const chunk of stream) { + if (chunk.type === "tool_call_partial") { + chunks.push(chunk) + } + } + + expect(chunks.length).toBe(2) + expect(chunks[0]).toEqual({ + type: "tool_call_partial", + index: 0, + id: "call_123", // Should be filled from pendingToolCallId + name: "read_file", // Should be filled from pendingToolCallName + arguments: '{"path":', + }) + expect(chunks[1]).toEqual({ + type: "tool_call_partial", + index: 0, + id: "call_123", + name: "read_file", + arguments: '"/tmp/test.txt"}', + }) + }) }) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index 58a62497f71..cd5c3bbcd75 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -32,6 +32,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio protected options: ApiHandlerOptions private client: OpenAI private readonly providerName = "OpenAI Native" + /** + * Some Responses streams emit tool-call argument deltas without stable call id/name. + * Track the last observed tool identity from output_item events so we can still + * emit `tool_call_partial` chunks (tool-call-only streams). + */ + private pendingToolCallId: string | undefined + private pendingToolCallName: string | undefined // Resolved service tier from Responses API (actual tier used by OpenAI) private lastServiceTier: ServiceTier | undefined // Complete response output array (includes reasoning items with encrypted_content) @@ -51,6 +58,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio "response.reasoning_summary_text.delta", "response.refusal.delta", "response.output_item.added", + "response.output_item.done", "response.done", "response.completed", "response.tool_call_arguments.delta", @@ -155,6 +163,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio this.lastResponseOutput = undefined // Reset last response id for this request this.lastResponseId = undefined + // Reset pending tool identity for this request + this.pendingToolCallId = undefined + this.pendingToolCallName = undefined // Use Responses API for ALL models const { verbosity, reasoning } = this.getModel() @@ -1136,17 +1147,22 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio event?.type === "response.tool_call_arguments.delta" || event?.type === "response.function_call_arguments.delta" ) { - // Emit partial chunks directly - NativeToolCallParser handles state management - const callId = event.call_id || event.tool_call_id || event.id - const name = event.name || event.function_name + // Some streams omit stable identity on delta events; fall back to the + // most recently observed tool identity from output_item events. + const callId = event.call_id || event.tool_call_id || event.id || this.pendingToolCallId || undefined + const name = event.name || event.function_name || this.pendingToolCallName || undefined const args = event.delta || event.arguments - yield { - type: "tool_call_partial", - index: event.index ?? 0, - id: callId, - name, - arguments: args, + // Avoid emitting incomplete tool_call_partial chunks; the downstream + // NativeToolCallParser needs a name to start a call. + if (typeof name === "string" && name.length > 0 && typeof callId === "string" && callId.length > 0) { + yield { + type: "tool_call_partial", + index: event.index ?? 0, + id: callId, + name, + arguments: args, + } } return } @@ -1164,6 +1180,16 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio if (event?.type === "response.output_item.added" || event?.type === "response.output_item.done") { const item = event?.item if (item) { + // Capture tool identity so subsequent argument deltas can be attributed. + if (item.type === "function_call" || item.type === "tool_call") { + const callId = item.call_id || item.tool_call_id || item.id + const name = item.name || item.function?.name || item.function_name + if (typeof callId === "string" && callId.length > 0) { + this.pendingToolCallId = callId + this.pendingToolCallName = typeof name === "string" ? name : undefined + } + } + if (item.type === "text" && item.text) { yield { type: "text", text: item.text } } else if (item.type === "reasoning" && item.text) { From e652dc108de541ecfbd7fcae869c1fc3f069e8b6 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 22:35:28 -0700 Subject: [PATCH 2/5] fix: treat tool-call-only streams as having content --- src/api/providers/openai-native.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index cd5c3bbcd75..ebb0f44f5f4 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -677,7 +677,13 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio if (parsed?.type && this.coreHandledEventTypes.has(parsed.type)) { for await (const outChunk of this.processEvent(parsed, model)) { // Track whether we've emitted any content so fallback handling can decide appropriately - if (outChunk.type === "text" || outChunk.type === "reasoning") { + // Include tool calls so tool-call-only responses aren't treated as empty + if ( + outChunk.type === "text" || + outChunk.type === "reasoning" || + outChunk.type === "tool_call" || + outChunk.type === "tool_call_partial" + ) { hasContent = true } yield outChunk From 99b39afb11272130a8fc880d108d035879b0620f Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 14 Jan 2026 22:58:59 -0700 Subject: [PATCH 3/5] feat: add originator and session_id headers for request tracking --- src/api/providers/openai-native.ts | 31 ++++++++++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index ebb0f44f5f4..ee71881313e 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -1,3 +1,5 @@ +import * as os from "os" +import { v7 as uuidv7 } from "uuid" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" @@ -32,6 +34,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio protected options: ApiHandlerOptions private client: OpenAI private readonly providerName = "OpenAI Native" + // Session ID for request tracking (persists for the lifetime of the handler) + private readonly sessionId: string /** * Some Responses streams emit tool-call argument deltas without stable call id/name. * Track the last observed tool identity from output_item events so we can still @@ -70,13 +74,23 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio constructor(options: ApiHandlerOptions) { super() this.options = options + // Generate a session ID for request tracking + this.sessionId = uuidv7() // Default to including reasoning.summary: "auto" for models that support Responses API // reasoning summaries unless explicitly disabled. if (this.options.enableResponsesReasoningSummary === undefined) { this.options.enableResponsesReasoningSummary = true } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" - this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey }) + // Include originator, session_id, and User-Agent headers for API tracking and debugging + this.client = new OpenAI({ + baseURL: this.options.openAiNativeBaseUrl, + apiKey, + defaultHeaders: { + originator: "roo-code", + session_id: this.sessionId, + }, + }) } private normalizeUsage(usage: any, model: OpenAiNativeModel): ApiStreamUsageChunk | undefined { @@ -390,10 +404,18 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Create AbortController for cancellation this.abortController = new AbortController() + // Build per-request headers using taskId when available, falling back to sessionId + const taskId = metadata?.taskId + const requestHeaders: Record = { + originator: "roo-code", + session_id: taskId || this.sessionId, + } + try { - // Use the official SDK + // Use the official SDK with per-request headers const stream = (await (this.client as any).responses.create(requestBody, { signal: this.abortController.signal, + headers: requestHeaders, })) as AsyncIterable if (typeof (stream as any)[Symbol.asyncIterator] !== "function") { @@ -526,6 +548,9 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Create AbortController for cancellation this.abortController = new AbortController() + // Build per-request headers using taskId when available, falling back to sessionId + const taskId = metadata?.taskId + try { const response = await fetch(url, { method: "POST", @@ -533,6 +558,8 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, Accept: "text/event-stream", + originator: "roo-code", + session_id: taskId || this.sessionId, }, body: JSON.stringify(requestBody), signal: this.abortController.signal, From dcabbf9c2bbf3e9bc0ab9c2dac82366b0166eea3 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Thu, 15 Jan 2026 00:06:16 -0700 Subject: [PATCH 4/5] feat: add custom User-Agent header for debugging --- src/api/providers/openai-native.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index ee71881313e..b50e7658398 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -3,6 +3,7 @@ import { v7 as uuidv7 } from "uuid" import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" +import { Package } from "../../shared/package" import { type ModelInfo, openAiNativeDefaultModelId, @@ -83,12 +84,14 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio } const apiKey = this.options.openAiNativeApiKey ?? "not-provided" // Include originator, session_id, and User-Agent headers for API tracking and debugging + const userAgent = `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}` this.client = new OpenAI({ baseURL: this.options.openAiNativeBaseUrl, apiKey, defaultHeaders: { originator: "roo-code", session_id: this.sessionId, + "User-Agent": userAgent, }, }) } @@ -406,9 +409,11 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Build per-request headers using taskId when available, falling back to sessionId const taskId = metadata?.taskId + const userAgent = `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}` const requestHeaders: Record = { originator: "roo-code", session_id: taskId || this.sessionId, + "User-Agent": userAgent, } try { @@ -550,6 +555,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio // Build per-request headers using taskId when available, falling back to sessionId const taskId = metadata?.taskId + const userAgent = `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}` try { const response = await fetch(url, { @@ -560,6 +566,7 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio Accept: "text/event-stream", originator: "roo-code", session_id: taskId || this.sessionId, + "User-Agent": userAgent, }, body: JSON.stringify(requestBody), signal: this.abortController.signal, From 511ea502f385c793686546c0ae3ebcc5a539c26a Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Thu, 15 Jan 2026 09:35:23 -0700 Subject: [PATCH 5/5] fix: align headers between openai-native and openai-codex providers - Remove unnecessary Accept: text/event-stream header from openai-native - Fix undefined extensionVersion variable in openai-codex completePrompt() - Update tests to match new header expectations --- src/api/providers/__tests__/openai-native.spec.ts | 2 -- src/api/providers/openai-codex.ts | 10 ++++------ src/api/providers/openai-native.ts | 1 - 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/api/providers/__tests__/openai-native.spec.ts b/src/api/providers/__tests__/openai-native.spec.ts index 2f9f0bb9d74..a95ba0a004c 100644 --- a/src/api/providers/__tests__/openai-native.spec.ts +++ b/src/api/providers/__tests__/openai-native.spec.ts @@ -319,7 +319,6 @@ describe("OpenAiNativeHandler", () => { headers: expect.objectContaining({ "Content-Type": "application/json", Authorization: "Bearer test-api-key", - Accept: "text/event-stream", }), body: expect.any(String), }), @@ -1325,7 +1324,6 @@ describe("GPT-5 streaming event coverage (additional)", () => { headers: expect.objectContaining({ "Content-Type": "application/json", Authorization: "Bearer test-api-key", - Accept: "text/event-stream", }), body: expect.any(String), }), diff --git a/src/api/providers/openai-codex.ts b/src/api/providers/openai-codex.ts index 490b5baaae4..8034502c0d7 100644 --- a/src/api/providers/openai-codex.ts +++ b/src/api/providers/openai-codex.ts @@ -14,6 +14,7 @@ import { } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" +import { Package } from "../../shared/package" import type { ApiHandlerOptions } from "../../shared/api" import { ApiStream, ApiStreamUsageChunk } from "../transform/stream" @@ -25,9 +26,6 @@ import { isMcpTool } from "../../utils/mcp-name" import { openAiCodexOAuthManager } from "../../integrations/openai-codex/oauth" import { t } from "../../i18n" -// Get extension version for User-Agent header -const extensionVersion: string = require("../../package.json").version ?? "unknown" - export type OpenAiCodexModel = ReturnType /** @@ -354,7 +352,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion const codexHeaders: Record = { originator: "roo-code", session_id: taskId || this.sessionId, - "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, + "User-Agent": `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, ...(accountId ? { "ChatGPT-Account-Id": accountId } : {}), } @@ -494,7 +492,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion Authorization: `Bearer ${accessToken}`, originator: "roo-code", session_id: taskId || this.sessionId, - "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, + "User-Agent": `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, } // Add ChatGPT-Account-Id if available (required for organization subscriptions) @@ -1058,7 +1056,7 @@ export class OpenAiCodexHandler extends BaseProvider implements SingleCompletion Authorization: `Bearer ${accessToken}`, originator: "roo-code", session_id: this.sessionId, - "User-Agent": `roo-code/${extensionVersion} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, + "User-Agent": `roo-code/${Package.version} (${os.platform()} ${os.release()}; ${os.arch()}) node/${process.version.slice(1)}`, } // Add ChatGPT-Account-Id if available diff --git a/src/api/providers/openai-native.ts b/src/api/providers/openai-native.ts index b50e7658398..b028d95c1e2 100644 --- a/src/api/providers/openai-native.ts +++ b/src/api/providers/openai-native.ts @@ -563,7 +563,6 @@ export class OpenAiNativeHandler extends BaseProvider implements SingleCompletio headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}`, - Accept: "text/event-stream", originator: "roo-code", session_id: taskId || this.sessionId, "User-Agent": userAgent,