import type { ProviderModelConfig } from "@earendil-works/pi-coding-agent"; import type { ThinkingLevelMap } from "@earendil-works/pi-ai"; /** * Chat model config. Pi 1.0 widened ProviderModelConfig into a chat/image/classifier * union; Pi 0.87 has a single chat shape. Extract resolves to the chat shape on both. */ export type ChatModelConfig = Extract; // Reviewed fallback metadata. Availability comes from /v1/models when reachable. // Sources: https://parley-docs.mit.edu/{models,cost-guidance,cli-tools/opencode} export const BUNDLED_METADATA_DATE = "2026-09-29"; const BUDGET_THINKING_LEVEL_MAP: ThinkingLevelMap = { minimal: "minimal", low: "low", medium: "medium", high: "high", xhigh: "xhigh", }; const OPENAI_REASONING_EFFORT_MAP: ThinkingLevelMap = { minimal: "minimal", low: "low", medium: "medium", high: "high", }; // GPT-6 Astra accepts low | medium | high | xhigh | max. const ASTRA_REASONING_EFFORT_MAP: ThinkingLevelMap = { minimal: "low", low: "low", medium: "medium", high: "high", xhigh: "xhigh", max: "max", }; const cost = (input: number, output: number, cacheRead = 0, cacheWrite = 0) => ({ input, output, cacheRead, cacheWrite, }); const model = ( id: string, name: string, options: { reasoning?: boolean; thinkingLevelMap?: ThinkingLevelMap; inputCost: number; outputCost: number; cacheRead?: number; cacheWrite?: number; contextWindow: number; maxTokens: number; }, ) => ({ id, name, reasoning: options.reasoning ?? false, ...(options.reasoning ? { thinkingLevelMap: options.thinkingLevelMap ?? BUDGET_THINKING_LEVEL_MAP } : {}), input: ["text", "image"] as ("text" | "image")[], cost: cost(options.inputCost, options.outputCost, options.cacheRead ?? 0, options.cacheWrite ?? 0), contextWindow: options.contextWindow, maxTokens: options.maxTokens, }); export const BUNDLED_MODELS: ChatModelConfig[] = [ model("openai/gpt-5-nano", "GPT-5 Nano", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.10, outputCost: 0.50, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5-mini", "GPT-5 Mini", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.25, outputCost: 2.00, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5", "GPT-5", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 1.25, outputCost: 10.00, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5.1", "GPT-5.1", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 1.25, outputCost: 10.00, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5.2", "GPT-5.2", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 1.25, outputCost: 10.00, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5.4", "GPT-5.4", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 2.50, outputCost: 15.00, cacheRead: 0.25, contextWindow: 1000000, maxTokens: 128000 }), model("openai/gpt-5.4-mini", "GPT-5.4 Mini", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.75, outputCost: 4.50, cacheRead: 0.075, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5.4-nano", "GPT-5.4 Nano", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.20, outputCost: 1.25, cacheRead: 0.02, contextWindow: 400000, maxTokens: 128000 }), model("openai/gpt-5.5", "GPT-5.5", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 5.00, outputCost: 30.00, cacheRead: 0.5, contextWindow: 1000000, maxTokens: 128000 }), model("openai/gpt-5.6-luna", "GPT-5.6 Luna", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.20, outputCost: 1.20, cacheRead: 0.02, cacheWrite: 0.25, contextWindow: 1050000, maxTokens: 128000 }), model("openai/gpt-5.6-terra", "GPT-5.6 Terra", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 2.00, outputCost: 12.00, cacheRead: 0.2, cacheWrite: 2.5, contextWindow: 1050000, maxTokens: 128000 }), model("openai/gpt-5.6-sol", "GPT-5.6 Sol", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 4.00, outputCost: 20.00, cacheRead: 0.4, cacheWrite: 5.0, contextWindow: 1050000, maxTokens: 128000 }), model("openai/gpt-6-astra", "GPT-6 Astra", { reasoning: true, thinkingLevelMap: ASTRA_REASONING_EFFORT_MAP, inputCost: 10.00, outputCost: 50.00, cacheRead: 1.0, cacheWrite: 12.5, contextWindow: 922000, maxTokens: 128000 }), model("openai/gpt-6-sol", "GPT-6 Sol", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 2.00, outputCost: 10.00, cacheRead: 0.2, cacheWrite: 2.5, contextWindow: 922000, maxTokens: 128000 }), model("openai/gpt-6-luna", "GPT-6 Luna", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 0.10, outputCost: 0.50, cacheRead: 0.01, cacheWrite: 0.125, contextWindow: 1050000, maxTokens: 128000 }), // GPT-5.3 Codex is not on Parley's cost guidance table; rate copied from GPT-5. model("openai/gpt-5.3-codex", "GPT-5.3 Codex", { reasoning: true, thinkingLevelMap: OPENAI_REASONING_EFFORT_MAP, inputCost: 1.25, outputCost: 10.00, contextWindow: 400000, maxTokens: 128000 }), model("bedrock/claude-haiku-4-5", "Claude Haiku 4.5", { reasoning: true, inputCost: 1.00, outputCost: 5.00, cacheRead: 0.10, cacheWrite: 2.00, contextWindow: 200000, maxTokens: 64000 }), model("bedrock/claude-sonnet-4-6", "Claude Sonnet 4.6", { reasoning: true, inputCost: 3.00, outputCost: 15.00, cacheRead: 0.30, cacheWrite: 6.00, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-sonnet-5", "Claude Sonnet 5", { reasoning: true, inputCost: 2.00, outputCost: 10.00, cacheRead: 0.2, cacheWrite: 4.0, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-sonnet-5-5", "Claude Sonnet 5.5", { reasoning: true, inputCost: 2.00, outputCost: 10.00, cacheRead: 0.2, cacheWrite: 4.0, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-opus-4-7", "Claude Opus 4.7", { reasoning: true, inputCost: 5.00, outputCost: 25.00, cacheRead: 0.50, cacheWrite: 10.00, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-opus-4-8", "Claude Opus 4.8", { reasoning: true, inputCost: 5.00, outputCost: 25.00, cacheRead: 0.50, cacheWrite: 10.00, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-opus-5", "Claude Opus 5", { reasoning: true, inputCost: 5.00, outputCost: 25.00, cacheRead: 0.50, cacheWrite: 10.00, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/claude-opus-5-5", "Claude Opus 5.5", { reasoning: true, inputCost: 4.00, outputCost: 20.00, cacheRead: 0.20, cacheWrite: 8.00, contextWindow: 1000000, maxTokens: 128000 }), model("bedrock/llama-4-maverick-17b", "Llama 4 Maverick", { inputCost: 0.00, outputCost: 0.00, contextWindow: 1000000, maxTokens: 8000 }), model("google/gemini-2.5-pro", "Gemini 2.5 Pro", { reasoning: true, inputCost: 2.50, outputCost: 15.00, contextWindow: 1000000, maxTokens: 64000 }), model("google/gemini-3.0-flash", "Gemini 3.0 Flash", { reasoning: true, inputCost: 0.50, outputCost: 3.00, contextWindow: 200000, maxTokens: 32000 }), model("google/gemini-3.1-pro", "Gemini 3.1 Pro", { reasoning: true, inputCost: 2.00, outputCost: 12.00, contextWindow: 1000000, maxTokens: 64000 }), // Gemini 3.6/3.7/3.8 Flash are on an introductory rate through 31 Dec 2026 (then $1.50 / $7.50). model("google/gemini-3.5-flash", "Gemini 3.5 Flash", { reasoning: true, inputCost: 1.50, outputCost: 9.00, contextWindow: 1000000, maxTokens: 64000 }), model("google/gemini-3.5-flash-lite", "Gemini 3.5 Flash-Lite", { reasoning: true, inputCost: 0.30, outputCost: 2.50, contextWindow: 1000000, maxTokens: 64000 }), model("google/gemini-3.6-flash", "Gemini 3.6 Flash", { reasoning: true, inputCost: 0.75, outputCost: 3.75, contextWindow: 1000000, maxTokens: 64000 }), model("google/gemini-3.7-flash", "Gemini 3.7 Flash", { reasoning: true, inputCost: 0.75, outputCost: 3.75, contextWindow: 1000000, maxTokens: 64000 }), model("google/gemini-3.8-flash", "Gemini 3.8 Flash", { reasoning: true, inputCost: 0.75, outputCost: 3.75, contextWindow: 1000000, maxTokens: 64000 }), ]; export function usesAdaptiveThinking(id: string): boolean { const match = /^bedrock\/claude-(opus|sonnet)-(\d+)(?:-(\d+))?/.exec(id); if (!match) return false; const major = Number(match[2]), minor = Number(match[3] ?? 0); return match[1] === "sonnet" ? major >= 5 : major > 4 || (major === 4 && minor >= 7); } /** Restrict auto-registration to chat families our streaming adapter understands. */ export function canonicalModelId(id: string, owner?: string): string | undefined { const bare = id.includes("/") ? id.split("/")[1] : id; let provider: string; if (/^claude-(?:haiku|sonnet|opus)-\d+(?:-\d+)*$/.test(bare)) provider = "bedrock"; else if (/^gpt-\d+(?:\.\d+)?(?:-(?:mini|nano|codex|luna|terra|sol|astra))?$/.test(bare)) provider = "openai"; else if (/^gemini-\d+(?:\.\d+)?-(?:pro|flash)(?:-lite)?$/.test(bare)) provider = "google"; else if (bare === "llama-4-maverick-17b") provider = "bedrock"; else return undefined; if (id.includes("/") && id !== `${provider}/${bare}`) return undefined; if (owner && owner !== provider) return undefined; return `${provider}/${bare}`; } /** A family template is a compatibility estimate, not verified model metadata. */ export function modelTemplate(id: string): ChatModelConfig | undefined { const exact = BUNDLED_MODELS.find(m => m.id === id); if (exact) return structuredClone(exact); let templateId: string; if (id.startsWith("bedrock/claude-opus-")) templateId = "bedrock/claude-opus-5-5"; else if (id.startsWith("bedrock/claude-sonnet-")) templateId = "bedrock/claude-sonnet-5-5"; else if (id.startsWith("bedrock/claude-haiku-")) templateId = "bedrock/claude-haiku-4-5"; else if (id.startsWith("openai/")) { const tier = /-(luna|terra|sol|astra|mini|nano)$/.exec(id)?.[1]; templateId = tier === "astra" ? "openai/gpt-6-astra" : tier === "luna" || tier === "sol" ? `openai/gpt-6-${tier}` : tier === "terra" ? "openai/gpt-5.6-terra" : tier === "mini" || tier === "nano" ? `openai/gpt-5.4-${tier}` : "openai/gpt-5.5"; } else if (id.startsWith("google/")) { templateId = id.endsWith("-lite") ? "google/gemini-3.5-flash-lite" : id.endsWith("-pro") ? "google/gemini-3.1-pro" : "google/gemini-3.8-flash"; } else return undefined; const template = structuredClone(BUNDLED_MODELS.find(m => m.id === templateId)!); // Avoid inflating an unverified context/output limit to the latest flagship's limits. return { ...template, id, contextWindow: 128000, maxTokens: 8192 }; } export function applyDocumentedCostRules(model: ProviderModelConfig, now = Date.now()): void { // Published long-context surcharges apply to the whole request, not just excess tokens. const c = model.cost; if (/^openai\/gpt-(?:6-|5\.6-|5\.(?:4|5)$)/.test(model.id)) { c.tiers = [{ inputTokensAbove: 272000, input: c.input * 2, output: c.output * 1.5, cacheRead: c.cacheRead * 2, cacheWrite: c.cacheWrite * 2 }]; } else if (model.id === "google/gemini-3.1-pro") { c.tiers = [{ inputTokensAbove: 200000, input: 4, output: 18, cacheRead: 0, cacheWrite: 0 }]; } if (/^google\/gemini-3\.[678]-flash$/.test(model.id) && now >= Date.UTC(2027, 0, 1) && c.input === 0.75 && c.output === 3.75) { c.input = 1.5; c.output = 7.5; } }