diff --git a/.env b/.env index e637682be..a135ab0ed 100644 --- a/.env +++ b/.env @@ -11,7 +11,4 @@ TRACE_OUTPUT_FOLDER=./traces IDLE_TIMEOUT=255 # Model mappings to redirect requests from one model to another -# Format: "source1:target1,source2:target2" -# Example: "claude-sonnet-4-20250514:claude-opus-4,gpt-4:gpt-4-turbo" -# MODEL_MAPPINGS="claude-sonnet-4-5-20250929:claude-sonnet-4.5,claude-opus-4-5-20251101:claude-opus-4.5,claude-haiku-4-5-20251001:claude-haiku-4.5" -MODEL_MAPPINGS="claude-opus-4-6:claude-opus-4.6,claude-sonnet-4-5:claude-sonnet-4.5,claude-opus-4-5:claude-opus-4.5,claude-haiku-4-5:claude-haiku-4.5" +MODEL_MAPPINGS="claude-opus-4-8:claude-opus-4.8,claude-opus-4-7:claude-opus-4.7-1m-internal,claude-sonnet-4-6:claude-sonnet-4.6,claude-haiku-4-5:claude-haiku-4.5" diff --git a/settings.json b/settings.json index 90974fe69..ea0bf8376 100644 --- a/settings.json +++ b/settings.json @@ -2,19 +2,26 @@ "env": { "ANTHROPIC_BASE_URL": "http://localhost:4141", "ANTHROPIC_AUTH_TOKEN": "dummy", - "ANTHROPIC_MODEL": "claude-opus-4-6", - "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-5", + "ANTHROPIC_MODEL": "claude-opus-4-8", + "ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-4-6", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4-5", - "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-6", - "CLAUDE_CODE_SUBAGENT_MODEL": "claude-opus-4-6", + "ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-4-8", + "CLAUDE_CODE_SUBAGENT_MODEL": "claude-opus-4-8", "CLAUDE_CODE_ATTRIBUTION_HEADER": "0", - "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD": "1" + "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD": "1", + "CLAUDE_CODE_USE_POWERSHELL_TOOL": "1" }, "permissions": { - "deny": [ - "WebSearch" - ] + "defaultMode": "bypassPermissions" }, - "model": "claude-opus-4-6", - "autoUpdatesChannel": "latest" -} \ No newline at end of file + "statusLine": { + "type": "command", + "command": "npx -y ccstatusline@latest", + "padding": 0, + "refreshInterval": 10 + }, + "autoUpdatesChannel": "latest", + "tui": "fullscreen", + "skipDangerousModePermissionPrompt": true, + "preferredNotifChannel": "kitty" +} diff --git a/src/routes/messages/anthropic-types.ts b/src/routes/messages/anthropic-types.ts index 881fffcc8..427e701b7 100644 --- a/src/routes/messages/anthropic-types.ts +++ b/src/routes/messages/anthropic-types.ts @@ -19,7 +19,7 @@ export interface AnthropicMessagesPayload { name?: string } thinking?: { - type: "enabled" + type: "disabled" | "enabled" | "adaptive" budget_tokens?: number } service_tier?: "auto" | "standard_only" diff --git a/src/routes/messages/non-stream-translation.ts b/src/routes/messages/non-stream-translation.ts index dc41e6382..f64097361 100644 --- a/src/routes/messages/non-stream-translation.ts +++ b/src/routes/messages/non-stream-translation.ts @@ -43,6 +43,7 @@ export function translateToOpenAI( user: payload.metadata?.user_id, tools: translateAnthropicToolsToOpenAI(payload.tools), tool_choice: translateAnthropicToolChoiceToOpenAI(payload.tool_choice), + thinking: translateAnthropicThinkingToOpenAI(payload.thinking), } } @@ -276,6 +277,19 @@ function translateAnthropicToolChoiceToOpenAI( } } +function translateAnthropicThinkingToOpenAI( + thinking: AnthropicMessagesPayload["thinking"], +): ChatCompletionsPayload["thinking"] { + if (!thinking) { + return undefined + } + + return { + ...thinking, + type: thinking.type === "adaptive" ? "enabled" : thinking.type, + } +} + // Response translation export function translateToAnthropic( diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts index 8534151da..ccf26c16b 100644 --- a/src/services/copilot/create-chat-completions.ts +++ b/src/services/copilot/create-chat-completions.ts @@ -10,7 +10,9 @@ export const createChatCompletions = async ( ) => { if (!state.copilotToken) throw new Error("Copilot token not found") - const enableVision = payload.messages.some( + const requestPayload = normalizeChatCompletionsPayload(payload) + + const enableVision = requestPayload.messages.some( (x) => typeof x.content !== "string" && x.content?.some((x) => x.type === "image_url"), @@ -18,7 +20,7 @@ export const createChatCompletions = async ( // Agent/user check for X-Initiator header // Determine if any message is from an agent ("assistant" or "tool") - const isAgentCall = payload.messages.some((msg) => + const isAgentCall = requestPayload.messages.some((msg) => ["assistant", "tool"].includes(msg.role), ) @@ -31,7 +33,7 @@ export const createChatCompletions = async ( const response = await fetch(`${copilotBaseUrl(state)}/chat/completions`, { method: "POST", headers, - body: JSON.stringify(payload), + body: JSON.stringify(requestPayload), }) if (!response.ok) { @@ -39,13 +41,29 @@ export const createChatCompletions = async ( throw new HTTPError("Failed to create chat completions", response) } - if (payload.stream) { + if (requestPayload.stream) { return events(response) } return (await response.json()) as ChatCompletionResponse } +function normalizeChatCompletionsPayload( + payload: ChatCompletionsPayload, +): ChatCompletionsPayload { + if (payload.thinking?.type !== "adaptive") { + return payload + } + + return { + ...payload, + thinking: { + ...payload.thinking, + type: "enabled", + }, + } +} + // Streaming types export interface ChatCompletionChunk { @@ -148,6 +166,12 @@ export interface ChatCompletionsPayload { | { type: "function"; function: { name: string } } | null user?: string | null + thinking?: ThinkingConfig | null +} + +interface ThinkingConfig { + type: "disabled" | "enabled" | "adaptive" | (string & {}) + budget_tokens?: number } export interface Tool { diff --git a/tests/anthropic-request.test.ts b/tests/anthropic-request.test.ts index 06c663778..8f841c820 100644 --- a/tests/anthropic-request.test.ts +++ b/tests/anthropic-request.test.ts @@ -197,6 +197,19 @@ describe("Anthropic to OpenAI translation logic", () => { expect(assistantMessage?.tool_calls).toHaveLength(1) expect(assistantMessage?.tool_calls?.[0].function.name).toBe("get_weather") }) + + test("should normalize adaptive thinking to enabled", () => { + const anthropicPayload: AnthropicMessagesPayload = { + model: "claude-sonnet-4", + messages: [{ role: "user", content: "Think this through." }], + max_tokens: 100, + thinking: { type: "adaptive" }, + } + + const openAIPayload = translateToOpenAI(anthropicPayload) + + expect(openAIPayload.thinking).toEqual({ type: "enabled" }) + }) }) describe("OpenAI Chat Completion v1 Request Payload Validation with Zod", () => { diff --git a/tests/create-chat-completions.test.ts b/tests/create-chat-completions.test.ts index d18e741aa..d67f39f98 100644 --- a/tests/create-chat-completions.test.ts +++ b/tests/create-chat-completions.test.ts @@ -12,7 +12,7 @@ state.accountType = "individual" // Helper to mock fetch const fetchMock = mock( - (_url: string, opts: { headers: Record }) => { + (_url: string, opts: { body?: string; headers: Record }) => { return { ok: true, json: () => ({ id: "123", object: "chat.completion", choices: [] }), @@ -23,6 +23,14 @@ const fetchMock = mock( // @ts-expect-error - Mock fetch doesn't implement all fetch properties ;(globalThis as unknown as { fetch: typeof fetch }).fetch = fetchMock +function getLastFetchOptions() { + const lastCall = fetchMock.mock.calls.at(-1) + if (!lastCall) { + throw new Error("fetch was not called") + } + return lastCall[1] as { body?: string; headers: Record } +} + test("sets X-Initiator to agent if tool/assistant present", async () => { const payload: ChatCompletionsPayload = { messages: [ @@ -33,9 +41,7 @@ test("sets X-Initiator to agent if tool/assistant present", async () => { } await createChatCompletions(payload) expect(fetchMock).toHaveBeenCalled() - const headers = ( - fetchMock.mock.calls[0][1] as { headers: Record } - ).headers + const { headers } = getLastFetchOptions() expect(headers["X-Initiator"]).toBe("agent") }) @@ -49,8 +55,20 @@ test("sets X-Initiator to user if only user present", async () => { } await createChatCompletions(payload) expect(fetchMock).toHaveBeenCalled() - const headers = ( - fetchMock.mock.calls[1][1] as { headers: Record } - ).headers + const { headers } = getLastFetchOptions() expect(headers["X-Initiator"]).toBe("user") }) + +test("normalizes adaptive thinking before forwarding to Copilot", async () => { + const payload: ChatCompletionsPayload = { + messages: [{ role: "user", content: "hi" }], + model: "claude-sonnet-4", + thinking: { type: "adaptive" }, + } + + await createChatCompletions(payload) + + const { body } = getLastFetchOptions() + const forwardedPayload = JSON.parse(body ?? "{}") as ChatCompletionsPayload + expect(forwardedPayload.thinking).toEqual({ type: "enabled" }) +})