summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAiden Cline <[email protected]>2026-02-01 01:36:44 -0600
committerGitHub <[email protected]>2026-02-01 01:36:44 -0600
commitd1d7447493e19ff6236b4363b502fc2de59bf67d (patch)
tree0e5d9db331b9a11af56d9c56e3637fac2e01b919
parentc3faeae9d0a71bb6434178f07ba5ac51f348196a (diff)
downloadopencode-d1d7447493e19ff6236b4363b502fc2de59bf67d.tar.gz
opencode-d1d7447493e19ff6236b4363b502fc2de59bf67d.zip
fix: ensure switching anthropic models mid convo on copilot works without errors, fix issue with reasoning opaque not being picked up for gemini models (#11569)
-rw-r--r--packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts4
-rw-r--r--packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts19
-rw-r--r--packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts31
-rw-r--r--packages/opencode/test/provider/copilot/copilot-chat-model.test.ts35
4 files changed, 83 insertions, 6 deletions
diff --git a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts
index 642d7145f..d6f7cb34b 100644
--- a/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts
+++ b/packages/opencode/src/provider/sdk/copilot/chat/convert-to-openai-compatible-chat-messages.ts
@@ -100,7 +100,7 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
break
}
case "reasoning": {
- reasoningText = part.text
+ if (part.text) reasoningText = part.text
break
}
case "tool-call": {
@@ -122,7 +122,7 @@ export function convertToOpenAICompatibleChatMessages(prompt: LanguageModelV2Pro
role: "assistant",
content: text || null,
tool_calls: toolCalls.length > 0 ? toolCalls : undefined,
- reasoning_text: reasoningText,
+ reasoning_text: reasoningOpaque ? reasoningText : undefined,
reasoning_opaque: reasoningOpaque,
...metadata,
})
diff --git a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts
index 94641e640..c85d3f3d1 100644
--- a/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts
+++ b/packages/opencode/src/provider/sdk/copilot/chat/openai-compatible-chat-language-model.ts
@@ -219,7 +219,13 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
// text content:
const text = choice.message.content
if (text != null && text.length > 0) {
- content.push({ type: "text", text })
+ content.push({
+ type: "text",
+ text,
+ providerMetadata: choice.message.reasoning_opaque
+ ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
+ : undefined,
+ })
}
// reasoning content (Copilot uses reasoning_text):
@@ -243,6 +249,9 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: toolCall.function.arguments!,
+ providerMetadata: choice.message.reasoning_opaque
+ ? { copilot: { reasoningOpaque: choice.message.reasoning_opaque } }
+ : undefined,
})
}
}
@@ -478,7 +487,11 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
}
if (!isActiveText) {
- controller.enqueue({ type: "text-start", id: "txt-0" })
+ controller.enqueue({
+ type: "text-start",
+ id: "txt-0",
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
+ })
isActiveText = true
}
@@ -559,6 +572,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: toolCall.function.arguments,
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
})
toolCall.hasFinished = true
}
@@ -601,6 +615,7 @@ export class OpenAICompatibleChatLanguageModel implements LanguageModelV2 {
toolCallId: toolCall.id ?? generateId(),
toolName: toolCall.function.name,
input: toolCall.function.arguments,
+ providerMetadata: reasoningOpaque ? { copilot: { reasoningOpaque } } : undefined,
})
toolCall.hasFinished = true
}
diff --git a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts
index ffc746911..9f305123a 100644
--- a/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts
+++ b/packages/opencode/test/provider/copilot/convert-to-copilot-messages.test.ts
@@ -354,7 +354,7 @@ describe("tool calls", () => {
})
describe("reasoning (copilot-specific)", () => {
- test("should include reasoning_text from reasoning part", () => {
+ test("should omit reasoning_text without reasoning_opaque", () => {
const result = convertToCopilotMessages([
{
role: "assistant",
@@ -370,7 +370,7 @@ describe("reasoning (copilot-specific)", () => {
role: "assistant",
content: "The answer is 42.",
tool_calls: undefined,
- reasoning_text: "Let me think about this...",
+ reasoning_text: undefined,
reasoning_opaque: undefined,
},
])
@@ -404,6 +404,33 @@ describe("reasoning (copilot-specific)", () => {
])
})
+ test("should include reasoning_opaque from text part providerOptions", () => {
+ const result = convertToCopilotMessages([
+ {
+ role: "assistant",
+ content: [
+ {
+ type: "text",
+ text: "Done!",
+ providerOptions: {
+ copilot: { reasoningOpaque: "opaque-text-456" },
+ },
+ },
+ ],
+ },
+ ])
+
+ expect(result).toEqual([
+ {
+ role: "assistant",
+ content: "Done!",
+ tool_calls: undefined,
+ reasoning_text: undefined,
+ reasoning_opaque: "opaque-text-456",
+ },
+ ])
+ })
+
test("should handle reasoning-only assistant message", () => {
const result = convertToCopilotMessages([
{
diff --git a/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts b/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts
index 0b82c1868..562da4507 100644
--- a/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts
+++ b/packages/opencode/test/provider/copilot/copilot-chat-model.test.ts
@@ -65,6 +65,12 @@ const FIXTURES = {
`data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null,"role":"assistant","tool_calls":[{"function":{"arguments":"{\\"code\\":\\"1 + 1\\"}","name":"project_eval"},"id":"call_MHw3RDhmT1J5Z3B6WlhpVjlveTc","index":0,"type":"function"}],"reasoning_opaque":"ytGNWFf2doK38peANDvm7whkLPKrd+Fv6/k34zEPBF6Qwitj4bTZT0FBXleydLb6"}}],"created":1766068644,"id":"oBFEaafzD9DVlOoPkY3l4Qs","usage":{"completion_tokens":12,"prompt_tokens":8677,"prompt_tokens_details":{"cached_tokens":3692},"total_tokens":8768,"reasoning_tokens":79},"model":"gemini-3-pro-preview"}`,
`data: [DONE]`,
],
+
+ reasoningOpaqueWithToolCallsNoReasoningText: [
+ `data: {"choices":[{"index":0,"delta":{"content":null,"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"read_file"},"id":"call_reasoning_only","index":0,"type":"function"}],"reasoning_opaque":"opaque-xyz"}}],"created":1769917420,"id":"opaque-only","usage":{"completion_tokens":0,"prompt_tokens":0,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":0,"reasoning_tokens":0},"model":"gemini-3-flash-preview"}`,
+ `data: {"choices":[{"finish_reason":"tool_calls","index":0,"delta":{"content":null,"role":"assistant","tool_calls":[{"function":{"arguments":"{}","name":"read_file"},"id":"call_reasoning_only_2","index":1,"type":"function"}]}}],"created":1769917420,"id":"opaque-only","usage":{"completion_tokens":12,"prompt_tokens":123,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":135,"reasoning_tokens":0},"model":"gemini-3-flash-preview"}`,
+ `data: [DONE]`,
+ ],
}
function createMockFetch(chunks: string[]) {
@@ -447,6 +453,35 @@ describe("doStream", () => {
})
})
+ test("should attach reasoning_opaque to tool calls without reasoning_text", async () => {
+ const mockFetch = createMockFetch(FIXTURES.reasoningOpaqueWithToolCallsNoReasoningText)
+ const model = createModel(mockFetch)
+
+ const { stream } = await model.doStream({
+ prompt: TEST_PROMPT,
+ includeRawChunks: false,
+ })
+
+ const parts = await convertReadableStreamToArray(stream)
+ const reasoningParts = parts.filter(
+ (p) => p.type === "reasoning-start" || p.type === "reasoning-delta" || p.type === "reasoning-end",
+ )
+
+ expect(reasoningParts).toHaveLength(0)
+
+ const toolCall = parts.find((p) => p.type === "tool-call" && p.toolCallId === "call_reasoning_only")
+ expect(toolCall).toMatchObject({
+ type: "tool-call",
+ toolCallId: "call_reasoning_only",
+ toolName: "read_file",
+ providerMetadata: {
+ copilot: {
+ reasoningOpaque: "opaque-xyz",
+ },
+ },
+ })
+ })
+
test("should include response metadata from first chunk", async () => {
const mockFetch = createMockFetch(FIXTURES.basicText)
const model = createModel(mockFetch)