summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorNateSmyth <[email protected]>2026-01-19 01:11:54 -0500
committerGitHub <[email protected]>2026-01-19 00:11:54 -0600
commit260ab60c0b9ba1667a326c1b19ea46473156df0c (patch)
tree2d1060fe25dc1560ac9a62170ea6fcdbf501fb42
parente2f1f4d81e152f19f6f9d2f8ed873f310296eba4 (diff)
downloadopencode-260ab60c0b9ba1667a326c1b19ea46473156df0c.tar.gz
opencode-260ab60c0b9ba1667a326c1b19ea46473156df0c.zip
fix: track reasoning by output_index for copilot compatibility (#9124)
Co-authored-by: Aiden Cline <[email protected]>
-rw-r--r--packages/opencode/src/provider/provider.ts14
-rw-r--r--packages/opencode/src/provider/sdk/openai-compatible/src/responses/openai-responses-language-model.ts59
2 files changed, 46 insertions, 27 deletions
diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts
index d4d4b3e26..ad57867df 100644
--- a/packages/opencode/src/provider/provider.ts
+++ b/packages/opencode/src/provider/provider.ts
@@ -615,13 +615,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: {
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 94b0edaf3..0990b7e00 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,