diff --git a/extensions/levelcode-ai/providers/catalog.js b/extensions/levelcode-ai/providers/catalog.js index ac3b554..3ee6c76 100644 --- a/extensions/levelcode-ai/providers/catalog.js +++ b/extensions/levelcode-ai/providers/catalog.js @@ -57,7 +57,17 @@ const CAPS = { // warns "full" 5× too early (the roster feeds the PICKER's ctx, but the meter reads this local CAPS). // No `fast` (a 2.8T reasoning model is a poor ghost-text completer). tools:true matches its agentic // intent; flip to false ONLY if the reasoning-loop spike (docs/KIMI-K3.md §3) shows it breaks the loop. - 'moonshotai/kimi-k3': { context: 1048576, tools: true, reasoning: true } + 'moonshotai/kimi-k3': { context: 1048576, tools: true, reasoning: true }, + // Same reason as K3: a 1M window that the `claude-` heuristic would otherwise report as 200K, + // making the in-run context meter cry "full" 5× too early. Listed under its OpenRouter slug + // because that is how it is reached (BYOK here, and the gateway roster uses the same id). + 'anthropic/claude-opus-5': { context: 1000000, tools: true, vision: true, caching: true }, + // Opus 4.8 is ALSO 1M — but only stated here under the OpenRouter slug, where every endpoint + // (Anthropic first-party, Bedrock, Azure, Google) advertises 1M/128K. The bare `claude-opus-4-8` + // row above stays at 200K on purpose: that key serves the DIRECT Anthropic provider, and this + // extension sends no `anthropic-beta` header, so the long window is not confirmed on that route. + // Exact-id lookup beats basename, so the two coexist and each route reports its own truth. + 'anthropic/claude-opus-4-8': { context: 1000000, tools: true, vision: true, caching: true } }; /** The basename of a model id ('openai/gpt-4o' → 'gpt-4o', 'gpt-4o' → 'gpt-4o'). */ diff --git a/extensions/levelcode-ai/providers/index.js b/extensions/levelcode-ai/providers/index.js index 44b7826..d9d7f51 100644 --- a/extensions/levelcode-ai/providers/index.js +++ b/extensions/levelcode-ai/providers/index.js @@ -45,6 +45,7 @@ const PROVIDERS = { caps: { tools: true, vision: true }, headers: { 'HTTP-Referer': 'https://levelcode.ai', 'X-Title': 'LevelCode' }, models: [ { id: 'moonshotai/kimi-k2.7-code', label: 'Kimi K2.7 Code', detail: 'Moonshot · coding, 262K · via OpenRouter' }, + { id: 'anthropic/claude-opus-5', label: 'Claude Opus 5', detail: 'Anthropic · frontier, 1M · via OpenRouter' }, { id: 'anthropic/claude-sonnet-4-6', label: 'Claude Sonnet 4.6', detail: 'via OpenRouter' }, { id: 'openai/gpt-4o', label: 'GPT-4o', detail: 'via OpenRouter' }, { id: 'google/gemini-2.0-flash-001', label: 'Gemini 2.0 Flash', detail: 'via OpenRouter' }, diff --git a/extensions/levelcode-ai/sketch.js b/extensions/levelcode-ai/sketch.js index 4b37a57..17b5444 100644 --- a/extensions/levelcode-ai/sketch.js +++ b/extensions/levelcode-ai/sketch.js @@ -41,7 +41,10 @@ const MODEL_TIERS = { openrouter: { fast: "google/gemini-2.0-flash-001", balanced: "anthropic/claude-sonnet-4-6", - powerful: "anthropic/claude-opus-4-8", + // Opus 5 costs exactly what 4.8 did ($5/$25), so "powerful" gets the newer model for the same + // money. It is also in the OpenRouter registry now, which the old id never was — so a per-node + // model override naming it now validates instead of silently falling back to the tier default. + powerful: "anthropic/claude-opus-5", }, groq: { fast: "llama-3.1-8b-instant", diff --git a/extensions/levelcode-ai/sketch/pricing.js b/extensions/levelcode-ai/sketch/pricing.js index 32fb724..d4d4876 100644 --- a/extensions/levelcode-ai/sketch/pricing.js +++ b/extensions/levelcode-ai/sketch/pricing.js @@ -19,6 +19,11 @@ const { baseName } = require('../providers/catalog'); const PRICING = { // Anthropic (published) 'claude-opus-4-8': { inM: 5.00, outM: 25.00 }, + // Opus 5 ties 4.8's sheet, so the `claude-opus` family fallback below already gets it right — but + // the FAST variant is double ($10/$50), and that fallback would silently under-report it by 2×. + // Pinning both keeps the cost meter honest for whichever slug a BYOK user types. + 'claude-opus-5': { inM: 5.00, outM: 25.00 }, + 'claude-opus-5-fast': { inM: 10.00, outM: 50.00 }, 'claude-sonnet-4-6': { inM: 3.00, outM: 15.00 }, 'claude-haiku-4-5': { inM: 1.00, outM: 5.00 }, 'claude-haiku-4-5-20251001': { inM: 1.00, outM: 5.00 }, diff --git a/extensions/levelcode-ai/test/catalog.test.js b/extensions/levelcode-ai/test/catalog.test.js index 31216b7..30aaefd 100644 --- a/extensions/levelcode-ai/test/catalog.test.js +++ b/extensions/levelcode-ai/test/catalog.test.js @@ -61,6 +61,14 @@ test('contextWindowFor: known model wins; fallback then 200000 when unknown', () assert.strictEqual(C.contextWindowFor('openai', 'unknown-x'), 200000); // Gateway Kimi K3 — its 1M window must beat the 200000 fallback, or the meter warns 5× too early. assert.strictEqual(C.contextWindowFor('openai', 'moonshotai/kimi-k3', 200000), 1048576); + // Same trap for Opus 5: the `claude-` heuristic would hand back 200K and shrink a 1M window. + assert.strictEqual(C.contextWindowFor('openai', 'anthropic/claude-opus-5', 200000), 1000000); + // Opus 4.8 is route-dependent, and exact-id lookup is what keeps the two apart: the OpenRouter + // slug is a confirmed 1M, while the bare id (direct Anthropic, no beta header sent) stays 200K. + assert.strictEqual(C.contextWindowFor('openrouter', 'anthropic/claude-opus-4-8', 200000), 1000000); + assert.strictEqual(C.contextWindowFor('claude', 'claude-opus-4-8', 200000), 200000); + // Claude ids we DON'T list still fall to the heuristic's 200K default. + assert.strictEqual(C.contextWindowFor('claude', 'claude-something-unreleased', 200000), 200000); }); // ---- fastCompletionModel ---- diff --git a/extensions/levelcode-ai/test/promptCaching.test.js b/extensions/levelcode-ai/test/promptCaching.test.js index 76fcaa3..f627647 100644 --- a/extensions/levelcode-ai/test/promptCaching.test.js +++ b/extensions/levelcode-ai/test/promptCaching.test.js @@ -94,7 +94,7 @@ test('splitOutCachedTokens: zero cached_tokens does not subtract', () => { // ---- Anthropic-family gating for OpenRouter explicit caching ---- test('isAnthropicFamily: identifies Claude upstreams on OpenRouter, IDs only Anthropic routes', () => { - for (const id of ['claude-opus-4-8', 'anthropic/claude-sonnet-4-6', 'claude-sonnet-4-6']) { + for (const id of ['claude-opus-4-8', 'anthropic/claude-sonnet-4-6', 'claude-sonnet-4-6', 'anthropic/claude-opus-5']) { assert.strictEqual(O.isAnthropicFamily(id), true, id); } for (const id of ['openai/gpt-4o', 'deepseek/deepseek-chat', 'gpt-4o', 'moonshotai/kimi-k2.7-code', '']) { diff --git a/extensions/levelcode-ai/test/sketch.test.js b/extensions/levelcode-ai/test/sketch.test.js index ee8d8dd..5a3077d 100644 --- a/extensions/levelcode-ai/test/sketch.test.js +++ b/extensions/levelcode-ai/test/sketch.test.js @@ -72,6 +72,10 @@ test('modelPricing: exact, basename (openrouter prefix), family fallback, unknow assert.strictEqual(P.modelPricing('claude-opus-4-8').inM, 5.0); assert.strictEqual(P.modelPricing('anthropic/claude-sonnet-4-6').outM, 15.0); // basename via vendor/model assert.strictEqual(P.modelPricing('claude-sonnet-4-6-preview').outM, 15.0); // family prefix + // Opus 5 matches 4.8's sheet; its FAST variant is double. Without the explicit rows the + // `claude-opus` family fallback prices the fast one at half what it costs. + assert.deepStrictEqual(P.modelPricing('anthropic/claude-opus-5'), { inM: 5.0, outM: 25.0 }); + assert.deepStrictEqual(P.modelPricing('anthropic/claude-opus-5-fast'), { inM: 10.0, outM: 50.0 }); assert.strictEqual(P.modelPricing('totally-unknown-model'), null); assert.strictEqual(P.modelPricing(''), null); });