From da764c4c2535a1897bca9cba900d9db9230eec8e Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 2 Jul 2026 16:10:16 +0000 Subject: [PATCH 1/2] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/xlabtg/teleton-agent/issues/685 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 00000000..05820a6d --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-07-02T16:10:15.961Z for PR creation at branch issue-685-7823dcb3516f for issue https://github.com/xlabtg/teleton-agent/issues/685 \ No newline at end of file From c66c0a3848a594dc342a393ff5adf4560ae46b23 Mon Sep 17 00:00:00 2001 From: konard Date: Thu, 2 Jul 2026 16:27:49 +0000 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BC=D0=BE=D0=B4=D0=B5=D0=BB=D1=8C=20Anthropic?= =?UTF-8?q?=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB=D1=87=D0=B0=D0=BD?= =?UTF-8?q?=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitkeep | 1 - README.md | 2 +- config.example.yaml | 4 +- docs/configuration.md | 8 ++-- src/agent/__tests__/client-abort.test.ts | 19 ++++++++++ src/agent/__tests__/runtime-retry.test.ts | 20 ++++++++++ src/agent/__tests__/runtime-utils.test.ts | 31 +++++++++++++++ src/agent/client.ts | 14 ++++++- src/agent/runtime-utils.ts | 38 +++++++++++++++++++ src/agent/runtime.ts | 12 ++++++ src/config/__tests__/loader.test.ts | 2 +- src/config/model-catalog.ts | 11 ++++-- src/config/providers.ts | 4 +- src/config/schema.ts | 2 +- .../__tests__/anthropic-provider.test.ts | 36 ++++++++++++++++++ .../__tests__/claude-code-provider.test.ts | 2 +- src/providers/model-resolver.ts | 35 +++++++++++++++++ src/webui/__tests__/setup-routes.test.ts | 10 ++--- 18 files changed, 228 insertions(+), 23 deletions(-) delete mode 100644 .gitkeep create mode 100644 src/providers/__tests__/anthropic-provider.test.ts diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 05820a6d..00000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-07-02T16:10:15.961Z for PR creation at branch issue-685-7823dcb3516f for issue https://github.com/xlabtg/teleton-agent/issues/685 \ No newline at end of file diff --git a/README.md b/README.md index 35ff5f4f..6907a76a 100644 --- a/README.md +++ b/README.md @@ -206,7 +206,7 @@ The `teleton setup` wizard generates a fully configured `~/.teleton/config.yaml` agent: provider: "anthropic" # anthropic | claude-code | openai | google | xai | groq | openrouter | moonshot | mistral | cerebras | zai | minimax | huggingface | nvidia | gocoon | local api_key: "sk-ant-api03-..." - model: "claude-opus-4-6" + model: "claude-opus-4-8" utility_model: "claude-haiku-4-5-20251001" # optional: summarization, compaction, vision max_agentic_iterations: 5 session_reset_policy: diff --git a/config.example.yaml b/config.example.yaml index 0096f1f4..d96e7bd5 100644 --- a/config.example.yaml +++ b/config.example.yaml @@ -5,8 +5,8 @@ agent: provider: "anthropic" # anthropic | openai | google | xai | groq | openrouter | nvidia api_key: "YOUR_API_KEY" - model: "claude-haiku-4-5-20251001" # Model ID (varies by provider) - # utility_model: "claude-3-5-haiku-20241022" # Optional: cheap model for summarization + model: "claude-opus-4-8" # Model ID (varies by provider) + # utility_model: "claude-haiku-4-5-20251001" # Optional: cheap model for summarization max_tokens: 4096 temperature: 0.7 # system_prompt: null # Custom system prompt override (null = use SOUL.md) diff --git a/docs/configuration.md b/docs/configuration.md index 37bb659f..b44bd6d6 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -40,7 +40,7 @@ LLM provider and agentic loop configuration. |-----|------|---------|-------------| | `agent.provider` | `enum` | `"anthropic"` | LLM provider. One of: `anthropic`, `claude-code`, `openai`, `google`, `xai`, `groq`, `openrouter`, `moonshot`, `mistral`, `cerebras`, `zai`, `minimax`, `huggingface`, `nvidia`, `gocoon`, `local`. | | `agent.api_key` | `string` | `""` | API key for the chosen provider. Can be overridden with `TELETON_API_KEY` env var. | -| `agent.model` | `string` | `"claude-haiku-4-5-20251001"` | Primary model ID. Auto-detected from provider if not set (only for non-Anthropic providers). | +| `agent.model` | `string` | `"claude-opus-4-8"` | Primary model ID. Auto-detected from provider if not set (only for non-Anthropic providers). | | `agent.utility_model` | `string` | *auto-detected* | Cheap/fast model used for summarization and compaction. If omitted, the platform selects one based on the provider (e.g., `claude-haiku-4-5-20251001` for Anthropic, `gpt-4o-mini` for OpenAI). | | `agent.base_url` | `string` | *optional* | Base URL for local LLM server (e.g., `http://localhost:11434/v1`). Must be a valid URL. | | `agent.max_tokens` | `number` | `4096` | Maximum tokens in each LLM response. | @@ -65,7 +65,7 @@ Controls when conversation sessions are cleared, giving the agent a fresh memory agent: provider: "anthropic" api_key: "sk-ant-..." - model: "claude-haiku-4-5-20251001" + model: "claude-opus-4-8" utility_model: "claude-haiku-4-5-20251001" max_tokens: 4096 temperature: 0.7 @@ -83,7 +83,7 @@ When you change the `provider` and omit `model`, the platform auto-selects: | Provider | Default Model | Default Utility Model | |----------|--------------|----------------------| -| `anthropic` | `claude-haiku-4-5-20251001` | `claude-haiku-4-5-20251001` | +| `anthropic` | `claude-opus-4-8` | `claude-haiku-4-5-20251001` | | `codex` | `gpt-5.5` | `gpt-5.1-codex-mini` | | `openai` | `gpt-5.5` | `gpt-4o-mini` | | `google` | `gemini-2.5-flash` | `gemini-2.0-flash-lite` | @@ -737,7 +737,7 @@ meta: agent: provider: "anthropic" api_key: "sk-ant-..." - model: "claude-haiku-4-5-20251001" + model: "claude-opus-4-8" max_tokens: 4096 temperature: 0.7 max_agentic_iterations: 5 diff --git a/src/agent/__tests__/client-abort.test.ts b/src/agent/__tests__/client-abort.test.ts index ace31bf9..9673ec33 100644 --- a/src/agent/__tests__/client-abort.test.ts +++ b/src/agent/__tests__/client-abort.test.ts @@ -88,4 +88,23 @@ describe("chatWithContext abort signal handling", () => { expect(completeSignal?.aborted).toBe(true); expect(completeSignal?.reason).toBe(reason); }); + + it("omits temperature for Claude Opus 4.8 requests", async () => { + const { chatWithContext } = await import("../client.js"); + + await chatWithContext( + { + ...agentConfig(), + provider: "anthropic", + api_key: "sk-ant-test-key", + model: "claude-opus-4-8", + temperature: 0.7, + }, + { + context: { messages: [], systemPrompt: "test" }, + } + ); + + expect(getCapturedCompleteOptions()).not.toHaveProperty("temperature"); + }); }); diff --git a/src/agent/__tests__/runtime-retry.test.ts b/src/agent/__tests__/runtime-retry.test.ts index a4849ddc..5952902f 100644 --- a/src/agent/__tests__/runtime-retry.test.ts +++ b/src/agent/__tests__/runtime-retry.test.ts @@ -223,6 +223,26 @@ describe("AgentRuntime retry backoff", () => { expect(chatWithContextMock).toHaveBeenCalledTimes(2); }); + it("reports Anthropic 410 responses as an unavailable model with recovery guidance", async () => { + const runtime = await createRuntime(); + chatWithContextMock.mockResolvedValueOnce(errorResponse("410 status code (no body)")); + + await expect( + runtime.processMessage({ + chatId: "1001", + userMessage: "hello", + userName: "Owner", + toolContext: { + senderId: 1001, + config: makeConfig(), + }, + }) + ).rejects.toThrow( + 'Provider anthropic rejected model "claude-opus-4-6" with 410 status code (no body)' + ); + expect(chatWithContextMock).toHaveBeenCalledTimes(1); + }); + it("stops a pending rate-limit backoff when the signal is aborted", async () => { vi.useFakeTimers(); const runtime = await createRuntime(); diff --git a/src/agent/__tests__/runtime-utils.test.ts b/src/agent/__tests__/runtime-utils.test.ts index a0c75bec..09c50c76 100644 --- a/src/agent/__tests__/runtime-utils.test.ts +++ b/src/agent/__tests__/runtime-utils.test.ts @@ -6,6 +6,8 @@ import { sleepWithAbort, isNetworkError, isNetworkErrorMessage, + isModelUnavailableError, + getModelUnavailableDiagnostic, getEmptyResponseDiagnostic, getEmptyResponseRecoveryPrompt, trimRagContext, @@ -379,6 +381,35 @@ describe("isNetworkErrorMessage", () => { }); }); +describe("model unavailable diagnostics", () => { + it("detects Anthropic 410 responses as unavailable model errors", () => { + expect(isModelUnavailableError("410 status code (no body)")).toBe(true); + }); + + it("returns update guidance when the default model differs from the configured model", () => { + const result = getModelUnavailableDiagnostic({ + provider: "anthropic", + model: "claude-opus-4-6", + defaultModel: "claude-opus-4-8", + errorMessage: "410 status code (no body)", + }); + + expect(result).toContain('Provider anthropic rejected model "claude-opus-4-6"'); + expect(result).toContain('Update agent.model to "claude-opus-4-8"'); + }); + + it("does not produce guidance for unrelated API errors", () => { + expect( + getModelUnavailableDiagnostic({ + provider: "anthropic", + model: "claude-opus-4-8", + defaultModel: "claude-opus-4-8", + errorMessage: "500 internal server error", + }) + ).toBeNull(); + }); +}); + // ─── T14b: getEmptyResponseDiagnostic ────────────────────────────────── describe("getEmptyResponseDiagnostic", () => { diff --git a/src/agent/client.ts b/src/agent/client.ts index 93c23d37..42c76eb8 100644 --- a/src/agent/client.ts +++ b/src/agent/client.ts @@ -58,6 +58,12 @@ function normalizeProviderTemperature(provider: SupportedProvider, temperature: return Math.min(Math.max(temperature, 0), NVIDIA_MAX_TEMPERATURE); } +function shouldOmitProviderTemperature(provider: SupportedProvider, modelId: string): boolean { + if (provider !== "anthropic" && provider !== "claude-code") return false; + + return /^claude-opus-4-(?:7|8)(?:$|-)/.test(modelId.toLowerCase()); +} + export interface ChatOptions { systemPrompt?: string; context: Context; @@ -134,13 +140,15 @@ export async function chatWithContext( const completeOptions: Record = { apiKey: getEffectiveApiKey(provider, config.api_key), maxTokens: options.maxTokens ?? config.max_tokens, - temperature, sessionId: options.sessionId, cacheRetention: provider === "nvidia" ? NVIDIA_CACHE_RETENTION : DEFAULT_CACHE_RETENTION, signal: options.signal ? AbortSignal.any([options.signal, requestTimeoutSignal]) : requestTimeoutSignal, }; + if (!shouldOmitProviderTemperature(provider, config.model)) { + completeOptions.temperature = temperature; + } let response = await complete(model, context, completeOptions as ProviderStreamOptions); @@ -191,10 +199,12 @@ export function streamWithContext(config: AgentConfig, options: ChatOptions): St const streamOptions: Record = { apiKey: getEffectiveApiKey(provider, config.api_key), maxTokens: options.maxTokens ?? config.max_tokens, - temperature, sessionId: options.sessionId, cacheRetention: provider === "nvidia" ? NVIDIA_CACHE_RETENTION : DEFAULT_CACHE_RETENTION, }; + if (!shouldOmitProviderTemperature(provider, config.model)) { + streamOptions.temperature = temperature; + } const eventStream = stream(model, context, streamOptions as ProviderStreamOptions); diff --git a/src/agent/runtime-utils.ts b/src/agent/runtime-utils.ts index 3de12246..06fdf59d 100644 --- a/src/agent/runtime-utils.ts +++ b/src/agent/runtime-utils.ts @@ -97,6 +97,44 @@ export function isNetworkErrorMessage(message: string): boolean { ); } +export interface ModelUnavailableDiagnosticInput { + provider: string; + model: string; + errorMessage: string; + defaultModel?: string; +} + +export function isModelUnavailableError(errorMessage: string): boolean { + const lower = errorMessage.toLowerCase(); + return ( + /\b410\b/.test(lower) || + lower.includes("gone") || + lower.includes("model not found") || + lower.includes("model_not_found") || + lower.includes("model unavailable") || + lower.includes("model_unavailable") || + lower.includes("not available or recognized") || + lower.includes("does not exist") || + (lower.includes("selected model") && lower.includes("not available")) + ); +} + +export function getModelUnavailableDiagnostic( + input: ModelUnavailableDiagnosticInput +): string | null { + if (!isModelUnavailableError(input.errorMessage)) return null; + + const base = + `Provider ${input.provider} rejected model "${input.model}" with ${input.errorMessage}. ` + + "This usually means the configured model ID or provider endpoint is no longer available."; + + if (input.defaultModel && input.defaultModel !== input.model) { + return `${base} Update agent.model to "${input.defaultModel}" or run teleton setup again.`; + } + + return `${base} Choose a currently supported model for this provider and update agent.model.`; +} + export interface EmptyResponseDiagnosticInput { provider: string; model: string; diff --git a/src/agent/runtime.ts b/src/agent/runtime.ts index 0d7fe5d5..356aac25 100644 --- a/src/agent/runtime.ts +++ b/src/agent/runtime.ts @@ -87,6 +87,7 @@ import { isNetworkErrorMessage, getEmptyResponseDiagnostic, getEmptyResponseRecoveryPrompt, + getModelUnavailableDiagnostic, trimRagContext, LoopStallDetector, sleepWithAbort, @@ -1098,6 +1099,17 @@ export class AgentRuntime { `Network error after ${NETWORK_ERROR_MAX_RETRIES} retries. Please check your connection and try again.` ); } else { + const modelDiagnostic = getModelUnavailableDiagnostic({ + provider, + model: this.config.agent.model, + errorMessage: errorMsg, + defaultModel: providerMeta.defaultModel, + }); + if (modelDiagnostic) { + log.error(`🚨 ${modelDiagnostic}`); + throw new Error(modelDiagnostic); + } + log.error(`🚨 API error: ${errorMsg}`); throw new Error(`API error: ${errorMsg || "Unknown error"}`); } diff --git a/src/config/__tests__/loader.test.ts b/src/config/__tests__/loader.test.ts index d08fb7d8..9f2cf97f 100644 --- a/src/config/__tests__/loader.test.ts +++ b/src/config/__tests__/loader.test.ts @@ -420,7 +420,7 @@ describe("Config Loader", () => { const config = loadConfig(TEST_CONFIG_PATH); // Agent defaults - expect(config.agent.model).toBe("claude-opus-4-6"); + expect(config.agent.model).toBe("claude-opus-4-8"); expect(config.agent.max_tokens).toBe(4096); expect(config.agent.temperature).toBe(0.7); expect(config.agent.max_agentic_iterations).toBe(5); diff --git a/src/config/model-catalog.ts b/src/config/model-catalog.ts index 59d404ec..c3ab3574 100644 --- a/src/config/model-catalog.ts +++ b/src/config/model-catalog.ts @@ -96,15 +96,20 @@ export const GROQ_TTS_MODELS: ModelOption[] = [ export const MODEL_OPTIONS: Record = { anthropic: [ + { + value: "claude-opus-4-8", + name: "Claude Opus 4.8", + description: "Current default, 1M ctx, $5/$25", + }, { value: "claude-opus-4-7", name: "Claude Opus 4.7", - description: "Most capable available, 1M ctx, reasoning, $5/$25", + description: "Previous gen, 1M ctx, reasoning, $5/$25", }, { value: "claude-opus-4-6", name: "Claude Opus 4.6", - description: "Previous gen, 1M ctx, reasoning, $5/$25", + description: "Legacy, may be retired, 1M ctx, $5/$25", }, { value: "claude-opus-4-5-20251101", @@ -119,7 +124,7 @@ export const MODEL_OPTIONS: Record = { { value: "claude-haiku-4-5-20251001", name: "Claude Haiku 4.5", - description: "Fast & cheap, 200K ctx, $1/$5 (default)", + description: "Fast & cheap, 200K ctx, $1/$5", }, ], openai: [ diff --git a/src/config/providers.ts b/src/config/providers.ts index 6ae71bfc..906d02ee 100644 --- a/src/config/providers.ts +++ b/src/config/providers.ts @@ -37,7 +37,7 @@ const PROVIDER_REGISTRY: Record = { keyPrefix: "sk-ant-", keyHint: "sk-ant-api03-...", consoleUrl: "https://console.anthropic.com/", - defaultModel: "claude-opus-4-6", + defaultModel: "claude-opus-4-8", utilityModel: "claude-haiku-4-5-20251001", toolLimit: null, piAiProvider: "anthropic", @@ -49,7 +49,7 @@ const PROVIDER_REGISTRY: Record = { keyPrefix: "sk-ant-", keyHint: "Auto-detected from Claude Code", consoleUrl: "https://console.anthropic.com/", - defaultModel: "claude-opus-4-6", + defaultModel: "claude-opus-4-8", utilityModel: "claude-haiku-4-5-20251001", toolLimit: null, piAiProvider: "anthropic", diff --git a/src/config/schema.ts b/src/config/schema.ts index a73467cd..4bad3863 100644 --- a/src/config/schema.ts +++ b/src/config/schema.ts @@ -72,7 +72,7 @@ export const AgentConfigSchema = z.object({ .url() .optional() .describe("Base URL for local LLM server (e.g. http://localhost:11434/v1)"), - model: z.string().default("claude-opus-4-6"), + model: z.string().default("claude-opus-4-8"), utility_model: z .string() .optional() diff --git a/src/providers/__tests__/anthropic-provider.test.ts b/src/providers/__tests__/anthropic-provider.test.ts new file mode 100644 index 00000000..12a756b6 --- /dev/null +++ b/src/providers/__tests__/anthropic-provider.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from "vitest"; +import { getModelsForProvider } from "../../config/model-catalog.js"; +import { getProviderMetadata } from "../../config/providers.js"; +import { AgentConfigSchema } from "../../config/schema.js"; +import { getProviderModel } from "../model-resolver.js"; + +describe("Anthropic provider registration", () => { + it("defaults to the current Opus model for primary calls", () => { + const meta = getProviderMetadata("anthropic"); + + expect(meta.defaultModel).toBe("claude-opus-4-8"); + expect(meta.utilityModel).toBe("claude-haiku-4-5-20251001"); + }); + + it("resolves Claude Opus 4.8 even before pi-ai ships it in the generated registry", () => { + const model = getProviderModel("anthropic", "claude-opus-4-8"); + + expect(model.id).toBe("claude-opus-4-8"); + expect(model.api).toBe("anthropic-messages"); + expect(model.provider).toBe("anthropic"); + expect(model.contextWindow).toBe(1_000_000); + expect(model.maxTokens).toBe(128_000); + }); + + it("exposes Claude Opus 4.8 first in the setup model catalog", () => { + const models = getModelsForProvider("anthropic"); + + expect(models[0]?.value).toBe("claude-opus-4-8"); + }); + + it("uses Claude Opus 4.8 in AgentConfigSchema defaults", () => { + const result = AgentConfigSchema.parse({}); + + expect(result.model).toBe("claude-opus-4-8"); + }); +}); diff --git a/src/providers/__tests__/claude-code-provider.test.ts b/src/providers/__tests__/claude-code-provider.test.ts index a631c8b5..191ca5a0 100644 --- a/src/providers/__tests__/claude-code-provider.test.ts +++ b/src/providers/__tests__/claude-code-provider.test.ts @@ -14,7 +14,7 @@ describe("claude-code provider registration", () => { expect(meta.displayName).toBe("Claude Code (Auto)"); expect(meta.piAiProvider).toBe("anthropic"); expect(meta.toolLimit).toBeNull(); - expect(meta.defaultModel).toBe("claude-opus-4-6"); + expect(meta.defaultModel).toBe("claude-opus-4-8"); expect(meta.utilityModel).toBe("claude-haiku-4-5-20251001"); expect(meta.keyPrefix).toBe("sk-ant-"); }); diff --git a/src/providers/model-resolver.ts b/src/providers/model-resolver.ts index 2342a5e0..0a04ccf7 100644 --- a/src/providers/model-resolver.ts +++ b/src/providers/model-resolver.ts @@ -7,6 +7,29 @@ const log = createLogger("LLM"); const modelCache = new Map>(); +// Bridge current Anthropic IDs until pi-ai's generated registry includes them. +// Keep reasoning disabled here because pi-ai 0.73.1 does not know Opus 4.8's +// adaptive-thinking rules and would otherwise choose the older budgeted path. +const ANTHROPIC_MODEL_OVERRIDES: Record> = { + "claude-opus-4-8": { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + api: "anthropic-messages", + provider: "anthropic", + baseUrl: "https://api.anthropic.com", + reasoning: false, + input: ["text", "image"], + cost: { + input: 5, + output: 25, + cacheRead: 0.5, + cacheWrite: 6.25, + }, + contextWindow: 1_000_000, + maxTokens: 128_000, + }, +}; + const GOCOON_MODELS: Record> = {}; /** Register models discovered from a running gocoon-runner (native OpenAI-compatible API). */ @@ -181,6 +204,12 @@ export function getProviderModel(provider: SupportedProvider, modelId: string): return model; } + if (meta.piAiProvider === "anthropic" && ANTHROPIC_MODEL_OVERRIDES[modelId]) { + const model = ANTHROPIC_MODEL_OVERRIDES[modelId]; + modelCache.set(cacheKey, model); + return model; + } + // Moonshot backward-compat: remap old model IDs to kimi-coding IDs if (provider === "moonshot" && MOONSHOT_MODEL_ALIASES[modelId]) { modelId = MOONSHOT_MODEL_ALIASES[modelId]; @@ -200,6 +229,12 @@ export function getProviderModel(provider: SupportedProvider, modelId: string): const fallbackCached = modelCache.get(fallbackKey); if (fallbackCached) return fallbackCached; + if (meta.piAiProvider === "anthropic" && ANTHROPIC_MODEL_OVERRIDES[meta.defaultModel]) { + const model = ANTHROPIC_MODEL_OVERRIDES[meta.defaultModel]; + modelCache.set(fallbackKey, model); + return model; + } + try { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- same as above: dynamic strings const model = getModel(meta.piAiProvider as any, meta.defaultModel as any); diff --git a/src/webui/__tests__/setup-routes.test.ts b/src/webui/__tests__/setup-routes.test.ts index ba945afa..55bdc23f 100644 --- a/src/webui/__tests__/setup-routes.test.ts +++ b/src/webui/__tests__/setup-routes.test.ts @@ -55,7 +55,7 @@ vi.mock("../../config/providers.js", () => ({ { id: "anthropic", displayName: "Anthropic (Claude)", - defaultModel: "claude-haiku-4-5-20251001", + defaultModel: "claude-opus-4-8", utilityModel: "claude-haiku-4-5-20251001", toolLimit: null, keyPrefix: "sk-ant-", @@ -74,7 +74,7 @@ vi.mock("../../config/providers.js", () => ({ getProviderMetadata: vi.fn(() => ({ id: "anthropic", displayName: "Anthropic (Claude)", - defaultModel: "claude-haiku-4-5-20251001", + defaultModel: "claude-opus-4-8", })), validateApiKeyFormat: vi.fn(), })); @@ -179,7 +179,7 @@ describe("Setup API Routes", () => { { id: "anthropic", displayName: "Anthropic (Claude)", - defaultModel: "claude-haiku-4-5-20251001", + defaultModel: "claude-opus-4-8", utilityModel: "claude-haiku-4-5-20251001", toolLimit: null, keyPrefix: "sk-ant-", @@ -198,7 +198,7 @@ describe("Setup API Routes", () => { (getProviderMetadata as Mock).mockReturnValue({ id: "anthropic", displayName: "Anthropic (Claude)", - defaultModel: "claude-haiku-4-5-20251001", + defaultModel: "claude-opus-4-8", }); (validateApiKeyFormat as Mock).mockReturnValue(undefined); (ConfigSchema.parse as Mock).mockImplementation((v: unknown) => v); @@ -1035,7 +1035,7 @@ mtproto: const writeCall = (writeFileSync as Mock).mock.calls[0]; // The model should fall back to providerMeta.defaultModel - expect(writeCall[1]).toContain("claude-haiku-4-5-20251001"); + expect(writeCall[1]).toContain("claude-opus-4-8"); }); it("writes config with restricted permissions (0o600)", async () => {