From 29668cef1fd6658503a23fdfb90bcab44df70aeb Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Wed, 19 Nov 2025 18:37:15 +0800 Subject: [PATCH 01/16] feat: support copilot reasoning_opaque and reasoning_text --- src/routes/messages/anthropic-types.ts | 1 + src/routes/messages/handler.ts | 1 + src/routes/messages/non-stream-translation.ts | 80 +++-- src/routes/messages/stream-translation.ts | 278 ++++++++++++++---- .../copilot/create-chat-completions.ts | 10 +- tests/anthropic-request.test.ts | 4 +- tests/anthropic-response.test.ts | 2 + 7 files changed, 280 insertions(+), 96 deletions(-) diff --git a/src/routes/messages/anthropic-types.ts b/src/routes/messages/anthropic-types.ts index 881fffcc..03f24d10 100644 --- a/src/routes/messages/anthropic-types.ts +++ b/src/routes/messages/anthropic-types.ts @@ -196,6 +196,7 @@ export interface AnthropicStreamState { messageStartSent: boolean contentBlockIndex: number contentBlockOpen: boolean + thinkingBlockOpen: boolean toolCalls: { [openAIToolIndex: number]: { id: string diff --git a/src/routes/messages/handler.ts b/src/routes/messages/handler.ts index 85dbf624..a40d3f1d 100644 --- a/src/routes/messages/handler.ts +++ b/src/routes/messages/handler.ts @@ -60,6 +60,7 @@ export async function handleCompletion(c: Context) { contentBlockIndex: 0, contentBlockOpen: false, toolCalls: {}, + thinkingBlockOpen: false, } for await (const rawEvent of response) { diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index dc41e638..3b1a491d 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -139,25 +139,26 @@ 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( (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") + const allThinkingContent = thinkingBlocks + .filter((b) => b.thinking && b.thinking.length > 0) + .map((b) => b.thinking) + .join("\n\n") + + 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", @@ -172,6 +173,8 @@ function handleAssistantMessage( { role: "assistant", content: mapContent(message.content), + reasoning_text: allThinkingContent, + reasoning_opaque: signature, }, ] } @@ -191,11 +194,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") } @@ -204,12 +204,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": { @@ -219,7 +213,6 @@ function mapContent( url: `data:${block.source.media_type};base64,${block.source.data}`, }, }) - break } // No default @@ -282,19 +275,19 @@ export function translateToAnthropic( response: ChatCompletionResponse, ): AnthropicResponse { // Merge content from all choices - const allTextBlocks: Array = [] - const allToolUseBlocks: Array = [] - let stopReason: "stop" | "length" | "tool_calls" | "content_filter" | null = - null // default - stopReason = response.choices[0]?.finish_reason ?? stopReason + const assistantContentBlocks: Array = [] + 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 thingBlocks = getAnthropicThinkBlocks( + choice.message.reasoning_text, + choice.message.reasoning_opaque, + ) const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls) - allTextBlocks.push(...textBlocks) - allToolUseBlocks.push(...toolUseBlocks) + assistantContentBlocks.push(...textBlocks, ...thingBlocks, ...toolUseBlocks) // Use the finish_reason from the first choice, or prioritize tool_calls if (choice.finish_reason === "tool_calls" || stopReason === "stop") { @@ -302,14 +295,12 @@ export function translateToAnthropic( } } - // 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: { @@ -342,6 +333,31 @@ function getAnthropicTextBlocks( return [] } +function getAnthropicThinkBlocks( + reasoningText: string | null | undefined, + reasoningOpaque: string | null | undefined, +): Array { + if (reasoningText) { + return [ + { + type: "thinking", + thinking: reasoningText, + signature: "", + }, + ] + } + if (reasoningOpaque) { + return [ + { + type: "thinking", + thinking: "", + signature: reasoningOpaque, + }, + ] + } + return [] +} + function getAnthropicToolUseBlocks( toolCalls: Array | undefined, ): Array { diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index 55094448..fc6715a9 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -1,4 +1,8 @@ -import { type ChatCompletionChunk } from "~/services/copilot/create-chat-completions" +import { + type ChatCompletionChunk, + type Choice, + type Delta, +} from "~/services/copilot/create-chat-completions" import { type AnthropicStreamEventData, @@ -16,7 +20,6 @@ function isToolBlockOpen(state: AnthropicStreamState): boolean { ) } -// eslint-disable-next-line max-lines-per-function, complexity export function translateChunkToAnthropicEvents( chunk: ChatCompletionChunk, state: AnthropicStreamState, @@ -30,22 +33,49 @@ export function translateChunkToAnthropicEvents( const choice = chunk.choices[0] const { delta } = choice - if (!state.messageStartSent) { - events.push({ - type: "message_start", - message: { - id: chunk.id, - type: "message", - role: "assistant", - content: [], - model: chunk.model, - stop_reason: null, - stop_sequence: null, + handleMessageStart(state, events, chunk) + + handleThinkingText(delta, state, events) + + handleContent(delta, state, events) + + handleToolCalls(delta, state, events) + + handleFinish(choice, state, { events, chunk }) + + return events +} + +function handleFinish( + choice: Choice, + state: AnthropicStreamState, + context: { + events: Array + chunk: ChatCompletionChunk + }, +) { + const { events, chunk } = context + if (choice.finish_reason && choice.finish_reason.length > 0) { + if (state.contentBlockOpen) { + context.events.push({ + type: "content_block_stop", + index: state.contentBlockIndex, + }) + state.contentBlockOpen = false + } + + events.push( + { + type: "message_delta", + delta: { + stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason), + stop_sequence: null, + }, usage: { input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0), - output_tokens: 0, // Will be updated in message_delta when finished + output_tokens: chunk.usage?.completion_tokens ?? 0, ...(chunk.usage?.prompt_tokens_details?.cached_tokens !== undefined && { cache_read_input_tokens: @@ -53,44 +83,21 @@ export function translateChunkToAnthropicEvents( }), }, }, - }) - state.messageStartSent = true - } - - if (delta.content) { - if (isToolBlockOpen(state)) { - // A tool block was open, so close it before starting a text block. - events.push({ - type: "content_block_stop", - index: state.contentBlockIndex, - }) - state.contentBlockIndex++ - state.contentBlockOpen = false - } - - if (!state.contentBlockOpen) { - events.push({ - type: "content_block_start", - index: state.contentBlockIndex, - content_block: { - type: "text", - text: "", - }, - }) - state.contentBlockOpen = true - } - - events.push({ - type: "content_block_delta", - index: state.contentBlockIndex, - delta: { - type: "text_delta", - text: delta.content, + { + type: "message_stop", }, - }) + ) } +} + +function handleToolCalls( + delta: Delta, + state: AnthropicStreamState, + events: Array, +) { + if (delta.tool_calls && delta.tool_calls.length > 0) { + closeThinkingBlockIfOpen(delta, state, events) - if (delta.tool_calls) { for (const toolCall of delta.tool_calls) { if (toolCall.id && toolCall.function?.name) { // New tool call starting. @@ -141,28 +148,70 @@ export function translateChunkToAnthropicEvents( } } } +} - if (choice.finish_reason) { - if (state.contentBlockOpen) { +function handleContent( + delta: Delta, + state: AnthropicStreamState, + events: Array, +) { + if (delta.content && delta.content.length > 0) { + closeThinkingBlockIfOpen(delta, state, events) + + if (isToolBlockOpen(state)) { + // A tool block was open, so close it before starting a text block. events.push({ type: "content_block_stop", index: state.contentBlockIndex, }) + state.contentBlockIndex++ state.contentBlockOpen = false } - events.push( - { - type: "message_delta", - delta: { - stop_reason: mapOpenAIStopReasonToAnthropic(choice.finish_reason), - stop_sequence: null, + if (!state.contentBlockOpen) { + events.push({ + type: "content_block_start", + index: state.contentBlockIndex, + content_block: { + type: "text", + text: "", }, + }) + state.contentBlockOpen = true + } + + events.push({ + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "text_delta", + text: delta.content, + }, + }) + } +} + +function handleMessageStart( + state: AnthropicStreamState, + events: Array, + chunk: ChatCompletionChunk, +) { + if (!state.messageStartSent) { + events.push({ + type: "message_start", + message: { + id: chunk.id, + type: "message", + role: "assistant", + content: [], + model: chunk.model, + stop_reason: null, + stop_sequence: null, usage: { input_tokens: (chunk.usage?.prompt_tokens ?? 0) - (chunk.usage?.prompt_tokens_details?.cached_tokens ?? 0), - output_tokens: chunk.usage?.completion_tokens ?? 0, + output_tokens: 0, // Will be updated in message_delta when finished ...(chunk.usage?.prompt_tokens_details?.cached_tokens !== undefined && { cache_read_input_tokens: @@ -170,13 +219,122 @@ export function translateChunkToAnthropicEvents( }), }, }, + }) + state.messageStartSent = true + } +} + +function handleReasoningOpaque( + delta: Delta, + events: Array, + state: AnthropicStreamState, +) { + if (delta.reasoning_opaque && delta.reasoning_opaque.length > 0) { + events.push( + { + type: "content_block_start", + index: state.contentBlockIndex, + content_block: { + type: "thinking", + thinking: "", + }, + }, { - type: "message_stop", + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "thinking_delta", + thinking: "", + }, + }, + { + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "signature_delta", + signature: delta.reasoning_opaque, + }, + }, + { + type: "content_block_stop", + index: state.contentBlockIndex, }, ) } +} - return events +function handleThinkingText( + delta: Delta, + state: AnthropicStreamState, + events: Array, +) { + if (delta.reasoning_text && delta.reasoning_text.length > 0) { + if (!state.thinkingBlockOpen) { + events.push({ + type: "content_block_start", + index: state.contentBlockIndex, + content_block: { + type: "thinking", + thinking: "", + }, + }) + state.thinkingBlockOpen = true + } + + events.push({ + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "thinking_delta", + thinking: delta.reasoning_text, + }, + }) + + if (delta.reasoning_opaque && delta.reasoning_opaque.length > 0) { + events.push( + { + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "signature_delta", + signature: delta.reasoning_opaque, + }, + }, + { + type: "content_block_stop", + index: state.contentBlockIndex, + }, + ) + state.contentBlockIndex++ + state.thinkingBlockOpen = false + } + } +} + +function closeThinkingBlockIfOpen( + delta: Delta, + state: AnthropicStreamState, + events: Array, +): void { + if (state.thinkingBlockOpen) { + events.push( + { + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "signature_delta", + signature: "", + }, + }, + { + type: "content_block_stop", + index: state.contentBlockIndex, + }, + ) + state.contentBlockIndex++ + state.thinkingBlockOpen = false + } + handleReasoningOpaque(delta, events, state) } export function translateErrorToAnthropicErrorEvent(): AnthropicStreamEventData { diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index 8534151d..e848b27a 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -69,7 +69,7 @@ export interface ChatCompletionChunk { } } -interface Delta { +export interface Delta { content?: string | null role?: "user" | "assistant" | "system" | "tool" tool_calls?: Array<{ @@ -81,9 +81,11 @@ interface Delta { arguments?: string } }> + reasoning_text?: string | null + reasoning_opaque?: string | null } -interface Choice { +export interface Choice { index: number delta: Delta finish_reason: "stop" | "length" | "tool_calls" | "content_filter" | null @@ -112,6 +114,8 @@ export interface ChatCompletionResponse { interface ResponseMessage { role: "assistant" content: string | null + reasoning_text?: string | null + reasoning_opaque?: string | null tool_calls?: Array } @@ -166,6 +170,8 @@ export interface Message { name?: string tool_calls?: Array tool_call_id?: string + reasoning_text?: string | null + reasoning_opaque?: string | null } export interface ToolCall { diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index 06c66377..eb1d9b25 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -150,7 +150,7 @@ describe("Anthropic to OpenAI translation logic", () => { const assistantMessage = openAIPayload.messages.find( (m) => m.role === "assistant", ) - expect(assistantMessage?.content).toContain( + expect(assistantMessage?.reasoning_text).toContain( "Let me think about this simple math problem...", ) expect(assistantMessage?.content).toContain("2+2 equals 4.") @@ -188,7 +188,7 @@ describe("Anthropic to OpenAI translation logic", () => { const assistantMessage = openAIPayload.messages.find( (m) => m.role === "assistant", ) - expect(assistantMessage?.content).toContain( + expect(assistantMessage?.reasoning_text).toContain( "I need to call the weather API", ) expect(assistantMessage?.content).toContain( diff --git a/tests/anthropic-response.test.ts b/tests/anthropic-response.test.ts index ecd71aac..e849a02a 100644 --- a/tests/anthropic-response.test.ts +++ b/tests/anthropic-response.test.ts @@ -252,6 +252,7 @@ describe("OpenAI to Anthropic Streaming Response Translation", () => { contentBlockIndex: 0, contentBlockOpen: false, toolCalls: {}, + thinkingBlockOpen: false, } const translatedStream = openAIStream.flatMap((chunk) => translateChunkToAnthropicEvents(chunk, streamState), @@ -352,6 +353,7 @@ describe("OpenAI to Anthropic Streaming Response Translation", () => { contentBlockIndex: 0, contentBlockOpen: false, toolCalls: {}, + thinkingBlockOpen: false, } const translatedStream = openAIStream.flatMap((chunk) => translateChunkToAnthropicEvents(chunk, streamState), From a2467d32e63c979af7e5373ab0868e52c98401fc Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Wed, 19 Nov 2025 21:39:07 +0800 Subject: [PATCH 02/16] feat: add signature field to AnthropicThinkingBlock --- src/routes/messages/anthropic-types.ts | 1 + tests/anthropic-request.test.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/routes/messages/anthropic-types.ts b/src/routes/messages/anthropic-types.ts index 03f24d10..2fb7849f 100644 --- a/src/routes/messages/anthropic-types.ts +++ b/src/routes/messages/anthropic-types.ts @@ -56,6 +56,7 @@ export interface AnthropicToolUseBlock { export interface AnthropicThinkingBlock { type: "thinking" thinking: string + signature: string } export type AnthropicUserContentBlock = diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index eb1d9b25..baed2f6d 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -136,6 +136,7 @@ describe("Anthropic to OpenAI translation logic", () => { { type: "thinking", thinking: "Let me think about this simple math problem...", + signature: "abc123", }, { type: "text", text: "2+2 equals 4." }, ], @@ -168,6 +169,7 @@ describe("Anthropic to OpenAI translation logic", () => { type: "thinking", thinking: "I need to call the weather API to get current weather information.", + signature: "def456", }, { type: "text", text: "I'll check the weather for you." }, { From 58f7a45c6c43e1a883661c65a372867db0516b37 Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Wed, 19 Nov 2025 21:46:20 +0800 Subject: [PATCH 03/16] feat: add idleTimeout configuration for bun server --- src/start.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/start.ts b/src/start.ts index 14abbbdf..171d4ac9 100644 --- a/src/start.ts +++ b/src/start.ts @@ -117,6 +117,9 @@ export async function runServer(options: RunServerOptions): Promise { serve({ fetch: server.fetch as ServerHandler, port: options.port, + bun: { + idleTimeout: 0, + }, }) } From 3fa55199c176ce33e7db60fa5d0f11eb14abd386 Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Wed, 19 Nov 2025 21:49:58 +0800 Subject: [PATCH 04/16] feat: enhance reasoning handling in tool calls and change the thinking order when stream=false and exclude reasoning_opaque from token calculation in calculateMessageTokens --- src/lib/tokenizer.ts | 3 +++ src/routes/messages/non-stream-translation.ts | 10 +++---- src/routes/messages/stream-translation.ts | 27 ++++++++++++++++--- 3 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/lib/tokenizer.ts b/src/lib/tokenizer.ts index 8c3eda73..b9ebafad 100644 --- a/src/lib/tokenizer.ts +++ b/src/lib/tokenizer.ts @@ -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 } diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 3b1a491d..94a0f7e1 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -287,7 +287,7 @@ export function translateToAnthropic( ) const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls) - assistantContentBlocks.push(...textBlocks, ...thingBlocks, ...toolUseBlocks) + assistantContentBlocks.push(...thingBlocks, ...textBlocks, ...toolUseBlocks) // Use the finish_reason from the first choice, or prioritize tool_calls if (choice.finish_reason === "tool_calls" || stopReason === "stop") { @@ -320,7 +320,7 @@ export function translateToAnthropic( function getAnthropicTextBlocks( messageContent: Message["content"], ): Array { - if (typeof messageContent === "string") { + if (typeof messageContent === "string" && messageContent.length > 0) { return [{ type: "text", text: messageContent }] } @@ -337,16 +337,16 @@ function getAnthropicThinkBlocks( reasoningText: string | null | undefined, reasoningOpaque: string | null | undefined, ): Array { - if (reasoningText) { + if (reasoningText && reasoningText.length > 0) { return [ { type: "thinking", thinking: reasoningText, - signature: "", + signature: reasoningOpaque || "", }, ] } - if (reasoningOpaque) { + if (reasoningOpaque && reasoningOpaque.length > 0) { return [ { type: "thinking", diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index fc6715a9..9dc2dd52 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -62,6 +62,8 @@ function handleFinish( index: state.contentBlockIndex, }) state.contentBlockOpen = false + state.contentBlockIndex++ + handleReasoningOpaque(choice.delta, events, state) } events.push( @@ -96,7 +98,9 @@ function handleToolCalls( events: Array, ) { if (delta.tool_calls && delta.tool_calls.length > 0) { - closeThinkingBlockIfOpen(delta, state, events) + closeThinkingBlockIfOpen(state, events) + + handleReasoningOpaqueInToolCalls(state, events, delta) for (const toolCall of delta.tool_calls) { if (toolCall.id && toolCall.function?.name) { @@ -150,13 +154,29 @@ function handleToolCalls( } } +function handleReasoningOpaqueInToolCalls( + state: AnthropicStreamState, + events: Array, + delta: Delta, +) { + if (state.contentBlockOpen) { + events.push({ + type: "content_block_stop", + index: state.contentBlockIndex, + }) + state.contentBlockIndex++ + state.contentBlockOpen = false + } + handleReasoningOpaque(delta, events, state) +} + function handleContent( delta: Delta, state: AnthropicStreamState, events: Array, ) { if (delta.content && delta.content.length > 0) { - closeThinkingBlockIfOpen(delta, state, events) + closeThinkingBlockIfOpen(state, events) if (isToolBlockOpen(state)) { // A tool block was open, so close it before starting a text block. @@ -260,6 +280,7 @@ function handleReasoningOpaque( index: state.contentBlockIndex, }, ) + state.contentBlockIndex++ } } @@ -312,7 +333,6 @@ function handleThinkingText( } function closeThinkingBlockIfOpen( - delta: Delta, state: AnthropicStreamState, events: Array, ): void { @@ -334,7 +354,6 @@ function closeThinkingBlockIfOpen( state.contentBlockIndex++ state.thinkingBlockOpen = false } - handleReasoningOpaque(delta, events, state) } export function translateErrorToAnthropicErrorEvent(): AnthropicStreamEventData { From dfb40d2625a46872ecd4aca99a00dc0ec17b479a Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Thu, 20 Nov 2025 07:41:31 +0800 Subject: [PATCH 05/16] feat: conditionally handle reasoningOpaque in handleFinish based on tool block state --- src/routes/messages/stream-translation.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index 9dc2dd52..44b69bf3 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -57,13 +57,16 @@ function handleFinish( const { events, chunk } = context if (choice.finish_reason && choice.finish_reason.length > 0) { if (state.contentBlockOpen) { + const toolBlockOpen = isToolBlockOpen(state) context.events.push({ type: "content_block_stop", index: state.contentBlockIndex, }) state.contentBlockOpen = false state.contentBlockIndex++ - handleReasoningOpaque(choice.delta, events, state) + if (!toolBlockOpen) { + handleReasoningOpaque(choice.delta, events, state) + } } events.push( From 7657d872e2e31c98ca3baa032cce20c9b720086c Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Thu, 20 Nov 2025 11:02:08 +0800 Subject: [PATCH 06/16] fix: handleReasoningOpaqueInToolCalls add isToolBlockOpen judge --- src/routes/messages/stream-translation.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index 44b69bf3..6002d510 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -162,7 +162,7 @@ function handleReasoningOpaqueInToolCalls( events: Array, delta: Delta, ) { - if (state.contentBlockOpen) { + if (state.contentBlockOpen && !isToolBlockOpen(state)) { events.push({ type: "content_block_stop", index: state.contentBlockIndex, From 7f8187b63bd9509e1b54e17c1d4d13fd9377ad13 Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Wed, 3 Dec 2025 14:46:08 +0800 Subject: [PATCH 07/16] feat: support claude model thinking block --- src/routes/messages/non-stream-translation.ts | 53 ++++++++++++++++--- src/routes/messages/stream-translation.ts | 43 ++++++++------- .../copilot/create-chat-completions.ts | 1 + src/services/copilot/get-models.ts | 2 + 4 files changed, 74 insertions(+), 25 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 94a0f7e1..29be0d7f 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -1,3 +1,6 @@ +import type { Model } from "~/services/copilot/get-models" + +import { state } from "~/lib/state" import { type ChatCompletionResponse, type ChatCompletionsPayload, @@ -29,11 +32,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, + modelId, ), max_tokens: payload.max_tokens, stop: payload.stop_sequences, @@ -43,14 +50,32 @@ 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, + ) + if (maxThinkingBudget > 0 && thinking.budget_tokens !== undefined) { + return Math.min(thinking.budget_tokens, maxThinkingBudget) + } + } + return undefined +} + 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") } return model @@ -59,13 +84,14 @@ function translateModelName(model: string): string { function translateAnthropicMessagesToOpenAI( anthropicMessages: Array, system: string | Array | undefined, + modelId: string, ): Array { const systemMessages = handleSystemPrompt(system) const otherMessages = anthropicMessages.flatMap((message) => message.role === "user" ? handleUserMessage(message) - : handleAssistantMessage(message), + : handleAssistantMessage(message, modelId), ) return [...systemMessages, ...otherMessages] @@ -125,6 +151,7 @@ function handleUserMessage(message: AnthropicUserMessage): Array { function handleAssistantMessage( message: AnthropicAssistantMessage, + modelId: string, ): Array { if (!Array.isArray(message.content)) { return [ @@ -139,14 +166,28 @@ function handleAssistantMessage( (block): block is AnthropicToolUseBlock => block.type === "tool_use", ) - const thinkingBlocks = message.content.filter( + let thinkingBlocks = message.content.filter( (block): block is AnthropicThinkingBlock => block.type === "thinking", ) - const allThinkingContent = thinkingBlocks + 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) - .join("\n\n") + + const allThinkingContent = + thinkingContents.length > 0 ? thinkingContents.join("\n\n") : undefined const signature = thinkingBlocks.find( (b) => b.signature && b.signature.length > 0, diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index 6002d510..b492d10f 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -212,6 +212,30 @@ function handleContent( }, }) } + + // handle for claude model + if ( + delta.content === "" + && delta.reasoning_opaque + && delta.reasoning_opaque.length > 0 + ) { + events.push( + { + type: "content_block_delta", + index: state.contentBlockIndex, + delta: { + type: "signature_delta", + signature: delta.reasoning_opaque, + }, + }, + { + type: "content_block_stop", + index: state.contentBlockIndex, + }, + ) + state.contentBlockIndex++ + state.thinkingBlockOpen = false + } } function handleMessageStart( @@ -313,25 +337,6 @@ function handleThinkingText( thinking: delta.reasoning_text, }, }) - - if (delta.reasoning_opaque && delta.reasoning_opaque.length > 0) { - events.push( - { - type: "content_block_delta", - index: state.contentBlockIndex, - delta: { - type: "signature_delta", - signature: delta.reasoning_opaque, - }, - }, - { - type: "content_block_stop", - index: state.contentBlockIndex, - }, - ) - state.contentBlockIndex++ - state.thinkingBlockOpen = false - } } } diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index e848b27a..2713a80f 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -152,6 +152,7 @@ export interface ChatCompletionsPayload { | { type: "function"; function: { name: string } } | null user?: string | null + thinking_budget?: number } export interface Tool { diff --git a/src/services/copilot/get-models.ts b/src/services/copilot/get-models.ts index 3cfa30af..a7fffe93 100644 --- a/src/services/copilot/get-models.ts +++ b/src/services/copilot/get-models.ts @@ -25,6 +25,8 @@ interface ModelLimits { } interface ModelSupports { + max_thinking_budget?: number + min_thinking_budget?: number tool_calls?: boolean parallel_tool_calls?: boolean dimensions?: boolean From cbe12eb850bd4e65f36c885d8ea68e07789ce522 Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Wed, 10 Dec 2025 22:53:10 +0800 Subject: [PATCH 08/16] feat: enhance thinking budget calculation and rename variables for clarity --- src/routes/messages/non-stream-translation.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 29be0d7f..e5a59a10 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -65,7 +65,11 @@ function getThinkingBudget( (model.capabilities.limits.max_output_tokens ?? 0) - 1, ) if (maxThinkingBudget > 0 && thinking.budget_tokens !== undefined) { - return Math.min(thinking.budget_tokens, maxThinkingBudget) + const budgetTokens = Math.min(thinking.budget_tokens, maxThinkingBudget) + return Math.max( + budgetTokens, + model.capabilities.supports.min_thinking_budget ?? 1024, + ) } } return undefined @@ -322,13 +326,13 @@ export function translateToAnthropic( // Process all choices to extract text and tool use blocks for (const choice of response.choices) { const textBlocks = getAnthropicTextBlocks(choice.message.content) - const thingBlocks = getAnthropicThinkBlocks( + const thinkBlocks = getAnthropicThinkBlocks( choice.message.reasoning_text, choice.message.reasoning_opaque, ) const toolUseBlocks = getAnthropicToolUseBlocks(choice.message.tool_calls) - assistantContentBlocks.push(...thingBlocks, ...textBlocks, ...toolUseBlocks) + assistantContentBlocks.push(...thinkBlocks, ...textBlocks, ...toolUseBlocks) // Use the finish_reason from the first choice, or prioritize tool_calls if (choice.finish_reason === "tool_calls" || stopReason === "stop") { From ebcacb20c5c287972879149595535c50c5d6d0b8 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Thu, 11 Dec 2025 10:12:41 +0800 Subject: [PATCH 09/16] feat: update Copilot version and API version in api-config; adjust fallback VSCode version --- src/lib/api-config.ts | 4 ++-- src/services/get-vscode-version.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 83bce92a..2006c57c 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -7,11 +7,11 @@ export const standardHeaders = () => ({ accept: "application/json", }) -const COPILOT_VERSION = "0.26.7" +const COPILOT_VERSION = "0.33.5" 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" ? diff --git a/src/services/get-vscode-version.ts b/src/services/get-vscode-version.ts index 6078f09b..5e3cef79 100644 --- a/src/services/get-vscode-version.ts +++ b/src/services/get-vscode-version.ts @@ -1,4 +1,4 @@ -const FALLBACK = "1.104.3" +const FALLBACK = "1.106.3" export async function getVSCodeVersion() { const controller = new AbortController() From 0d6f7aa99af92a748c4109d1e669442f672cc458 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Thu, 11 Dec 2025 13:21:38 +0800 Subject: [PATCH 10/16] feat: update Copilot version to 0.35.0 and fallback VSCode version to 1.107.0 --- src/lib/api-config.ts | 2 +- src/services/get-vscode-version.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 2006c57c..7124930e 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -7,7 +7,7 @@ export const standardHeaders = () => ({ accept: "application/json", }) -const COPILOT_VERSION = "0.33.5" +const COPILOT_VERSION = "0.35.0" const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}` const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}` diff --git a/src/services/get-vscode-version.ts b/src/services/get-vscode-version.ts index 5e3cef79..89c39dc0 100644 --- a/src/services/get-vscode-version.ts +++ b/src/services/get-vscode-version.ts @@ -1,4 +1,4 @@ -const FALLBACK = "1.106.3" +const FALLBACK = "1.107.0" export async function getVSCodeVersion() { const controller = new AbortController() From dcafbe1b434e5526f49fd49876896c6026f70d5e Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Sat, 13 Dec 2025 09:22:54 +0800 Subject: [PATCH 11/16] fix: simplify copilotBaseUrl logic and correct openai-intent header value --- src/lib/api-config.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 7124930e..5b2c611f 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -14,9 +14,8 @@ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}` 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` + export const copilotHeaders = (state: State, vision: boolean = false) => { const headers: Record = { Authorization: `Bearer ${state.copilotToken}`, @@ -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", From 51752450381c9b101544e09877000e5b8b5c58d0 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Wed, 31 Dec 2025 12:54:31 +0800 Subject: [PATCH 12/16] feat: interleaved thinking support --- src/routes/messages/non-stream-translation.ts | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index e5a59a10..b0795d69 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -14,7 +14,6 @@ import { import { type AnthropicAssistantContentBlock, type AnthropicAssistantMessage, - type AnthropicMessage, type AnthropicMessagesPayload, type AnthropicResponse, type AnthropicTextBlock, @@ -38,9 +37,9 @@ export function translateToOpenAI( return { model: modelId, messages: translateAnthropicMessagesToOpenAI( - payload.messages, - payload.system, + payload, modelId, + thinkingBudget, ), max_tokens: payload.max_tokens, stop: payload.stop_sequences, @@ -86,32 +85,53 @@ function translateModelName(model: string): string { } function translateAnthropicMessagesToOpenAI( - anthropicMessages: Array, - system: string | Array | undefined, + payload: AnthropicMessagesPayload, modelId: string, + thinkingBudget: number | undefined, ): Array { - const systemMessages = handleSystemPrompt(system) - - const otherMessages = anthropicMessages.flatMap((message) => + const systemMessages = handleSystemPrompt(payload.system, modelId) + const otherMessages = payload.messages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, modelId), ) - + if (modelId.startsWith("claude") && thinkingBudget) { + const thinkingMessage = { + role: "user", + content: "Please strictly follow Interleaved thinking", + } as Message + return [...systemMessages, thinkingMessage, ...otherMessages] + } return [...systemMessages, ...otherMessages] } function handleSystemPrompt( system: string | Array | undefined, + modelId: string, ): Array { if (!system) { return [] } + let extraPrompt = ` + ## Interleaved thinking + - Interleaved thinking is enabled + - You MUST think after receiving tool results before deciding the next action or final answer. + ` + if (!modelId.startsWith("claude")) { + extraPrompt = "" + } 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 }] } } From dd80c8d9ddf75afa797ced2d2a56ecbee409be62 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Wed, 31 Dec 2025 14:39:57 +0800 Subject: [PATCH 13/16] feat: enhance system prompt handling for interleaved thinking with thinking budget integration --- src/routes/messages/non-stream-translation.ts | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index b0795d69..57edfad2 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -89,7 +89,11 @@ function translateAnthropicMessagesToOpenAI( modelId: string, thinkingBudget: number | undefined, ): Array { - const systemMessages = handleSystemPrompt(payload.system, modelId) + const systemMessages = handleSystemPrompt( + payload.system, + modelId, + thinkingBudget, + ) const otherMessages = payload.messages.flatMap((message) => message.role === "user" ? handleUserMessage(message) @@ -98,7 +102,8 @@ function translateAnthropicMessagesToOpenAI( if (modelId.startsWith("claude") && thinkingBudget) { const thinkingMessage = { role: "user", - content: "Please strictly follow Interleaved thinking", + content: + "Please strictly follow Interleaved thinking", } as Message return [...systemMessages, thinkingMessage, ...otherMessages] } @@ -108,19 +113,21 @@ function translateAnthropicMessagesToOpenAI( function handleSystemPrompt( system: string | Array | undefined, modelId: string, + thinkingBudget: number | undefined, ): Array { if (!system) { return [] } - let extraPrompt = ` - ## Interleaved thinking - - Interleaved thinking is enabled - - You MUST think after receiving tool results before deciding the next action or final answer. - ` - if (!modelId.startsWith("claude")) { - extraPrompt = "" + 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. +` } + if (typeof system === "string") { return [{ role: "system", content: system + extraPrompt }] } else { From e45c6db3dfb54194986926ac74439bde97033250 Mon Sep 17 00:00:00 2001 From: caozhiyuan <568022847@qq.com> Date: Fri, 2 Jan 2026 22:21:35 +0800 Subject: [PATCH 14/16] feat: compatible with copilot API returning content->reasoning_text->reasoning_opaque in different deltas --- src/routes/messages/stream-translation.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index b492d10f..c62ce3ab 100644 --- a/src/routes/messages/stream-translation.ts +++ b/src/routes/messages/stream-translation.ts @@ -218,6 +218,7 @@ function handleContent( delta.content === "" && delta.reasoning_opaque && delta.reasoning_opaque.length > 0 + && state.thinkingBlockOpen ) { events.push( { @@ -317,6 +318,15 @@ function handleThinkingText( events: Array, ) { if (delta.reasoning_text && delta.reasoning_text.length > 0) { + // compatible with copilot API returning content->reasoning_text->reasoning_opaque in different deltas + // this is an extremely abnormal situation, probably a server-side bug + // only occurs in the claude model, with a very low probability of occurrence + if (state.contentBlockOpen) { + delta.content = delta.reasoning_text + delta.reasoning_text = undefined + return + } + if (!state.thinkingBlockOpen) { events.push({ type: "content_block_start", From 0afccfab6f2ff1cf67348dadaa4b49b6a8d6f204 Mon Sep 17 00:00:00 2001 From: Hyunggyu Jang Date: Thu, 1 Jan 2026 11:38:40 +0900 Subject: [PATCH 15/16] fix(api-config): use default API URL when account type is individual When account type is not specified or set to 'individual', use the default api.githubcopilot.com URL instead of constructing a subdomain-based URL. This restores previous behavior where business users could work without explicitly specifying their account type, as the default URL works for both individual and business accounts. Only constructs account-type-specific URLs (api.business.githubcopilot.com, api.enterprise.githubcopilot.com) when those account types are explicitly specified. --- src/lib/api-config.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 5b2c611f..2c808455 100644 --- a/src/lib/api-config.ts +++ b/src/lib/api-config.ts @@ -14,8 +14,9 @@ const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}` const API_VERSION = "2025-10-01" export const copilotBaseUrl = (state: State) => - `https://api.${state.accountType}.githubcopilot.com` - + state.accountType === "individual" ? + "https://api.githubcopilot.com" + : `https://api.${state.accountType}.githubcopilot.com` export const copilotHeaders = (state: State, vision: boolean = false) => { const headers: Record = { Authorization: `Bearer ${state.copilotToken}`, From 67b357a5e5a859804260bf7570e9313c1d620903 Mon Sep 17 00:00:00 2001 From: "Jeffrey.Cao" Date: Sun, 4 Jan 2026 18:35:54 +0800 Subject: [PATCH 16/16] feat: enforce interleaved thinking protocol in message handling --- src/routes/messages/non-stream-translation.ts | 34 +++++++++++++------ 1 file changed, 24 insertions(+), 10 deletions(-) diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index 57edfad2..58644850 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -100,12 +100,20 @@ function translateAnthropicMessagesToOpenAI( : handleAssistantMessage(message, modelId), ) if (modelId.startsWith("claude") && thinkingBudget) { - const thinkingMessage = { - role: "user", - content: - "Please strictly follow Interleaved thinking", - } as Message - return [...systemMessages, thinkingMessage, ...otherMessages] + const reminder = + "you MUST follow interleaved_thinking_protocol" + const firstUserIndex = otherMessages.findIndex((m) => m.role === "user") + if (firstUserIndex !== -1) { + const userMessage = otherMessages[firstUserIndex] + if (typeof userMessage.content === "string") { + userMessage.content = reminder + "\n\n" + userMessage.content + } else if (Array.isArray(userMessage.content)) { + userMessage.content = [ + { type: "text", text: reminder }, + ...userMessage.content, + ] as Array + } + } } return [...systemMessages, ...otherMessages] } @@ -122,10 +130,16 @@ function handleSystemPrompt( 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. -` + +ABSOLUTE REQUIREMENT - NON-NEGOTIABLE: +The current thinking_mode is interleaved, Whenever you have the result of a function call, think carefully , MUST output a thinking block +RULES: +Tool result → thinking block (ALWAYS, no exceptions) +This is NOT optional - it is a hard requirement +The thinking block must contain substantive reasoning (minimum 3-5 sentences) +Think about: what the results mean, what to do next, how to answer the user +NEVER skip this step, even if the result seems simple or obvious +` } if (typeof system === "string") {