mirror of
https://github.com/proxysoul/Empryo.git
synced 2026-11-07 06:25:57 -10:00
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:
14 files changed
+65
-17
No files matched your search
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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", () => {
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user