From 2428ad2bcd337fee0f296cf07d0b323dac58d50e Mon Sep 17 00:00:00 2001 From: Randi <55005611+rdself@users.noreply.github.com> Date: Fri, 29 May 2026 00:44:58 -0400 Subject: [PATCH] feat(claude): default xhigh support for newer Opus models (#2874) Integrated into release/v3.8.6. --- @omniroute/opencode-provider/README.md | 6 +- @omniroute/opencode-provider/src/index.ts | 5 +- .../opencode-provider/tests/index.test.ts | 10 ++- docs/guides/USER_GUIDE.md | 4 +- open-sse/config/providerModels.ts | 11 ++- open-sse/config/providerRegistry.ts | 7 +- open-sse/executors/base.ts | 4 +- open-sse/services/claudeCodeCompatible.ts | 2 +- open-sse/services/modelFamilyFallback.ts | 1 + open-sse/services/providerCostData.ts | 1 + src/lib/db/settings.ts | 5 +- src/lib/providers/claudeFastMode.ts | 10 ++- src/lib/providers/staticModels.ts | 1 + src/shared/constants/modelSpecs.ts | 14 ++++ src/shared/constants/pricing.ts | 10 +++ .../base-executor-sanitize-effort.test.ts | 4 +- tests/unit/cc-compatible-provider.test.ts | 7 +- .../claude-code-compatible-request.test.ts | 9 ++- tests/unit/claude-fast-mode.test.ts | 73 +++++++++++++++++++ tests/unit/provider-models-config.test.ts | 12 +++ tests/unit/t12-pricing-updates.test.ts | 4 + .../unit/t31-t33-t34-t38-model-specs.test.ts | 2 + .../unit/translator-openai-to-claude.test.ts | 9 ++- 23 files changed, 181 insertions(+), 30 deletions(-) create mode 100644 tests/unit/claude-fast-mode.test.ts diff --git a/@omniroute/opencode-provider/README.md b/@omniroute/opencode-provider/README.md index fdbfeba8c8..624fddbb6e 100644 --- a/@omniroute/opencode-provider/README.md +++ b/@omniroute/opencode-provider/README.md @@ -100,7 +100,7 @@ Exported for completeness. Strips trailing `/`, deduplicates a trailing `/v1`, a - `OMNIROUTE_PROVIDER_KEY` — `"omniroute"` (the key used under `provider.*`). - `OMNIROUTE_PROVIDER_NPM` — `"@ai-sdk/openai-compatible"` (the runtime delegate). - `OPENCODE_CONFIG_SCHEMA` — `"https://opencode.ai/config.json"`. -- `OMNIROUTE_DEFAULT_OPENCODE_MODELS` — readonly list of 4 default model ids. +- `OMNIROUTE_DEFAULT_OPENCODE_MODELS` — readonly list of default model ids. ## Custom model catalog @@ -110,10 +110,10 @@ import { createOmniRouteProvider } from "@omniroute/opencode-provider"; createOmniRouteProvider({ baseURL: "http://localhost:20128", apiKey: "sk_omniroute", - models: ["auto", "claude-opus-4-7", "gpt-5.5"], + models: ["auto", "claude-opus-4-8", "gpt-5.5"], modelLabels: { auto: "Auto-Combo (recommended)", - "claude-opus-4-7": "Claude Opus 4.7", + "claude-opus-4-8": "Claude Opus 4.8", "gpt-5.5": "GPT-5.5", }, }); diff --git a/@omniroute/opencode-provider/src/index.ts b/@omniroute/opencode-provider/src/index.ts index 454f45012c..6f3a2384ee 100644 --- a/@omniroute/opencode-provider/src/index.ts +++ b/@omniroute/opencode-provider/src/index.ts @@ -46,6 +46,7 @@ export const OPENCODE_CONFIG_SCHEMA = "https://opencode.ai/config.json" as const * with Claude Code passthrough models (`cc/` prefix). */ export const OMNIROUTE_DEFAULT_OPENCODE_MODELS = [ + "cc/claude-opus-4-8", "cc/claude-opus-4-7", "cc/claude-sonnet-4-6", "cc/claude-haiku-4-5-20251001", @@ -83,7 +84,8 @@ export interface ModelCapabilities { * Matches the context lengths used by OmniRoute's provider registry. */ export const OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS: Record = { - "cc/claude-opus-4-7": 200_000, + "cc/claude-opus-4-8": 1_000_000, + "cc/claude-opus-4-7": 1_000_000, "cc/claude-sonnet-4-6": 200_000, "cc/claude-haiku-4-5-20251001": 200_000, "claude-opus-4-5-thinking": 200_000, @@ -100,6 +102,7 @@ export const OMNIROUTE_DEFAULT_MODEL_CONTEXT_LENGTHS: Record = { * model via `OmniRouteProviderOptions.modelCapabilities`. */ export const OMNIROUTE_DEFAULT_MODEL_CAPABILITIES: Record = { + "cc/claude-opus-4-8": { attachment: true, reasoning: true, temperature: true, tool_call: true }, "cc/claude-opus-4-7": { attachment: true, reasoning: true, temperature: true, tool_call: true }, "cc/claude-sonnet-4-6": { attachment: true, reasoning: true, temperature: true, tool_call: true }, "cc/claude-haiku-4-5-20251001": { attachment: true, temperature: true, tool_call: true }, diff --git a/@omniroute/opencode-provider/tests/index.test.ts b/@omniroute/opencode-provider/tests/index.test.ts index 9e14ba3bff..50d3439a33 100644 --- a/@omniroute/opencode-provider/tests/index.test.ts +++ b/@omniroute/opencode-provider/tests/index.test.ts @@ -388,6 +388,7 @@ test("createOmniRouteComboConfig includes optional fields when supplied", () => test("OMNIROUTE_DEFAULT_OPENCODE_MODELS includes cc/ prefixed models", () => { const defaults = [...OMNIROUTE_DEFAULT_OPENCODE_MODELS]; + assert.ok(defaults.includes("cc/claude-opus-4-8")); assert.ok( defaults.some((m) => m.startsWith("cc/")), "should have cc/ prefixed models" @@ -413,9 +414,10 @@ test("createOmniRouteProvider emits limit.context on default model entries", () baseURL: "http://localhost:20128", apiKey: "sk_omniroute", }); - const entry = provider.models["cc/claude-opus-4-7"]; + const entry = provider.models["cc/claude-opus-4-8"]; assert.ok(entry.limit, "model entry should have a limit field"); - assert.equal(entry.limit!.context, 200_000); + assert.equal(entry.limit!.context, 1_000_000); + assert.equal(provider.models["cc/claude-opus-4-7"].limit!.context, 1_000_000); }); test("createOmniRouteProvider omits limit.context for unknown model ids", () => { @@ -480,8 +482,8 @@ test("createOmniRouteProvider emits default capability flags inline with the mod baseURL: "http://localhost:20128", apiKey: "sk_omniroute", }); - const entry = provider.models["cc/claude-opus-4-7"]; - assert.equal(entry.name, "cc/claude-opus-4-7"); + const entry = provider.models["cc/claude-opus-4-8"]; + assert.equal(entry.name, "cc/claude-opus-4-8"); assert.equal(entry.attachment, true); assert.equal(entry.reasoning, true); assert.equal(entry.temperature, true); diff --git a/docs/guides/USER_GUIDE.md b/docs/guides/USER_GUIDE.md index 2cf8b50ff5..5a919867e7 100644 --- a/docs/guides/USER_GUIDE.md +++ b/docs/guides/USER_GUIDE.md @@ -578,7 +578,7 @@ For the full environment variable reference, see the [README](../README.md). > The list below is curated from `open-sse/config/providerRegistry.ts` for v3.8.0. Cloud catalogs (Gemini, OpenRouter, etc.) are synced dynamically — for the full live catalog open **Dashboard → Providers → [provider] → Available Models** or call `GET /api/models/catalog`. -**Claude Code (`cc/`)** — Pro/Max OAuth: `cc/claude-opus-4-7`, `cc/claude-opus-4-6`, `cc/claude-opus-4-5-20251101`, `cc/claude-sonnet-4-6`, `cc/claude-sonnet-4-5-20250929`, `cc/claude-haiku-4-5-20251001` +**Claude Code (`cc/`)** — Pro/Max OAuth: `cc/claude-opus-4-8`, `cc/claude-opus-4-7`, `cc/claude-opus-4-6`, `cc/claude-opus-4-5-20251101`, `cc/claude-sonnet-4-6`, `cc/claude-sonnet-4-5-20250929`, `cc/claude-haiku-4-5-20251001` **Codex (`cx/`)** — Plus/Pro OAuth: `cx/gpt-5.5` (+ effort tiers: `gpt-5.5-xhigh`, `gpt-5.5-high`, `gpt-5.5-medium`, `gpt-5.5-low`), `cx/gpt-5.4`, `cx/gpt-5.4-mini`, `cx/gpt-5.3-codex`, `cx/gpt-5.3-codex-spark`, `cx/gpt-5.2` @@ -626,7 +626,7 @@ For the full environment variable reference, see the [README](../README.md). **Other compatible providers** (selected): `cohere`, `databricks`, `snowflake`, `together`, `vertex`, `alibaba`, `alibaba-cn`, `bedrock` (via `aws-bedrock`), `azure-ai`, `openrouter` (passthrough catalog), `siliconflow`, `hyperbolic`, `huggingface`, `featherless-ai`, `cloudflare-ai`, `scaleway`, `deepinfra`, `vercel-ai-gateway`, `bazaarlink`, `friendliai`, `nous-research`, `reka`, `volcengine`, `ai21`, `gigachat`. Each maintains its own model list in `providerRegistry.ts` and can be auto-synced when the provider exposes a `/models` endpoint. -**Note on model IDs:** OmniRoute uses provider-native IDs (`claude-opus-4-7`, `gpt-5.5`, `glm-5.1`, `MiniMax-M2.7`, `kimi-k2.5`, `grok-4.20-0309-reasoning`). Some IDs include dotted versions because that is how the upstream API expects them. If a model is not listed above, run `omniroute models --search ` or hit `GET /api/models/catalog` to confirm availability. +**Note on model IDs:** OmniRoute uses provider-native IDs (`claude-opus-4-8`, `gpt-5.5`, `glm-5.1`, `MiniMax-M2.7`, `kimi-k2.5`, `grok-4.20-0309-reasoning`). Some IDs include dotted versions because that is how the upstream API expects them. If a model is not listed above, run `omniroute models --search ` or hit `GET /api/models/catalog` to confirm availability. diff --git a/open-sse/config/providerModels.ts b/open-sse/config/providerModels.ts index 66c7778b5a..c124984c91 100644 --- a/open-sse/config/providerModels.ts +++ b/open-sse/config/providerModels.ts @@ -64,5 +64,14 @@ export function supportsXHighEffort(aliasOrId: string, modelId: string): boolean const providerModels = PROVIDER_MODELS[alias] || PROVIDER_MODELS[aliasOrId]; // Unknown provider (not in registry) — pass through unchanged. if (!providerModels) return true; - return getProviderModel(alias, modelId)?.supportsXHighEffort === true; + const model = getProviderModel(alias, modelId); + + // Claude Code models default to supporting extra-high effort. Keep explicit + // false entries as the unsupported-model list so newly added Claude models do + // not need opt-in flags. + if (alias === "cc") { + return model?.supportsXHighEffort !== false; + } + + return model?.supportsXHighEffort === true; } diff --git a/open-sse/config/providerRegistry.ts b/open-sse/config/providerRegistry.ts index c7ba2fbded..fddcefbd8b 100644 --- a/open-sse/config/providerRegistry.ts +++ b/open-sse/config/providerRegistry.ts @@ -634,10 +634,15 @@ export const REGISTRY: Record = { tokenUrl: "https://console.anthropic.com/v1/oauth/token", }, models: [ + { + id: "claude-opus-4-8", + name: "Claude Opus 4.8", + contextLength: 1000000, + maxOutputTokens: 128000, + }, { id: "claude-opus-4-7", name: "Claude Opus 4.7", - supportsXHighEffort: true, contextLength: 1000000, maxOutputTokens: 128000, }, diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 8c31a28807..388f85e0f7 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -217,8 +217,8 @@ function hasActiveClaudeThinking(body: Record): boolean { * Each rejection burns a combo fallback attempt before reaching a working * provider. Apply provider-aware sanitation here (after transformRequest, so * reintroductions by per-provider transforms are also caught) before fetch. - * Models that genuinely support xhigh (registry flag supportsXHighEffort) - * pass through unchanged. + * Models that genuinely support xhigh pass through unchanged. Claude models + * default to xhigh support unless they are marked as legacy unsupported entries. */ const MISTRAL_NO_REASONING_EFFORT_PATTERN = /devstral/i; const GITHUB_NO_REASONING_EFFORT_PATTERN = /(claude|haiku|oswe)/i; diff --git a/open-sse/services/claudeCodeCompatible.ts b/open-sse/services/claudeCodeCompatible.ts index 1c589b09b0..4972f21835 100644 --- a/open-sse/services/claudeCodeCompatible.ts +++ b/open-sse/services/claudeCodeCompatible.ts @@ -51,7 +51,7 @@ const CLAUDE_CODE_COMPATIBLE_DEFAULT_SYSTEM_BLOCKS = [ text: "You are a Claude agent, built on Anthropic's Claude Agent SDK.", }, ]; -const CONTEXT_1M_SUPPORTED_MODELS = ["claude-opus-4-7", "claude-opus-4-6"]; +const CONTEXT_1M_SUPPORTED_MODELS = ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"]; export const CLAUDE_CODE_COMPATIBLE_STAINLESS_TIMEOUT_SECONDS = getStainlessTimeoutSeconds( process.env ); diff --git a/open-sse/services/modelFamilyFallback.ts b/open-sse/services/modelFamilyFallback.ts index 50d30f7617..06f2d4d05a 100644 --- a/open-sse/services/modelFamilyFallback.ts +++ b/open-sse/services/modelFamilyFallback.ts @@ -73,6 +73,7 @@ const MODEL_FAMILIES: Record = { "gemini-2.5-pro-preview-06-05": ["gemini-2.5-pro", "gemini-2.5-pro-exp-03-25"], // Claude Opus family + "claude-opus-4-8": ["claude-opus-4-7", "claude-opus-4-6", "claude-sonnet-4-6"], "claude-opus-4-7": ["claude-opus-4-6", "claude-opus-4-5-20251101", "claude-sonnet-4-6"], "claude-opus-4-6": ["claude-opus-4-6-thinking", "claude-opus-4-5-20251101", "claude-sonnet-4-6"], "claude-opus-4-6-thinking": ["claude-opus-4-6", "claude-opus-4-5-20251101"], diff --git a/open-sse/services/providerCostData.ts b/open-sse/services/providerCostData.ts index 98e668db89..7397b4641f 100644 --- a/open-sse/services/providerCostData.ts +++ b/open-sse/services/providerCostData.ts @@ -11,6 +11,7 @@ export interface ModelPricing { export const KNOWN_MODEL_PRICING: Record = { "gpt-4o": { inputCostPer1M: 2.5, outputCostPer1M: 10.0, isFree: false }, "gpt-4o-mini": { inputCostPer1M: 0.15, outputCostPer1M: 0.6, isFree: false }, + "claude-opus-4-8": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, "claude-opus-4-7": { inputCostPer1M: 15.0, outputCostPer1M: 75.0, isFree: false }, "claude-sonnet-4-6": { inputCostPer1M: 3.0, outputCostPer1M: 15.0, isFree: false }, "claude-haiku-4-5": { inputCostPer1M: 0.8, outputCostPer1M: 4.0, isFree: false }, diff --git a/src/lib/db/settings.ts b/src/lib/db/settings.ts index c9cbe957f7..8dcd735648 100644 --- a/src/lib/db/settings.ts +++ b/src/lib/db/settings.ts @@ -103,7 +103,10 @@ export async function getSettings() { autoRefreshProviderQuotaInterval: 180, comboConfigMode: "guided", codexServiceTier: { enabled: false }, - claudeFastMode: { enabled: false, supportedModels: ["claude-opus-4-7", "claude-opus-4-6"] }, + claudeFastMode: { + enabled: false, + supportedModels: ["claude-opus-4-8", "claude-opus-4-7", "claude-opus-4-6"], + }, codexSessionAffinityTtlMs: 0, alwaysPreserveClientCache: "auto", idempotencyWindowMs: 5000, diff --git a/src/lib/providers/claudeFastMode.ts b/src/lib/providers/claudeFastMode.ts index 9d2e7632fe..2418426198 100644 --- a/src/lib/providers/claudeFastMode.ts +++ b/src/lib/providers/claudeFastMode.ts @@ -6,7 +6,11 @@ type JsonRecord = Record; * Mirrors the binary-side gate observed in claude-code v2.1.145 (KT() check): * only the latest Opus tiers can request the priority service path. */ -export const CLAUDE_FAST_MODE_DEFAULT_MODELS = ["claude-opus-4-7", "claude-opus-4-6"] as const; +export const CLAUDE_FAST_MODE_DEFAULT_MODELS = [ + "claude-opus-4-8", + "claude-opus-4-7", + "claude-opus-4-6", +] as const; function asRecord(value: unknown): JsonRecord { return value && typeof value === "object" && !Array.isArray(value) ? (value as JsonRecord) : {}; @@ -39,7 +43,7 @@ export function isClaudeFastModeEnabled(settings: unknown): boolean { /** * Returns the configured supported-model list, defaulting to the conservative - * Opus 4-7 / 4-6 pair (matches the binary KT() gate). + * Opus 4-8 / 4-7 / 4-6 set. */ export function getClaudeFastModeSupportedModels(settings: unknown): string[] { const record = asRecord(settings); @@ -51,7 +55,7 @@ export function getClaudeFastModeSupportedModels(settings: unknown): string[] { /** * True when the toggle is on AND the model id prefix-matches a supported model. - * Prefix matching tolerates dated suffixes (e.g. claude-opus-4-7-20260201). + * Prefix matching tolerates dated suffixes (e.g. claude-opus-4-8-20260528). */ export function shouldRequestClaudeFastMode( settings: unknown, diff --git a/src/lib/providers/staticModels.ts b/src/lib/providers/staticModels.ts index 4c2db473b4..6e17464449 100644 --- a/src/lib/providers/staticModels.ts +++ b/src/lib/providers/staticModels.ts @@ -37,6 +37,7 @@ const STATIC_MODEL_PROVIDERS: Record Array<{ id: string; name: str ], antigravity: () => ANTIGRAVITY_PUBLIC_MODELS.map((model) => ({ ...model })), claude: () => [ + { id: "claude-opus-4-8", name: "Claude Opus 4.8" }, { id: "claude-opus-4-7", name: "Claude Opus 4.7" }, { id: "claude-opus-4-6", name: "Claude Opus 4.6" }, { id: "claude-sonnet-4-6", name: "Claude Sonnet 4.6" }, diff --git a/src/shared/constants/modelSpecs.ts b/src/shared/constants/modelSpecs.ts index 7af781da61..ff766f2431 100644 --- a/src/shared/constants/modelSpecs.ts +++ b/src/shared/constants/modelSpecs.ts @@ -187,6 +187,20 @@ export const MODEL_SPECS: Record = { aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-7", "claude-opus-4.7"), }, + // ── Claude Opus 4.8 ───────────────────────────────────────────── + "claude-opus-4-8": { + maxOutputTokens: 128000, + contextWindow: 1000000, + // Opus 4.8 inherits Opus 4.7's adaptive thinking constraints: no fixed + // thinking budget requests, with effort controlled by output_config. + defaultThinkingBudget: 32000, + thinkingBudgetCap: 120000, + supportsThinking: true, + supportsTools: true, + supportsVision: true, + aliases: BEDROCK_CLAUDE_ALIASES("claude-opus-4-8", "claude-opus-4.8"), + }, + // ── Claude Sonnet 4.5 ─────────────────────────────────────────── "claude-sonnet-4-5-20250929": { maxOutputTokens: 64000, diff --git a/src/shared/constants/pricing.ts b/src/shared/constants/pricing.ts index 93b34a0e9f..e91703b036 100644 --- a/src/shared/constants/pricing.ts +++ b/src/shared/constants/pricing.ts @@ -129,6 +129,13 @@ export const DEFAULT_PRICING = { // Claude Code (cc) cc: { + "claude-opus-4-8": { + input: 5.0, + output: 25.0, + cached: 2.5, + reasoning: 25.0, + cache_creation: 5.0, + }, "claude-opus-4-7": { input: 5.0, output: 25.0, @@ -710,6 +717,9 @@ export const DEFAULT_PRICING = { // Common model IDs (without dates) used across providers // Intentional duplicates of dot-notation variants (e.g. claude-opus-4.6) // to cover hyphen-notation IDs (claude-opus-4-6) used by some clients + "claude-opus-4.8": CLAUDE_OPUS_4_PRICING, + "claude-opus-4-8": CLAUDE_OPUS_4_PRICING, + "claude-opus-4-7": CLAUDE_OPUS_4_PRICING, "claude-opus-4-6": CLAUDE_OPUS_46_PRICING, "claude-sonnet-4-6": CLAUDE_SONNET_46_PRICING, "claude-opus-4-5-20251101": CLAUDE_OPUS_4_PRICING, diff --git a/tests/unit/base-executor-sanitize-effort.test.ts b/tests/unit/base-executor-sanitize-effort.test.ts index 2fafe80611..abb0930595 100644 --- a/tests/unit/base-executor-sanitize-effort.test.ts +++ b/tests/unit/base-executor-sanitize-effort.test.ts @@ -101,8 +101,8 @@ test("sanitizeReasoningEffortForProvider: mistral/devstral preserves reasoning w }); test("sanitizeReasoningEffortForProvider: codex with xhigh passes through unchanged when model supports it", () => { - // codex/gpt-5.5-xhigh and related Claude opus models are flagged - // supportsXHighEffort:true in providerRegistry. They must not be downgraded. + // codex/gpt-5.5-xhigh is flagged supportsXHighEffort:true in providerRegistry. + // Claude Opus 4.7+ models default to xhigh support unless explicitly opted out. const body = { model: "gpt-5.5-xhigh", reasoning_effort: "xhigh", diff --git a/tests/unit/cc-compatible-provider.test.ts b/tests/unit/cc-compatible-provider.test.ts index 5eac9493c8..47cc5ad958 100644 --- a/tests/unit/cc-compatible-provider.test.ts +++ b/tests/unit/cc-compatible-provider.test.ts @@ -18,7 +18,8 @@ const { CLAUDE_CODE_COMPATIBLE_DEFAULT_MODELS_PATH, joinClaudeCodeCompatibleUrl, } = await import("../../open-sse/services/claudeCodeCompatible.ts"); -const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); +const { getModelsByProviderId, supportsXHighEffort } = + await import("../../open-sse/config/providerModels.ts"); const { handleChatCore } = await import("../../open-sse/handlers/chatCore.ts"); const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); const providerNodesRoute = await import("../../src/app/api/provider-nodes/route.ts"); @@ -144,8 +145,8 @@ test("buildClaudeCodeCompatibleRequest keeps prior role history while dropping t }); test("buildClaudeCodeCompatibleRequest preserves xhigh for Claude models that support it", () => { - const xhighModel = getModelsByProviderId("claude").find( - (model) => model.supportsXHighEffort === true + const xhighModel = getModelsByProviderId("claude").find((model) => + supportsXHighEffort("claude", model.id) ); assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); const payload = buildClaudeCodeCompatibleRequest({ diff --git a/tests/unit/claude-code-compatible-request.test.ts b/tests/unit/claude-code-compatible-request.test.ts index 67057a4fa0..13ece0f7b3 100644 --- a/tests/unit/claude-code-compatible-request.test.ts +++ b/tests/unit/claude-code-compatible-request.test.ts @@ -11,12 +11,15 @@ const { resolveClaudeCodeCompatibleMaxTokens, buildClaudeCodeCompatibleRequest, } = await import("../../open-sse/services/claudeCodeCompatible.ts"); -const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); +const { getModelsByProviderId, supportsXHighEffort } = + await import("../../open-sse/config/providerModels.ts"); function getClaudeEffortFixtures() { const claudeModels = getModelsByProviderId("claude"); - const xhighModel = claudeModels.find((model) => model.supportsXHighEffort === true); - const standardModel = claudeModels.find((model) => model.supportsXHighEffort === false); + const xhighModel = claudeModels.find((model) => supportsXHighEffort("claude", model.id)); + const standardModel = claudeModels.find( + (model) => supportsXHighEffort("claude", model.id) === false + ); assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); assert.ok(standardModel, "expected at least one Claude model without xhigh support"); return { xhighModel, standardModel }; diff --git a/tests/unit/claude-fast-mode.test.ts b/tests/unit/claude-fast-mode.test.ts new file mode 100644 index 0000000000..92b6be05e2 --- /dev/null +++ b/tests/unit/claude-fast-mode.test.ts @@ -0,0 +1,73 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { + shouldRequestClaudeFastMode, + CLAUDE_FAST_MODE_DEFAULT_MODELS, + getClaudeFastModeSupportedModels, + isClaudeFastModeEnabled, +} = await import("../../src/lib/providers/claudeFastMode.ts"); + +const enabledSettings = { claudeFastMode: true }; +const disabledSettings = { claudeFastMode: false }; + +test("shouldRequestClaudeFastMode returns false when fast mode is disabled", () => { + assert.equal(shouldRequestClaudeFastMode(disabledSettings, "claude-opus-4-8"), false); + assert.equal(shouldRequestClaudeFastMode({}, "claude-opus-4-8"), false); + assert.equal(shouldRequestClaudeFastMode(null, "claude-opus-4-8"), false); +}); + +test("shouldRequestClaudeFastMode returns false for non-string or empty modelId", () => { + assert.equal(shouldRequestClaudeFastMode(enabledSettings, null), false); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, undefined), false); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, ""), false); +}); + +test("shouldRequestClaudeFastMode returns true for claude-opus-4-8 exact match", () => { + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-8"), true); +}); + +test("shouldRequestClaudeFastMode prefix-matches claude-opus-4-8 with dated suffix", () => { + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-8-20260528"), true); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-8-20260101"), true); +}); + +test("shouldRequestClaudeFastMode returns true for claude-opus-4-7 and claude-opus-4-6", () => { + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-7"), true); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-6"), true); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-opus-4-7-20250101"), true); +}); + +test("shouldRequestClaudeFastMode returns false for non-Opus models", () => { + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-sonnet-4-6"), false); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "claude-haiku-4-5"), false); + assert.equal(shouldRequestClaudeFastMode(enabledSettings, "gpt-4o"), false); +}); + +test("CLAUDE_FAST_MODE_DEFAULT_MODELS includes claude-opus-4-8", () => { + assert.ok( + CLAUDE_FAST_MODE_DEFAULT_MODELS.includes("claude-opus-4-8"), + "claude-opus-4-8 must be in CLAUDE_FAST_MODE_DEFAULT_MODELS" + ); +}); + +test("getClaudeFastModeSupportedModels returns default list when none configured", () => { + const models = getClaudeFastModeSupportedModels({}); + assert.ok(models.includes("claude-opus-4-8")); + assert.ok(models.includes("claude-opus-4-7")); + assert.ok(models.includes("claude-opus-4-6")); +}); + +test("getClaudeFastModeSupportedModels respects custom override list", () => { + const settings = { claudeFastMode: { enabled: true, supportedModels: ["my-custom-model"] } }; + const models = getClaudeFastModeSupportedModels(settings); + assert.deepEqual(models, ["my-custom-model"]); +}); + +test("isClaudeFastModeEnabled handles boolean and object shape", () => { + assert.equal(isClaudeFastModeEnabled({ claudeFastMode: true }), true); + assert.equal(isClaudeFastModeEnabled({ claudeFastMode: false }), false); + assert.equal(isClaudeFastModeEnabled({ claudeFastMode: { enabled: true } }), true); + assert.equal(isClaudeFastModeEnabled({ claudeFastMode: { enabled: false } }), false); + assert.equal(isClaudeFastModeEnabled({}), false); +}); diff --git a/tests/unit/provider-models-config.test.ts b/tests/unit/provider-models-config.test.ts index 5628140cbb..d992860f3e 100644 --- a/tests/unit/provider-models-config.test.ts +++ b/tests/unit/provider-models-config.test.ts @@ -10,6 +10,7 @@ import { getModelsByProviderId, getProviderModels, isValidModel, + supportsXHighEffort, } from "../../open-sse/config/providerModels.ts"; test("provider models helpers expose model lists and defaults", () => { @@ -90,3 +91,14 @@ test("Kiro registry exposes the current CLI model lineup with context windows", assert.equal(byId.has("claude-sonnet-4-6"), false); assert.equal(byId.has("claude-haiku-4-5"), false); }); + +test("Claude xhigh effort support defaults on for new models and opts out legacy models", () => { + const claudeModels = new Set(getModelsByProviderId("claude").map((model) => model.id)); + + assert.ok(claudeModels.has("claude-opus-4-8")); + assert.equal(supportsXHighEffort("claude", "claude-opus-4-8"), true); + assert.equal(supportsXHighEffort("claude", "claude-opus-4-7"), true); + assert.equal(supportsXHighEffort("claude", "claude-opus-4-6"), false); + assert.equal(supportsXHighEffort("claude", "claude-sonnet-4-6"), false); + assert.equal(supportsXHighEffort("claude", "claude-future-5-0"), true); +}); diff --git a/tests/unit/t12-pricing-updates.test.ts b/tests/unit/t12-pricing-updates.test.ts index 0415ba4237..e15a907f6f 100644 --- a/tests/unit/t12-pricing-updates.test.ts +++ b/tests/unit/t12-pricing-updates.test.ts @@ -27,6 +27,10 @@ test("T12: pricing table includes MiniMax, GLM, Kimi and gpt-5.4 mini entries", assert.ok(pricing.kimi["kimi-k2.5"], "missing kimi/kimi-k2.5"); assert.ok(pricing.kimi["kimi-k2.5-thinking"], "missing kimi/kimi-k2.5-thinking"); assert.ok(pricing.kimi["kimi-for-coding"], "missing kimi/kimi-for-coding"); + + assert.ok(pricing.anthropic["claude-opus-4.8"], "missing anthropic/claude-opus-4.8"); + assert.ok(pricing.anthropic["claude-opus-4-8"], "missing anthropic/claude-opus-4-8"); + assert.ok(pricing.anthropic["claude-opus-4-7"], "missing anthropic/claude-opus-4-7"); }); test("T12: codex catalog includes GPT 5.5 variations", () => { diff --git a/tests/unit/t31-t33-t34-t38-model-specs.test.ts b/tests/unit/t31-t33-t34-t38-model-specs.test.ts index ef0175557b..ad455dec5c 100644 --- a/tests/unit/t31-t33-t34-t38-model-specs.test.ts +++ b/tests/unit/t31-t33-t34-t38-model-specs.test.ts @@ -49,6 +49,7 @@ test("T34: max output tokens are capped by model spec", () => { assert.equal(capMaxOutputTokens("gemini-3-flash", 131072), 65536); assert.equal(capMaxOutputTokens("gemini-3-flash"), 65536); assert.equal(capMaxOutputTokens("gemini-3.1-pro-high", 131072), 65535); + assert.equal(capMaxOutputTokens("claude-opus-4-8", 200000), 128000); assert.equal(capMaxOutputTokens("claude-opus-4-7", 200000), 128000); assert.equal(capMaxOutputTokens("anthropic.claude-sonnet-4-6", 200000), 64000); assert.equal(capMaxOutputTokens("eu.anthropic.claude-opus-4-6", 200000), 128000); @@ -64,6 +65,7 @@ test("T38: modelSpecs exposes centralized helpers with alias and prefix lookup", assert.equal(getModelSpec("gemini-3.1-pro-preview").maxOutputTokens, 65535); assert.equal(getModelSpec("gemini-3.1-pro-preview-customtools").maxOutputTokens, 65535); assert.equal(getModelSpec("claude-opus-4-7").contextWindow, 1000000); + assert.equal(getModelSpec("claude-opus-4.8").maxOutputTokens, 128000); assert.equal(getModelSpec("claude-opus-4.7").maxOutputTokens, 128000); assert.equal(getModelSpec("bedrock/eu.anthropic.claude-sonnet-4-6").contextWindow, 1000000); assert.equal(getModelSpec("bedrock/anthropic.claude-sonnet-4-5").contextWindow, 200000); diff --git a/tests/unit/translator-openai-to-claude.test.ts b/tests/unit/translator-openai-to-claude.test.ts index 579316bba3..36ab95f3f2 100644 --- a/tests/unit/translator-openai-to-claude.test.ts +++ b/tests/unit/translator-openai-to-claude.test.ts @@ -11,12 +11,15 @@ const { const { CLAUDE_SYSTEM_PROMPT } = await import("../../open-sse/config/constants.ts"); const { DEFAULT_THINKING_CLAUDE_SIGNATURE } = await import("../../open-sse/config/defaultThinkingSignature.ts"); -const { getModelsByProviderId } = await import("../../open-sse/config/providerModels.ts"); +const { getModelsByProviderId, supportsXHighEffort } = + await import("../../open-sse/config/providerModels.ts"); function getClaudeEffortFixtures() { const claudeModels = getModelsByProviderId("claude"); - const xhighModel = claudeModels.find((model) => model.supportsXHighEffort === true); - const standardModel = claudeModels.find((model) => model.supportsXHighEffort === false); + const xhighModel = claudeModels.find((model) => supportsXHighEffort("claude", model.id)); + const standardModel = claudeModels.find( + (model) => supportsXHighEffort("claude", model.id) === false + ); assert.ok(xhighModel, "expected at least one Claude model with xhigh support"); assert.ok(standardModel, "expected at least one Claude model without xhigh support"); return { xhighModel, standardModel };