From eef69d070ad2bf9e891a09302a7ba10620d2a81c Mon Sep 17 00:00:00 2001 From: Nate Smyth Date: Sat, 17 Jan 2026 14:44:11 -0500 Subject: [PATCH 1/2] fix: track reasoning by output_index for copilot compatibility --- .../openai-responses-language-model.ts | 59 ++++++++++++------- 1 file changed, 39 insertions(+), 20 deletions(-) diff --git a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts index 94b0edaf3f4..0990b7e0077 100644 --- a/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts +++ b/packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts @@ -815,14 +815,20 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { // flag that checks if there have been client-side tool calls (not executed by openai) let hasFunctionCall = false + // Track reasoning by output_index instead of item_id + // GitHub Copilot rotates encrypted item IDs on every event const activeReasoning: Record< - string, + number, { + canonicalId: string // the item.id from output_item.added encryptedContent?: string | null summaryParts: number[] } > = {} + // Track current active reasoning output_index for correlating summary events + let currentReasoningOutputIndex: number | null = null + // Track a stable text part id for the current assistant message. // Copilot may change item_id across text deltas; normalize to one id. let currentTextId: string | null = null @@ -933,10 +939,12 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { }, }) } else if (isResponseOutputItemAddedReasoningChunk(value)) { - activeReasoning[value.item.id] = { + activeReasoning[value.output_index] = { + canonicalId: value.item.id, encryptedContent: value.item.encrypted_content, summaryParts: [0], } + currentReasoningOutputIndex = value.output_index controller.enqueue({ type: "reasoning-start", @@ -1091,22 +1099,25 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { currentTextId = null } } else if (isResponseOutputItemDoneReasoningChunk(value)) { - const activeReasoningPart = activeReasoning[value.item.id] + const activeReasoningPart = activeReasoning[value.output_index] if (activeReasoningPart) { for (const summaryIndex of activeReasoningPart.summaryParts) { controller.enqueue({ type: "reasoning-end", - id: `${value.item.id}:${summaryIndex}`, + id: `${activeReasoningPart.canonicalId}:${summaryIndex}`, providerMetadata: { openai: { - itemId: value.item.id, + itemId: activeReasoningPart.canonicalId, reasoningEncryptedContent: value.item.encrypted_content ?? null, }, }, }) } + delete activeReasoning[value.output_index] + if (currentReasoningOutputIndex === value.output_index) { + currentReasoningOutputIndex = null + } } - delete activeReasoning[value.item.id] } } else if (isResponseFunctionCallArgumentsDeltaChunk(value)) { const toolCall = ongoingToolCalls[value.output_index] @@ -1198,32 +1209,40 @@ export class OpenAIResponsesLanguageModel implements LanguageModelV2 { logprobs.push(value.logprobs) } } else if (isResponseReasoningSummaryPartAddedChunk(value)) { + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + // the first reasoning start is pushed in isResponseOutputItemAddedReasoningChunk. - if (value.summary_index > 0) { - activeReasoning[value.item_id]?.summaryParts.push(value.summary_index) + if (activeItem && value.summary_index > 0) { + activeItem.summaryParts.push(value.summary_index) controller.enqueue({ type: "reasoning-start", - id: `${value.item_id}:${value.summary_index}`, + id: `${activeItem.canonicalId}:${value.summary_index}`, providerMetadata: { openai: { - itemId: value.item_id, - reasoningEncryptedContent: activeReasoning[value.item_id]?.encryptedContent ?? null, + itemId: activeItem.canonicalId, + reasoningEncryptedContent: activeItem.encryptedContent ?? null, }, }, }) } } else if (isResponseReasoningSummaryTextDeltaChunk(value)) { - controller.enqueue({ - type: "reasoning-delta", - id: `${value.item_id}:${value.summary_index}`, - delta: value.delta, - providerMetadata: { - openai: { - itemId: value.item_id, + const activeItem = + currentReasoningOutputIndex !== null ? activeReasoning[currentReasoningOutputIndex] : null + + if (activeItem) { + controller.enqueue({ + type: "reasoning-delta", + id: `${activeItem.canonicalId}:${value.summary_index}`, + delta: value.delta, + providerMetadata: { + openai: { + itemId: activeItem.canonicalId, + }, }, - }, - }) + }) + } } else if (isResponseFinishedChunk(value)) { finishReason = mapOpenAIResponseFinishReason({ finishReason: value.response.incomplete_details?.reason, From cb93d509bb9581a836c4d8d261d7653d45069eab Mon Sep 17 00:00:00 2001 From: Nate Smyth Date: Sat, 17 Jan 2026 17:47:22 -0500 Subject: [PATCH 2/2] fix: route compatible GPT-5* models to /responses --- packages/opencode/src/provider/provider.ts | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index bcb115edf41..e7d129887ae 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -120,7 +120,9 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { + // All GPT-5* models support the /responses API with reasoning + // But only GPT-5* models + if (modelID.startsWith("gpt-5")) { return sdk.responses(modelID) } return sdk.chat(modelID) @@ -132,7 +134,7 @@ export namespace Provider { return { autoload: false, async getModel(sdk: any, modelID: string, _options?: Record) { - if (modelID.includes("codex")) { + if (modelID.startsWith("gpt-5")) { return sdk.responses(modelID) } return sdk.chat(modelID) @@ -609,13 +611,13 @@ export namespace Provider { }, experimentalOver200K: model.cost?.context_over_200k ? { - cache: { - read: model.cost.context_over_200k.cache_read ?? 0, - write: model.cost.context_over_200k.cache_write ?? 0, - }, - input: model.cost.context_over_200k.input, - output: model.cost.context_over_200k.output, - } + cache: { + read: model.cost.context_over_200k.cache_read ?? 0, + write: model.cost.context_over_200k.cache_write ?? 0, + }, + input: model.cost.context_over_200k.input, + output: model.cost.context_over_200k.output, + } : undefined, }, limit: {