diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts index 0c3022957..2c8084559 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}`, @@ -24,7 +25,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", diff --git a/src/lib/config.ts b/src/lib/config.ts index dff63eb5c..e44953852 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -10,6 +10,7 @@ export interface AppConfig { string, "none" | "minimal" | "low" | "medium" | "high" | "xhigh" > + useFunctionApplyPatch?: boolean } const gpt5ExplorationPrompt = `## Exploration and reading files @@ -28,6 +29,7 @@ const defaultConfig: AppConfig = { modelReasoningEfforts: { "gpt-5-mini": "low", }, + useFunctionApplyPatch: true, } let cachedConfig: AppConfig | null = null diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index e5a59a10e..586448503 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,74 @@ 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, + thinkingBudget, + ) + const otherMessages = payload.messages.flatMap((message) => message.role === "user" ? handleUserMessage(message) : handleAssistantMessage(message, modelId), ) - + if (modelId.startsWith("claude") && thinkingBudget) { + 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] } function handleSystemPrompt( system: string | Array | undefined, + modelId: string, + thinkingBudget: number | undefined, ): Array { if (!system) { return [] } + let extraPrompt = "" + if (modelId.startsWith("claude") && thinkingBudget) { + extraPrompt = ` + +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") { - 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 }] } } diff --git a/src/routes/messages/stream-translation.ts b/src/routes/messages/stream-translation.ts index b492d10fc..c62ce3ab4 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", diff --git a/src/routes/responses/handler.ts b/src/routes/responses/handler.ts index 574d61fcf..14a841ac4 100644 --- a/src/routes/responses/handler.ts +++ b/src/routes/responses/handler.ts @@ -3,6 +3,7 @@ import type { Context } from "hono" import { streamSSE } from "hono/streaming" import { awaitApproval } from "~/lib/approval" +import { getConfig } from "~/lib/config" import { createHandlerLogger } from "~/lib/logger" import { checkRateLimit } from "~/lib/rate-limit" import { state } from "~/lib/state" @@ -24,6 +25,8 @@ export const handleResponses = async (c: Context) => { const payload = await c.req.json() logger.debug("Responses request payload:", JSON.stringify(payload)) + useFunctionApplyPatch(payload) + const selectedModel = state.models?.data.find( (model) => model.id === payload.model, ) @@ -78,3 +81,35 @@ const isAsyncIterable = (value: unknown): value is AsyncIterable => const isStreamingRequested = (payload: ResponsesPayload): boolean => Boolean(payload.stream) + +const useFunctionApplyPatch = (payload: ResponsesPayload): void => { + const config = getConfig() + const useFunctionApplyPatch = config.useFunctionApplyPatch ?? true + if (useFunctionApplyPatch) { + logger.debug("Using function tool apply_patch for responses") + if (Array.isArray(payload.tools)) { + const toolsArr = payload.tools + for (let i = 0; i < toolsArr.length; i++) { + const t = toolsArr[i] + if (t.type === "custom" && t.name === "apply_patch") { + toolsArr[i] = { + type: "function", + name: t.name, + description: "Use the `apply_patch` tool to edit files", + parameters: { + type: "object", + properties: { + input: { + type: "string", + description: "The entire contents of the apply_patch command", + }, + }, + required: ["input"], + }, + strict: false, + } + } + } + } + } +} diff --git a/src/services/copilot/create-responses.ts b/src/services/copilot/create-responses.ts index bc24ce544..9982a4d98 100644 --- a/src/services/copilot/create-responses.ts +++ b/src/services/copilot/create-responses.ts @@ -33,7 +33,7 @@ export interface ToolChoiceFunction { type: "function" } -export type Tool = FunctionTool +export type Tool = FunctionTool | Record export interface FunctionTool { name: string