fix(llm): Opus 4.8 support — adaptive-only thinking, correct headers, pricing verified

- Force adaptive thinking for Opus 4.7+/4.8 (type:"enabled" returns 400)
- Skip interleaved-thinking beta header for adaptive mode (auto-enabled per docs)
- Fix degradeProviderOptions to use adaptive + effort:low for Opus 4.7+
- Fix compat-reasoning opencode-zen path for adaptive-only models
- Fix copilot context windows: opus-4.6/4.7 → 1M, sonnet-4.6 → 1M
- Add isAdaptiveOnly() helper to model-id.ts
- Update provider count tests for NIM provider (21 → 22)

Co-Authored-By: SoulForge <[email protected]>
This commit is contained in:
PouiiroandSoulForge committed 2026-05-30 19:21:13 +02:00
1 parent fe548ad3fc
commit 6a2b6cc325
14 files changed
+65 -17

No files matched your search

+5
View File
@@ -4,6 +4,7 @@
* Each provider's createModel imports from here directly. */
import type { AppConfig } from "../../types/index.js";
import { isAdaptiveOnly } from "./model-id.js";
function parseProvider(modelId: string): { provider: string; model: string } {
const slash = modelId.indexOf("/");
@@ -62,6 +63,10 @@ export function getCompatReasoningBody(
// Zen's normaliser forwards the OpenAI-compat /v1/chat/completions request
// to upstream Anthropic which expects { thinking: { type, budget_tokens } }.
if (isClaude && provider === "opencode-zen") {
// Opus 4.7+ only supports adaptive thinking — type:"enabled" returns 400.
if (isAdaptiveOnly(base)) {
return { thinking: { type: "adaptive" } };
}
const explicitBudget = config.thinking?.budgetTokens;
const budget =
explicitBudget ?? { low: 2048, medium: 5000, high: 10000, xhigh: 20000 }[effort] ?? 5000;
+7
View File
@@ -40,3 +40,10 @@ export function supportsTemperature(modelId: string): boolean {
if (!v) return true;
return v.major < 5 && (v.major < 4 || v.minor < 7);
}
/** Opus 4.7+ only supports adaptive thinking — rejects type:"enabled" with budget_tokens. */
export function isAdaptiveOnly(modelId: string): boolean {
const base = extractBaseModel(modelId);
const v = parseOpusVersion(base);
if (!v) return false;
return v.major >= 5 || (v.major === 4 && v.minor >= 7);
}
+14 -6
View File
@@ -3,6 +3,7 @@ import type { AppConfig, ContextManagementConfig, EffortLevel } from "../../type
import {
extractBaseModel as _leafExtractBaseModel,
getModelId as _leafGetModelId,
isAdaptiveOnly as _leafIsAdaptiveOnly,
parseOpusVersion as _leafParseOpusVersion,
supportsTemperature as _leafSupportsTemperature,
} from "./model-id.js";
@@ -10,6 +11,7 @@ import { getModelContextWindow } from "./models.js";
import { getProvider } from "./providers/index.js";
const parseOpusVersion = _leafParseOpusVersion;
const isAdaptiveOnly = _leafIsAdaptiveOnly;
export const extractBaseModel = _leafExtractBaseModel;
export const getModelId = _leafGetModelId;
@@ -647,7 +649,9 @@ async function buildAnthropicOptions(
if (caps.thinking) {
const mode = config.thinking?.mode ?? "off";
if (mode === "auto" || mode === "adaptive") {
const adaptiveOnly = isAdaptiveOnly(modelId);
if (mode === "auto" || mode === "adaptive" || (mode === "enabled" && adaptiveOnly)) {
// Opus 4.7+ only supports adaptive thinking — type:"enabled" returns 400.
if (caps.adaptiveThinking) {
opts.thinking = { type: "adaptive" };
thinkingEnabled = true;
@@ -690,7 +694,9 @@ async function buildAnthropicOptions(
}
}
if (thinkingEnabled && caps.interleavedThinking) {
// Interleaved thinking: adaptive mode enables it automatically (no header needed).
// Manual mode on Sonnet 4.6 still requires the beta header.
if (thinkingEnabled && caps.interleavedThinking && opts.thinking?.type !== "adaptive") {
headers["anthropic-beta"] = "interleaved-thinking-2025-05-14";
}
@@ -846,7 +852,9 @@ export function degradeProviderOptions(modelId: string, level: number): Provider
if (caps.anthropicOptions && caps.thinking) {
// biome-ignore lint/suspicious/noExplicitAny: ProviderOptions inner shape is parsed by Zod at runtime
const opts: Record<string, any> = { thinking: { type: "enabled", budgetTokens: 5_000 } };
const opts: Record<string, any> = isAdaptiveOnly(modelId)
? { thinking: { type: "adaptive" }, effort: "low" }
: { thinking: { type: "enabled", budgetTokens: 5_000 } };
providerOptions.anthropic = opts;
}
@@ -873,9 +881,9 @@ export function degradeProviderOptions(modelId: string, level: number): Provider
}
if (caps.bedrockOptions && caps.thinking) {
providerOptions.bedrock = {
reasoningConfig: { type: "enabled", budgetTokens: 5_000 },
};
providerOptions.bedrock = isAdaptiveOnly(modelId)
? { reasoningConfig: { type: "adaptive", maxReasoningEffort: "low" } }
: { reasoningConfig: { type: "enabled", budgetTokens: 5_000 } };
}
return {
+2
View File
@@ -52,6 +52,7 @@ export const anthropic: ProviderDefinition = {
},
fallbackModels: [
{ 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" },
@@ -63,6 +64,7 @@ export const anthropic: ProviderDefinition = {
],
contextWindows: [
["claude-opus-4-8", 1_000_000],
["claude-opus-4-7", 1_000_000],
["claude-opus-4-6", 1_000_000],
["claude-sonnet-4-6", 1_000_000],
+5
View File
@@ -31,6 +31,7 @@ export const bedrock: ProviderDefinition = {
},
fallbackModels: [
{ id: "anthropic.claude-opus-4-8", name: "Claude Opus 4.8" },
{ id: "anthropic.claude-sonnet-4-20250514-v1:0", name: "Claude Sonnet 4" },
{ id: "anthropic.claude-haiku-4-5-20251001-v1:0", name: "Claude Haiku 4.5" },
{ id: "us.amazon.nova-pro-v1:0", name: "Amazon Nova Pro" },
@@ -39,6 +40,10 @@ export const bedrock: ProviderDefinition = {
],
contextWindows: [
["claude-opus-4-8", 1_000_000],
["claude-opus-4-7", 1_000_000],
["claude-opus-4-6", 1_000_000],
["claude-sonnet-4-6", 1_000_000],
["claude-sonnet-4", 200_000],
["claude-opus-4", 200_000],
["claude-haiku-4", 200_000],
+8 -4
View File
@@ -209,8 +209,10 @@ export const copilot: ProviderDefinition = {
},
fallbackModels: [
{ id: "claude-opus-4.6", name: "Claude Opus 4.6", contextWindow: 200_000 },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", contextWindow: 200_000 },
{ id: "claude-opus-4.8", name: "Claude Opus 4.8", contextWindow: 1_000_000 },
{ id: "claude-opus-4.7", name: "Claude Opus 4.7", contextWindow: 1_000_000 },
{ id: "claude-opus-4.6", name: "Claude Opus 4.6", contextWindow: 1_000_000 },
{ id: "claude-sonnet-4.6", name: "Claude Sonnet 4.6", contextWindow: 1_000_000 },
{ id: "claude-sonnet-4", name: "Claude Sonnet 4", contextWindow: 128_000 },
{ id: "claude-opus-4.5", name: "Claude Opus 4.5", contextWindow: 200_000 },
{ id: "claude-sonnet-4.5", name: "Claude Sonnet 4.5", contextWindow: 200_000 },
@@ -226,8 +228,10 @@ export const copilot: ProviderDefinition = {
],
contextWindows: [
["claude-opus-4.6", 200_000],
["claude-sonnet-4.6", 200_000],
["claude-opus-4.8", 1_000_000],
["claude-opus-4.7", 1_000_000],
["claude-opus-4.6", 1_000_000],
["claude-sonnet-4.6", 1_000_000],
["claude-opus-4.5", 200_000],
["claude-sonnet-4.5", 200_000],
["claude-sonnet-4", 128_000],
+2
View File
@@ -33,6 +33,7 @@ export const llmgateway: ProviderDefinition = {
},
fallbackModels: [
{ 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" },
@@ -63,6 +64,7 @@ export const llmgateway: ProviderDefinition = {
// Specific overrides first → shared patterns → generic catch-alls last.
contextWindows: [
// Claude (LLM Gateway uses hyphens: claude-opus-4-6)
["claude-opus-4-8", 1_000_000],
["claude-opus-4-7", 1_000_000],
["claude-opus-4-6", 1_000_000],
["claude-sonnet-4-6", 200_000],
+3
View File
@@ -72,6 +72,7 @@ export const opencodeZen: ProviderDefinition = {
{ id: "gpt-5", name: "GPT 5" },
{ id: "gpt-5-codex", name: "GPT 5 Codex" },
{ id: "gpt-5-nano", name: "GPT 5 Nano" },
{ 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-opus-4-5", name: "Claude Opus 4.5" },
@@ -133,6 +134,7 @@ export const opencodeZen: ProviderDefinition = {
["gpt-5-codex", 400_000],
["gpt-5-nano", 400_000],
// Claude (hyphen-style on Zen)
["claude-opus-4-8", 1_000_000],
["claude-opus-4-7", 1_000_000],
["claude-opus-4-6", 1_000_000],
["claude-opus-4-5", 200_000],
@@ -142,6 +144,7 @@ export const opencodeZen: ProviderDefinition = {
["claude-sonnet-4", 200_000],
["claude-haiku-4-5", 200_000],
// also tolerate dot-style if returned
["claude-opus-4.8", 1_000_000],
["claude-opus-4.6", 1_000_000],
["claude-sonnet-4.6", 1_000_000],
["claude-sonnet-4.5", 1_000_000],
+3
View File
@@ -28,6 +28,7 @@ export const openrouter: ProviderDefinition = {
},
fallbackModels: [
{ id: "anthropic/claude-opus-4.8", name: "Claude Opus 4.8" },
{ id: "anthropic/claude-opus-4.7", name: "Claude Opus 4.7" },
{ id: "anthropic/claude-opus-4.6", name: "Claude Opus 4.6" },
{ id: "anthropic/claude-sonnet-4.6", name: "Claude Sonnet 4.6" },
@@ -68,6 +69,8 @@ export const openrouter: ProviderDefinition = {
// Specific overrides must NOT be substrings of shared patterns (ordering matters).
contextWindows: [
// Claude (OpenRouter uses dots: claude-opus-4.6)
["claude-opus-4.8", 1_000_000],
["claude-opus-4.7", 1_000_000],
["claude-opus-4.6", 1_000_000],
["claude-sonnet-4.6", 1_000_000],
["claude-sonnet-4.5", 1_000_000],
+3
View File
@@ -66,6 +66,7 @@ export const proxy: ProviderDefinition = {
},
fallbackModels: [
{ 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" },
@@ -79,6 +80,8 @@ export const proxy: ProviderDefinition = {
// Specific overrides first → shared patterns → generic catch-alls last.
contextWindows: [
// Claude (both dot/hyphen styles)
["claude-opus-4-8", 1_000_000],
["claude-opus-4.8", 1_000_000],
["claude-opus-4-7", 1_000_000],
["claude-opus-4.7", 1_000_000],
["claude-opus-4-6", 1_000_000],
+8 -2
View File
@@ -47,6 +47,7 @@ const MODEL_PRICING: Record<string, ModelPricing> = {
// cacheWrite = 5-min TTL (1.25× base). 1h TTL would be 2× base; SDK doesn't
// expose which TTL was used, and we always request the 5m default.
// cacheRead = 0.1× base input.
"claude-opus-4-8": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
"claude-opus-4-7": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
"claude-opus-4-6": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
"claude-opus-4-5": { input: 5, cacheWrite: 6.25, cacheRead: 0.5, output: 25 },
@@ -186,9 +187,14 @@ const COPILOT_PRICING: Record<string, ModelPricing> = {
"claude-opus-4.6": M3,
// multiplier 7.5
"claude-opus-4.7": M75,
"claude-opus-4.8": M75,
// multiplier 7.5
"gpt-5.5": M75,
// multiplier 30 (Opus 4.6 fast mode preview)
// multiplier 10 (Opus 4.8 fast mode)
"claude-opus-4.8-fast": { input: 20, cacheWrite: 20, cacheRead: 2, output: 100 },
// multiplier 30 (Opus 4.6/4.7 fast mode)
"claude-opus-4.6-fast": M30,
"claude-opus-4.7-fast": M30,
};
function matchCopilotPricing(model: string): ModelPricing | undefined {
@@ -269,7 +275,7 @@ function matchPricing(modelId: string): ModelPricing {
if (id.includes(key)) return pricing;
}
// Fallback heuristics for unknown variants / OpenRouter prefixed IDs
if (id.includes("opus")) return MODEL_PRICING["claude-opus-4-6"] ?? DEFAULT_PRICING;
if (id.includes("opus")) return MODEL_PRICING["claude-opus-4-8"] ?? DEFAULT_PRICING;
if (id.includes("sonnet")) return DEFAULT_PRICING;
if (id.includes("haiku")) return MODEL_PRICING["claude-haiku-4-5"] ?? DEFAULT_PRICING;
if (id.includes("gemini")) return MODEL_PRICING["gemini-2.5-flash"] ?? DEFAULT_PRICING;
+1 -1
View File
@@ -23,7 +23,7 @@ const BASE_OPTIONS: LanguageModelV2CallOptions = {
describe("codex provider", () => {
test("is registered as a builtin provider", () => {
expect(getProvider("codex")).toBeDefined();
expect(getAllProviders().filter((provider) => !provider.custom)).toHaveLength(21);
expect(getAllProviders().filter((provider) => !provider.custom)).toHaveLength(22);
});
test("has provider metadata, fallback models, and shared auth hook", () => {
+2 -2
View File
@@ -14,9 +14,9 @@ describe("local providers registration", () => {
expect(getProvider("lmstudio")!.custom).toBeUndefined();
});
test("total builtin count is 21", () => {
test("total builtin count is 22", () => {
const builtins = getAllProviders().filter((p) => !p.custom);
expect(builtins.length).toBe(21);
expect(builtins.length).toBe(22);
});
test("ollama has required fields", () => {
+2 -2
View File
@@ -37,8 +37,8 @@ describe("new providers registration", () => {
test("total builtin count increased by 5", () => {
const builtins = getAllProviders().filter((p) => !p.custom);
// Was 13, then 18 after the previous provider batch, now 21 with Codex + OpenCode.
expect(builtins.length).toBe(21);
// Was 13, then 18 after the previous provider batch, now 22 with Codex + OpenCode + NIM.
expect(builtins.length).toBe(22);
});
});