Skip to content
Closed
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
29668ce
feat: support copilot reasoning_opaque and reasoning_text
caozhiyuan Nov 19, 2025
a2467d3
feat: add signature field to AnthropicThinkingBlock
caozhiyuan Nov 19, 2025
58f7a45
feat: add idleTimeout configuration for bun server
caozhiyuan Nov 19, 2025
3fa5519
feat: enhance reasoning handling in tool calls and change the thinkin…
caozhiyuan Nov 19, 2025
dfb40d2
feat: conditionally handle reasoningOpaque in handleFinish based on t…
caozhiyuan Nov 19, 2025
7657d87
fix: handleReasoningOpaqueInToolCalls add isToolBlockOpen judge
caozhiyuan Nov 20, 2025
7f8187b
feat: support claude model thinking block
caozhiyuan Dec 3, 2025
cbe12eb
feat: enhance thinking budget calculation and rename variables for cl…
caozhiyuan Dec 10, 2025
ebcacb2
feat: update Copilot version and API version in api-config; adjust fa…
caozhiyuan Dec 11, 2025
0d6f7aa
feat: update Copilot version to 0.35.0 and fallback VSCode version to…
caozhiyuan Dec 11, 2025
dcafbe1
fix: simplify copilotBaseUrl logic and correct openai-intent header v…
caozhiyuan Dec 13, 2025
5175245
feat: interleaved thinking support
caozhiyuan Dec 31, 2025
dd80c8d
feat: enhance system prompt handling for interleaved thinking with th…
caozhiyuan Dec 31, 2025
e45c6db
feat: compatible with copilot API returning content->reasoning_text->…
caozhiyuan Jan 2, 2026
0afccfa
fix(api-config): use default API URL when account type is individual
HyunggyuJang Jan 1, 2026
8191930
Merge pull request #65 from HyunggyuJang/fix-individual-account-url
caozhiyuan Jan 2, 2026
67b357a
feat: enforce interleaved thinking protocol in message handling
caozhiyuan Jan 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 5 additions & 6 deletions src/lib/api-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@ export const standardHeaders = () => ({
accept: "application/json",
})

const COPILOT_VERSION = "0.26.7"
const COPILOT_VERSION = "0.35.0"
const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`
const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`

const API_VERSION = "2025-04-01"
const API_VERSION = "2025-10-01"

export const copilotBaseUrl = (state: State) =>
state.accountType === "individual" ?
"https://api.githubcopilot.com"
: `https://api.${state.accountType}.githubcopilot.com`
`https://api.${state.accountType}.githubcopilot.com`
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactored copilotBaseUrl function now unconditionally uses the template https://api.${state.accountType}.githubcopilot.com, which will result in https://api.individual.githubcopilot.com for individual accounts. The previous implementation used https://api.githubcopilot.com (without the subdomain) for individual accounts. This is a breaking change that may cause API requests to fail for individual account users. Please verify that the GitHub Copilot API supports the new URL format for individual accounts, or restore the conditional logic.

Suggested change
`https://api.${state.accountType}.githubcopilot.com`
state.accountType === "individual"
? "https://api.githubcopilot.com"
: `https://api.${state.accountType}.githubcopilot.com`

Copilot uses AI. Check for mistakes.

export const copilotHeaders = (state: State, vision: boolean = false) => {
const headers: Record<string, string> = {
Authorization: `Bearer ${state.copilotToken}`,
Expand All @@ -25,7 +24,7 @@ export const copilotHeaders = (state: State, vision: boolean = false) => {
"editor-version": `vscode/${state.vsCodeVersion}`,
"editor-plugin-version": EDITOR_PLUGIN_VERSION,
"user-agent": USER_AGENT,
"openai-intent": "conversation-panel",
"openai-intent": "conversation-agent",
"x-github-api-version": API_VERSION,
"x-request-id": randomUUID(),
"x-vscode-user-agent-library-version": "electron-fetch",
Expand Down
3 changes: 3 additions & 0 deletions src/lib/tokenizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ const calculateMessageTokens = (
const tokensPerName = 1
let tokens = tokensPerMessage
for (const [key, value] of Object.entries(message)) {
if (key === "reasoning_opaque") {
continue
}
if (typeof value === "string") {
tokens += encoder.encode(value).length
}
Expand Down
2 changes: 2 additions & 0 deletions src/routes/messages/anthropic-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ export interface AnthropicToolUseBlock {
export interface AnthropicThinkingBlock {
type: "thinking"
thinking: string
signature: string
}

export type AnthropicUserContentBlock =
Expand Down Expand Up @@ -196,6 +197,7 @@ export interface AnthropicStreamState {
messageStartSent: boolean
contentBlockIndex: number
contentBlockOpen: boolean
thinkingBlockOpen: boolean
toolCalls: {
[openAIToolIndex: number]: {
id: string
Expand Down
1 change: 1 addition & 0 deletions src/routes/messages/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ export async function handleCompletion(c: Context) {
contentBlockIndex: 0,
contentBlockOpen: false,
toolCalls: {},
thinkingBlockOpen: false,
}

for await (const rawEvent of response) {
Expand Down
184 changes: 136 additions & 48 deletions src/routes/messages/non-stream-translation.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import type { Model } from "~/services/copilot/get-models"

import { state } from "~/lib/state"
import {
type ChatCompletionResponse,
type ChatCompletionsPayload,
Expand All @@ -11,7 +14,6 @@ import {
import {
type AnthropicAssistantContentBlock,
type AnthropicAssistantMessage,
type AnthropicMessage,
type AnthropicMessagesPayload,
type AnthropicResponse,
type AnthropicTextBlock,
Expand All @@ -29,11 +31,15 @@ import { mapOpenAIStopReasonToAnthropic } from "./utils"
export function translateToOpenAI(
payload: AnthropicMessagesPayload,
): ChatCompletionsPayload {
const modelId = translateModelName(payload.model)
const model = state.models?.data.find((m) => m.id === modelId)
const thinkingBudget = getThinkingBudget(payload, model)
return {
model: translateModelName(payload.model),
model: modelId,
messages: translateAnthropicMessagesToOpenAI(
payload.messages,
payload.system,
payload,
modelId,
thinkingBudget,
),
max_tokens: payload.max_tokens,
stop: payload.stop_sequences,
Expand All @@ -43,45 +49,96 @@ export function translateToOpenAI(
user: payload.metadata?.user_id,
tools: translateAnthropicToolsToOpenAI(payload.tools),
tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice),
thinking_budget: thinkingBudget,
}
}

function getThinkingBudget(
payload: AnthropicMessagesPayload,
model: Model | undefined,
): number | undefined {
const thinking = payload.thinking
if (model && thinking) {
const maxThinkingBudget = Math.min(
model.capabilities.supports.max_thinking_budget ?? 0,
(model.capabilities.limits.max_output_tokens ?? 0) - 1,
Comment on lines +62 to +64
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking budget calculation has a potential edge case issue. On line 64, when max_output_tokens is 0 or undefined, the calculation (model.capabilities.limits.max_output_tokens ?? 0) - 1 results in -1, which could lead to maxThinkingBudget being negative when compared with max_thinking_budget. While the check if (maxThinkingBudget > 0) on line 66 guards against negative values, this logic could be clearer. Consider explicitly handling the case where max_output_tokens is unavailable or zero before the subtraction.

Suggested change
const maxThinkingBudget = Math.min(
model.capabilities.supports.max_thinking_budget ?? 0,
(model.capabilities.limits.max_output_tokens ?? 0) - 1,
const maxTokensLimit =
model.capabilities.limits.max_output_tokens &&
model.capabilities.limits.max_output_tokens > 1
? model.capabilities.limits.max_output_tokens - 1
: 0
const maxThinkingBudget = Math.min(
model.capabilities.supports.max_thinking_budget ?? 0,
maxTokensLimit,

Copilot uses AI. Check for mistakes.
)
if (maxThinkingBudget > 0 && thinking.budget_tokens !== undefined) {
const budgetTokens = Math.min(thinking.budget_tokens, maxThinkingBudget)
return Math.max(
budgetTokens,
model.capabilities.supports.min_thinking_budget ?? 1024,
)
Comment on lines +67 to +71
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking budget calculation could return values that don't satisfy the minimum requirement. When thinking.budget_tokens is less than min_thinking_budget, the Math.max ensures the minimum is met. However, this could exceed maxThinkingBudget if the minimum is larger than the maximum. Consider validating that min_thinking_budget <= maxThinkingBudget before the calculation, or returning undefined if the constraints cannot be satisfied.

Suggested change
const budgetTokens = Math.min(thinking.budget_tokens, maxThinkingBudget)
return Math.max(
budgetTokens,
model.capabilities.supports.min_thinking_budget ?? 1024,
)
const minThinkingBudget =
model.capabilities.supports.min_thinking_budget ?? 1024
// If the minimum required budget exceeds the maximum allowed, the
// constraints cannot be satisfied; fall back to no thinking budget.
if (minThinkingBudget > maxThinkingBudget) {
return undefined
}
const budgetTokens = Math.min(thinking.budget_tokens, maxThinkingBudget)
return Math.max(budgetTokens, minThinkingBudget)

Copilot uses AI. Check for mistakes.
}
}
return undefined
}
Comment on lines +56 to +75
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new thinking budget calculation logic (getThinkingBudget function) and interleaved thinking prompt injection for Claude models lack test coverage. These are significant new features that handle complex logic including min/max budget constraints and model-specific behavior. Consider adding tests that verify: 1) budget calculation with various model capabilities, 2) the system prompt injection for Claude models with thinking enabled, 3) the system-reminder message insertion.

Copilot uses AI. Check for mistakes.

function translateModelName(model: string): string {
// Subagent requests use a specific model number which Copilot doesn't support
if (model.startsWith("claude-sonnet-4-")) {
return model.replace(/^claude-sonnet-4-.*/, "claude-sonnet-4")
} else if (model.startsWith("claude-opus-")) {
} else if (model.startsWith("claude-opus-4-")) {
return model.replace(/^claude-opus-4-.*/, "claude-opus-4")
Comment on lines +81 to 82
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The model name translation for "claude-opus-4-" now requires the prefix to be exactly "claude-opus-4-" (with the trailing dash), while the previous version matched "claude-opus-" (without the "4"). This change appears intentional but may break compatibility with "claude-opus-3-" or other opus model variants that were previously supported. Please verify this is the intended behavior or restore support for all opus model versions.

Suggested change
} else if (model.startsWith("claude-opus-4-")) {
return model.replace(/^claude-opus-4-.*/, "claude-opus-4")
} else if (model.startsWith("claude-opus-")) {
return model.replace(/^(claude-opus-\d+).*/, "$1")

Copilot uses AI. Check for mistakes.
}
return model
}

function translateAnthropicMessagesToOpenAI(
anthropicMessages: Array<AnthropicMessage>,
system: string | Array<AnthropicTextBlock> | undefined,
payload: AnthropicMessagesPayload,
modelId: string,
thinkingBudget: number | undefined,
): Array<Message> {
const systemMessages = handleSystemPrompt(system)

const otherMessages = anthropicMessages.flatMap((message) =>
const systemMessages = handleSystemPrompt(
payload.system,
modelId,
thinkingBudget,
)
const otherMessages = payload.messages.flatMap((message) =>
message.role === "user" ?
handleUserMessage(message)
: handleAssistantMessage(message),
: handleAssistantMessage(message, modelId),
)

if (modelId.startsWith("claude") && thinkingBudget) {
const thinkingMessage = {
role: "user",
content:
"<system-reminder>Please strictly follow Interleaved thinking</system-reminder>",
} as Message
return [...systemMessages, thinkingMessage, ...otherMessages]
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system-reminder message is placed immediately after system messages and before all other messages. This placement may break the expected message order when there are existing user/assistant message exchanges. The reminder should ideally be placed at the end of the messages array to avoid disrupting the conversation flow, or inserted more strategically based on the context. Consider moving it to the end or documenting why this specific placement is required.

Suggested change
return [...systemMessages, thinkingMessage, ...otherMessages]
return [...systemMessages, ...otherMessages, thinkingMessage]

Copilot uses AI. Check for mistakes.
}
Comment on lines +102 to +117
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The system prompt injection logic (lines 102-117 and 131-143) only activates when thinkingBudget is truthy. However, getThinkingBudget returns undefined in several cases: when model is not found, when payload.thinking is not provided, when thinking.budget_tokens is undefined, or when maxThinkingBudget is 0 or negative. This means the interleaved thinking protocol instructions won't be injected unless all these conditions are met. Consider whether the protocol instructions should be injected whenever payload.thinking exists, regardless of budget calculation success, or document this behavior clearly so users understand when thinking protocol is enabled.

Copilot uses AI. Check for mistakes.
return [...systemMessages, ...otherMessages]
}

function handleSystemPrompt(
system: string | Array<AnthropicTextBlock> | undefined,
modelId: string,
thinkingBudget: number | undefined,
): Array<Message> {
if (!system) {
return []
}

let extraPrompt = ""
if (modelId.startsWith("claude") && thinkingBudget) {
extraPrompt = `
## Interleaved thinking
- Interleaved thinking is enabled
- You MUST think after receiving tool results before deciding the next action or final answer.
`
}
Comment on lines +56 to +143
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new thinking budget calculation logic (lines 56-75) and system prompt injection logic (lines 102-117, 131-143) lack test coverage. These are critical features that manipulate model behavior and user inputs. Consider adding tests that verify: 1) thinking budget is correctly calculated when thinking.budget_tokens is provided, 2) thinking budget respects min/max boundaries from model capabilities, 3) system prompt injection happens only for Claude models with thinking budget, 4) the interleaved thinking protocol reminder is correctly prepended to the first user message.

Copilot uses AI. Check for mistakes.

if (typeof system === "string") {
return [{ role: "system", content: system }]
return [{ role: "system", content: system + extraPrompt }]
} else {
const systemText = system.map((block) => block.text).join("\n\n")
const systemText = system
.map((block, index) => {
if (index === 0) {
return block.text + extraPrompt
}
return block.text
})
.join("\n\n")
return [{ role: "system", content: systemText }]
}
}
Expand Down Expand Up @@ -125,6 +182,7 @@ function handleUserMessage(message: AnthropicUserMessage): Array<Message> {

function handleAssistantMessage(
message: AnthropicAssistantMessage,
modelId: string,
): Array<Message> {
if (!Array.isArray(message.content)) {
return [
Expand All @@ -139,25 +197,40 @@ function handleAssistantMessage(
(block): block is AnthropicToolUseBlock => block.type === "tool_use",
)

const textBlocks = message.content.filter(
(block): block is AnthropicTextBlock => block.type === "text",
)

const thinkingBlocks = message.content.filter(
let thinkingBlocks = message.content.filter(
(block): block is AnthropicThinkingBlock => block.type === "thinking",
)

// Combine text and thinking blocks, as OpenAI doesn't have separate thinking blocks
const allTextContent = [
...textBlocks.map((b) => b.text),
...thinkingBlocks.map((b) => b.thinking),
].join("\n\n")
if (modelId.startsWith("claude")) {
thinkingBlocks = thinkingBlocks.filter(
(b) =>
b.thinking
&& b.thinking.length > 0
&& b.signature
&& b.signature.length > 0
// gpt signature has @ in it, so filter those out for claude models
&& !b.signature.includes("@"),
Comment on lines +224 to +226
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking blocks filtering logic for Claude models includes a check for signatures that contain "@" (line 226) with the comment "gpt signature has @ in it, so filter those out for claude models". However, this heuristic approach is brittle - there's no guarantee that all GPT signatures will contain "@" or that Claude signatures will never contain "@". Consider using a more robust approach, such as checking the model ID of the original message source or using a dedicated signature format field to distinguish between model types.

Suggested change
&& b.signature.length > 0
// gpt signature has @ in it, so filter those out for claude models
&& !b.signature.includes("@"),
&& b.signature.length > 0,

Copilot uses AI. Check for mistakes.
)
}

const thinkingContents = thinkingBlocks
.filter((b) => b.thinking && b.thinking.length > 0)
.map((b) => b.thinking)
Comment on lines +214 to +232
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking blocks are filtered twice - once on line 200-202 to extract all thinking blocks, then again on lines 216-218 to filter those with non-empty thinking content. This redundant filtering is inefficient. Consider combining these filters or restructuring the logic to avoid processing the same blocks multiple times.

Copilot uses AI. Check for mistakes.
Comment on lines +218 to +232
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The filtering on line 231 checks if b.thinking && b.thinking.length > 0, which is redundant for Claude models because the same check was already done on lines 221-222. While this doesn't cause incorrect behavior, it adds unnecessary processing. Consider restructuring to avoid double filtering - for example, apply the thinking content filter before the Claude-specific signature filter, or ensure thinking blocks always have valid thinking content when they're created.

Suggested change
if (modelId.startsWith("claude")) {
thinkingBlocks = thinkingBlocks.filter(
(b) =>
b.thinking
&& b.thinking.length > 0
&& b.signature
&& b.signature.length > 0
// gpt signature has @ in it, so filter those out for claude models
&& !b.signature.includes("@"),
)
}
const thinkingContents = thinkingBlocks
.filter((b) => b.thinking && b.thinking.length > 0)
.map((b) => b.thinking)
// First, ensure all thinking blocks have non-empty thinking content
thinkingBlocks = thinkingBlocks.filter(
(b) => b.thinking && b.thinking.length > 0,
)
if (modelId.startsWith("claude")) {
thinkingBlocks = thinkingBlocks.filter(
(b) =>
b.signature
&& b.signature.length > 0
// gpt signature has @ in it, so filter those out for claude models
&& !b.signature.includes("@"),
)
}
const thinkingContents = thinkingBlocks.map((b) => b.thinking)

Copilot uses AI. Check for mistakes.

const allThinkingContent =
thinkingContents.length > 0 ? thinkingContents.join("\n\n") : undefined

const signature = thinkingBlocks.find(
(b) => b.signature && b.signature.length > 0,
)?.signature

return toolUseBlocks.length > 0 ?
[
{
role: "assistant",
content: allTextContent || null,
content: mapContent(message.content),
reasoning_text: allThinkingContent,
reasoning_opaque: signature,
tool_calls: toolUseBlocks.map((toolUse) => ({
id: toolUse.id,
type: "function",
Expand All @@ -172,6 +245,8 @@ function handleAssistantMessage(
{
role: "assistant",
content: mapContent(message.content),
reasoning_text: allThinkingContent,
reasoning_opaque: signature,
},
]
}
Expand All @@ -191,11 +266,8 @@ function mapContent(
const hasImage = content.some((block) => block.type === "image")
if (!hasImage) {
return content
.filter(
(block): block is AnthropicTextBlock | AnthropicThinkingBlock =>
block.type === "text" || block.type === "thinking",
)
.map((block) => (block.type === "text" ? block.text : block.thinking))
.filter((block): block is AnthropicTextBlock => block.type === "text")
.map((block) => block.text)
.join("\n\n")
}

Expand All @@ -204,12 +276,6 @@ function mapContent(
switch (block.type) {
case "text": {
contentParts.push({ type: "text", text: block.text })

break
}
case "thinking": {
contentParts.push({ type: "text", text: block.thinking })

break
}
case "image": {
Expand All @@ -219,7 +285,6 @@ function mapContent(
url: `data:${block.source.media_type};base64,${block.source.data}`,
},
})

break
}
// No default
Expand Down Expand Up @@ -282,34 +347,32 @@ export function translateToAnthropic(
response: ChatCompletionResponse,
): AnthropicResponse {
// Merge content from all choices
const allTextBlocks: Array<AnthropicTextBlock> = []
const allToolUseBlocks: Array<AnthropicToolUseBlock> = []
let stopReason: "stop" | "length" | "tool_calls" | "content_filter" | null =
null // default
stopReason = response.choices[0]?.finish_reason ?? stopReason
const assistantContentBlocks: Array<AnthropicAssistantContentBlock> = []
let stopReason = response.choices[0]?.finish_reason ?? null

// Process all choices to extract text and tool use blocks
for (const choice of response.choices) {
const textBlocks = getAnthropicTextBlocks(choice.message.content)
const thinkBlocks = getAnthropicThinkBlocks(
choice.message.reasoning_text,
choice.message.reasoning_opaque,
)
const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls)

allTextBlocks.push(...textBlocks)
allToolUseBlocks.push(...toolUseBlocks)
assistantContentBlocks.push(...thinkBlocks, ...textBlocks, ...toolUseBlocks)
Copy link

Copilot AI Dec 31, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The thinking blocks are placed before text blocks in the response (line 362), which means the thinking content will always appear first in the response regardless of its original position. This may not accurately represent the interleaved thinking flow if text was generated before thinking or if there were multiple rounds of thinking and text. Consider tracking the original order of blocks or documenting why thinking blocks must always come first in the response.

Suggested change
assistantContentBlocks.push(...thinkBlocks, ...textBlocks, ...toolUseBlocks)
assistantContentBlocks.push(...textBlocks, ...thinkBlocks, ...toolUseBlocks)

Copilot uses AI. Check for mistakes.

// Use the finish_reason from the first choice, or prioritize tool_calls
if (choice.finish_reason === "tool_calls" || stopReason === "stop") {
stopReason = choice.finish_reason
}
}

// Note: GitHub Copilot doesn't generate thinking blocks, so we don't include them in responses

return {
id: response.id,
type: "message",
role: "assistant",
model: response.model,
content: [...allTextBlocks, ...allToolUseBlocks],
content: assistantContentBlocks,
stop_reason: mapOpenAIStopReasonToAnthropic(stopReason),
stop_sequence: null,
usage: {
Expand All @@ -329,7 +392,7 @@ export function translateToAnthropic(
function getAnthropicTextBlocks(
messageContent: Message["content"],
): Array<AnthropicTextBlock> {
if (typeof messageContent === "string") {
if (typeof messageContent === "string" && messageContent.length > 0) {
return [{ type: "text", text: messageContent }]
}

Expand All @@ -342,6 +405,31 @@ function getAnthropicTextBlocks(
return []
}

function getAnthropicThinkBlocks(
reasoningText: string | null | undefined,
reasoningOpaque: string | null | undefined,
): Array<AnthropicThinkingBlock> {
if (reasoningText && reasoningText.length > 0) {
return [
{
type: "thinking",
thinking: reasoningText,
signature: reasoningOpaque || "",
Comment on lines +426 to +431
Copy link

Copilot AI Jan 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The signature field in AnthropicThinkingBlock is now required (line 59 in anthropic-types.ts), but when reasoningOpaque is not provided, it defaults to an empty string (line 431). This is a breaking API change that could affect API consumers. Consider: 1) making the signature field optional to maintain backwards compatibility, 2) documenting that signature can be an empty string and what that means semantically, or 3) only including thinking blocks when both thinking and signature are non-empty to avoid exposing incomplete thinking blocks.

Suggested change
if (reasoningText && reasoningText.length > 0) {
return [
{
type: "thinking",
thinking: reasoningText,
signature: reasoningOpaque || "",
if (
reasoningText &&
reasoningText.length > 0 &&
reasoningOpaque &&
reasoningOpaque.length > 0
) {
return [
{
type: "thinking",
thinking: reasoningText,
signature: reasoningOpaque,

Copilot uses AI. Check for mistakes.
},
]
}
if (reasoningOpaque && reasoningOpaque.length > 0) {
return [
{
type: "thinking",
thinking: "",
signature: reasoningOpaque,
},
]
}
return []
}

function getAnthropicToolUseBlocks(
toolCalls: Array<ToolCall> | undefined,
): Array<AnthropicToolUseBlock> {
Expand Down
Loading