feat: model + thinking control — the stats bar's picker, on pi-ai's catalog

The model-control wave (user-decided design, 2026-09-12): ONE controller
for the conversation agents (prober/teacher are functionally one agent);
background sub-agents stay config-pinned. pi-ai owns the registry — the
shipped 366-model OpenRouter catalog (cost, context, modality,
thinkingLevelMap), getSupportedThinkingLevels/clampThinkingLevel for the
per-model thinking truth, and streamSimple's automatic clamp. No second
registry to drift out of sync.

Seams:
- config.ts: ReasoningEffort retired for pi's ThinkingLevel
  (off|minimal|low|medium|high|xhigh|max). GLM roles go honest at "low" —
  pi's catalog says GLM-5.3-flash accepts exactly low|high|max, and the old
  "minimal" was silently clamped to low by streamSimple since the swap;
  nothing changes behaviorally. visualMaker → qwen/qwen3.8-flash (fixes
  the dead qwen/qwen3.8-max slug — the catalog only ships -0902; also the
  user's cost pick, 0.15/0.47 per-1M).
- src/agent/models.ts: piEngineFor(role, settings?) — provider+id resolved
  from the catalog, thinking level from settings ?? config, CLAMPED to the
  model's supported list (a settings change between models can never send
  an invalid level). PiEngine gains `level` (observability/tests).
- ChatRecord.settings {model?: {provider, id}, thinkingLevel?} — written
  by PUT /api/chats/[id]/settings (replace-whole, no updatedAt churn),
  read by chat-handler at turn composition: agents are stateless per turn,
  so the change is next-turn-effective by construction. The turns log
  still records the ACTUAL billed model (the honest witness).
- GET /api/models: the configured-provider catalog as client-safe DTOs
  (core/contracts/models.ts) + config defaults; a second provider joins
  the picker by registering it in piModels() — nothing else changes.
- PUT settings validation gate (src/server/model-catalog.ts): unknown
  provider/model or unsupported level → 400 carrying the supported list,
  never a silent clamp.
- scripts/sync-pricing.ts: regenerated PRICES/CONTEXT_LIMITS from pi-ai's
  shipped openrouter.json (offline; same source as the engine + picker).
  All 366 models priced; glm's stale 0.15/0.5 → pi's 0.075/0.25, limit
  1310720 → 1048576 — cost is inferred at read time, so history re-prices.
- UI (components/model-picker.tsx): stats-bar right side becomes model +
  thinking buttons; menus open ABOVE; fuzzy search over "provider id
  name" (core/fuzzy.ts — pi's /model pattern); model rows show name/id/
  price/context; the thinking menu lists exactly the chosen model's
  supported levels (GLM: low|high|max — no dead options, no "off").
  Model switch drops an incompatible pinned level (falls back to config,
  which clamps honestly) rather than carrying a 400.

Tests (350/350, +15): catalog projection + defaults, the settings gate,
pi thinking truth pinned against upgrades (glm low|high|max; minimal→low;
off→low — GLM cannot disable thinking), the engine factory's
override/fallback/clamp/throw matrix, fuzzy ranking, store round-trip,
and the re-priced folds (usage, stats bar, activity dock).

Live smoke (disposable driver, deleted): catalog route 366 + defaults;
glm+minimal → 400 + supported; turn 1 billed qwen/qwen3.8-flash under
the override; mid-session switch flipped turn 2 to glm; clear-overrides
restored the config pin. ALL CHECKS PASS on real OpenRouter.
This commit is contained in:
amosblomqvist
2026-09-12 06:02:40 +03:00
parent 740293c361
commit de0c286311
26 changed files with 1649 additions and 131 deletions
+26 -10
View File
@@ -1,4 +1,5 @@
import {
clampThinkingLevel,
createModels,
type Api,
type Model,
@@ -8,6 +9,7 @@ import {
import { openrouterProvider } from "@earendil-works/pi-ai/providers/openrouter";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import { MODELS, type ModelRole } from "../../config.ts";
import type { ChatSettings } from "../core/contracts/chat.ts";
/**
* The one engine factory every agent's model comes from (the pi-ai swap,
@@ -25,14 +27,17 @@ import { MODELS, type ModelRole } from "../../config.ts";
export interface PiEngine {
model: Model<Api>;
streamFn: StreamFunction;
/** The resolved (config-or-settings, CLAMPED) thinking level —
* observability + tests; the streamFn pins it anyway. */
level?: ThinkingLevel;
}
export type { ThinkingLevel };
/** config's effort scale maps 1:1 onto pi's thinking levels (none→off). */
/** The role's config thinking level (config's scale IS pi's since the
* model-control wave — no mapping needed). */
export function thinkingLevelFor(role: ModelRole): ThinkingLevel {
const effort = MODELS[role].effort as string;
return effort === "none" ? "off" : (effort as ThinkingLevel);
return MODELS[role].effort;
}
let collection: ReturnType<typeof createModels> | undefined;
@@ -51,18 +56,29 @@ export function openRouterApiKey(): string | undefined {
return process.env.OPENROUTER_API_KEY || undefined;
}
/** The production engine for one agent role (config.ts MODELS). */
export function piEngineFor(role: ModelRole): PiEngine {
/** The production engine for one agent role. `settings` is the chat's
* model-control override (prober/teacher only — sub-agent callers pass
* nothing and get config.ts's pin): any absent field falls back to the
* config spec, and the thinking level is CLAMPED to the model's supported
* list (pi's thinkingLevelMap — e.g. GLM-5.3-flash accepts low|high|max),
* so a settings change between models can never send an invalid level. */
export function piEngineFor(role: ModelRole, settings?: ChatSettings): PiEngine {
const models: Models = piModels();
const spec = MODELS[role];
const model = models.getModel("openrouter", spec.model);
if (!model) throw new Error(`OpenRouter catalog has no model ${spec.model} (role ${role})`);
const level = thinkingLevelFor(role);
const modelId = settings?.model?.id ?? spec.model;
const providerId = settings?.model?.provider ?? "openrouter";
const model = models.getModel(providerId, modelId);
if (!model) {
throw new Error(`${providerId} catalog has no model ${modelId} (role ${role})`);
}
const requested = settings?.thinkingLevel ?? thinkingLevelFor(role);
const level = clampThinkingLevel(model, requested);
return {
model,
level,
// bound streamSimple: the agent loop forwards state.thinkingLevel as
// `reasoning` (undefined = off on pi-ai's scale); pin the role's level
// so a bare engine can never drift.
// `reasoning` (undefined = off on pi-ai's scale); the clamp above keeps
// the pin honest even when config or settings drift off the map.
streamFn: (requestModel, context, options) =>
models.streamSimple(requestModel, context, {
...options,
+1
View File
@@ -14,3 +14,4 @@ export * from "./contracts/graph.ts";
export * from "./contracts/chat.ts";
export * from "./contracts/jobs.ts";
export * from "./contracts/probe.ts";
export * from "./contracts/models.ts";
+24
View File
@@ -7,6 +7,7 @@
* of "ai" (safe everywhere: server, client, tests). See docs/vision.md.
*/
import type { UIMessage } from "ai";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import type { Phase3 } from "../phases.ts";
import type { PlanNodeStatus, PlanTree } from "./plan.ts";
import type { EmissionStats } from "./graph.ts";
@@ -38,6 +39,25 @@ export interface ChatRecord {
* spend too. Same parallel-log discipline as `turns` (nothing rides
* inside messages). Keyed by startedAt; idempotent appends. */
extractions?: ExtractionRunUsage[];
/** Per-chat model/thinking overrides for the conversation agents. */
settings?: ChatSettings;
}
/** The conversation agents' runtime model choice (model-control wave,
* 2026-09-12): a per-chat override of config.ts's pinned prober/teacher
* spec. Absent fields fall back to config; written by the model picker
* (PUT /api/chats/[id]/settings) and read at turn composition — the
* agents are stateless per turn, so a change takes effect on the next
* turn by construction. Background sub-agents are NEVER overridden
* (config-pinned: they're infrastructure, not conversation). */
export interface ChatSettings {
/** pi-ai catalog coordinates: the provider that owns the model. Only
* "openrouter" is configured today; the field exists so a second
* provider needs no schema change. */
model?: { provider: string; id: string };
/** pi thinking level — must be in the chosen model's supported list
* (getSupportedThinkingLevels; enforced by the settings route). */
thinkingLevel?: ThinkingLevel;
}
// ─── Background-extraction usage (the money channel, extraction side) ────────
@@ -191,6 +211,10 @@ export interface ChatSummary {
phase: Phase3;
/** An unanswered quiz/ask question is waiting for the learner. */
pendingQuestion: boolean;
/** The record's model-control settings — rides the list payload via the
* summary spread (the picker never needs it from here; the full record
* is its source). */
settings?: ChatSettings;
}
/** The sidebar's session row: a summary + live liveness. The chats list
+39
View File
@@ -0,0 +1,39 @@
/**
* Model-catalog contracts (model-control wave, 2026-09-12) — the client-safe
* shapes of GET /api/models and PUT /api/chats/[id]/settings responses.
* Pure types; the server projection lives in src/server/model-catalog.ts
* (which OWNS these shapes — the contract is defined here only so client
* components never import from the server band).
*/
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
/** One picker row: everything the UI shows about a model, projected from
* pi-ai's catalog. Costs are USD per 1M tokens (the same catalog, the same
* units core/prices.ts's generated table carries). */
export interface ModelCatalogEntry {
id: string;
name: string;
provider: string;
reasoning: boolean;
input: ("text" | "image")[];
/** The thinking levels this model actually accepts (pi's
* thinkingLevelMap — e.g. GLM-5.3-flash: low|high|max). The thinking
* picker lists exactly these; anything else is a lie. */
supportedLevels: ThinkingLevel[];
cost: { input: number; output: number; cacheRead: number; cacheWrite: number };
contextWindow: number;
maxTokens: number;
}
/** The conversation agents' config pin (config.ts MODELS prober/teacher —
* one controller, so one pin): what a chat without settings runs. */
export interface CatalogDefaults {
model: { provider: string; id: string };
thinkingLevel: ThinkingLevel;
}
/** GET /api/models response. */
export interface ModelCatalogResponse {
models: ModelCatalogEntry[];
defaults: CatalogDefaults;
}
+38
View File
@@ -0,0 +1,38 @@
/**
* Fuzzy search scoring (model-control wave, 2026-09-12) — pi's `/model`
* picker pattern: space-separated tokens, each token must match SOME field
* (case-insensitive substring), ranked so exact field matches beat
* prefixes beat buried substrings. Pure core; the picker sorts by score.
*/
/** Score one candidate, or null when any token matches no field.
* Higher is better; ties break naturally by list order (stable sort).
*
* Per-token points: exact field equality 100 · field startsWith 60 ·
* substring 30 minus a depth penalty (earlier finds win). Whole-query
* contiguity in one field adds +50 (query "glm flash" vs the id
* "z-ai/glm-5.3-flash" loses to a model whose id contains "glm flash"
* verbatim — as it should). */
export function fuzzyScore(query: string, ...fields: string[]): number | null {
const tokens = query.trim().toLowerCase().split(/\s+/).filter(Boolean);
if (tokens.length === 0) return 0;
const hay = fields.map((f) => f.toLowerCase());
let total = 0;
for (const token of tokens) {
let best = -1;
for (const f of hay) {
if (f === token) best = Math.max(best, 100);
else if (f.startsWith(token)) best = Math.max(best, 60);
else {
const at = f.indexOf(token);
if (at !== -1) best = Math.max(best, 30 - Math.min(at, 27));
}
}
if (best < 0) return null;
total += best;
}
const joined = tokens.join(" ");
if (hay.some((f) => f.includes(joined))) total += 50;
return total;
}
+1
View File
@@ -26,3 +26,4 @@ export * from "./math-delims.ts";
export * from "./ui-message.ts";
export * from "./tool-parts.ts";
export * from "./prices.ts";
export * from "./fuzzy.ts";
+4
View File
@@ -12,6 +12,7 @@
import type { UIMessage } from "ai";
import type {
ChatRecord,
ChatSettings,
ChatSummary,
GraphEdge,
GraphNode,
@@ -45,6 +46,9 @@ export interface ChatStore {
appendTurnUsage(id: string, entry: TurnUsage): void;
/** Append one extraction-run usage entry — idempotent per startedAt. */
appendExtractionUsage(id: string, entry: ExtractionRunUsage): void;
/** Persist the per-chat model/thinking settings (the model control).
* Replace-whole: the picker sends the complete settings object. */
saveSettings(id: string, settings: ChatSettings): void;
}
// ─── GraphStore — the append-only evidence ledgers (data/graph) ──────────────
+733 -3
View File
@@ -28,16 +28,746 @@ export interface ModelPrice {
}
/** Token prices, USD per 1M tokens. A missing model makes cost render
* as absent ("—"), never zero. Refresh: npx tsx scripts/sync-pricing.ts */
* as absent ("—"), never zero. Refresh: bump @earendil-works/pi-ai, then
* npx tsx scripts/sync-pricing.ts (reads pi-ai's shipped catalog — the
* same source the engine and the picker use). */
export const PRICES: Record<string, ModelPrice> = {
"z-ai/glm-5.3-flash": { input: 0.15, output: 0.5, cacheRead: 0.03 },
"~anthropic/claude-fable-latest": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
"~anthropic/claude-haiku-latest": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"~anthropic/claude-opus-latest": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"~anthropic/claude-sonnet-latest": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"~deepseek/deepseek-v4-flash-latest": { input: 0.04998, output: 0.09996, cacheRead: 0.009996, cacheWrite: 0 },
"~google/gemini-flash-latest": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0.041667 },
"~google/gemini-pro-latest": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0.375 },
"~moonshotai/kimi-latest": { input: 2.55, output: 12.75, cacheRead: 0.256, cacheWrite: 0 },
"~openai/gpt-latest": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"~openai/gpt-mini-latest": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
"~x-ai/grok-latest": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
"~z-ai/glm-flash-latest": { input: 0.075, output: 0.25, cacheRead: 0.015, cacheWrite: 0 },
"~z-ai/glm-latest": { input: 1.15, output: 3.5, cacheRead: 0.1, cacheWrite: 0 },
"aion-labs/aion-2.0": { input: 0.8, output: 1.6, cacheRead: 0.2, cacheWrite: 0 },
"aion-labs/aion-3.0": { input: 3, output: 6, cacheRead: 0.75, cacheWrite: 0 },
"aion-labs/aion-3.0-mini": { input: 0.7, output: 1.4, cacheRead: 0.18, cacheWrite: 0 },
"amazon/nova-2-lite-v1": { input: 0.3, output: 2.5, cacheRead: 0, cacheWrite: 0 },
"amazon/nova-lite-v1": { input: 0.06, output: 0.24, cacheRead: 0, cacheWrite: 0 },
"amazon/nova-micro-v1": { input: 0.035, output: 0.14, cacheRead: 0, cacheWrite: 0 },
"amazon/nova-premier-v1": { input: 2.5, output: 12.5, cacheRead: 0.625, cacheWrite: 0 },
"amazon/nova-pro-v1": { input: 0.8, output: 3.2, cacheRead: 0, cacheWrite: 0 },
"anthropic/claude-3-haiku": { input: 0.25, output: 1.25, cacheRead: 0.03, cacheWrite: 0.3 },
"anthropic/claude-fable-5": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
"anthropic/claude-fable-5:batch": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-fable-5.1": { input: 10, output: 50, cacheRead: 0.25, cacheWrite: 12.5 },
"anthropic/claude-fable-5.1:batch": { input: 5, output: 25, cacheRead: 0.125, cacheWrite: 6.25 },
"anthropic/claude-haiku-4.5": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"anthropic/claude-haiku-4.5:batch": { input: 0.5, output: 2.5, cacheRead: 0.05, cacheWrite: 0.625 },
"anthropic/claude-opus-4": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
"anthropic/claude-opus-4.1": { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
"anthropic/claude-opus-4.1:batch": { input: 7.5, output: 37.5, cacheRead: 0.75, cacheWrite: 9.375 },
"anthropic/claude-opus-4.5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-opus-4.5:batch": { input: 2.5, output: 12.5, cacheRead: 0.25, cacheWrite: 3.125 },
"anthropic/claude-opus-4.6": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-opus-4.6:batch": { input: 2.5, output: 12.5, cacheRead: 0.25, cacheWrite: 3.125 },
"anthropic/claude-opus-4.7": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-opus-4.7:batch": { input: 2.5, output: 12.5, cacheRead: 0.25, cacheWrite: 3.125 },
"anthropic/claude-opus-4.8": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-opus-4.8:batch": { input: 2.5, output: 12.5, cacheRead: 0.25, cacheWrite: 3.125 },
"anthropic/claude-opus-5": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"anthropic/claude-opus-5:batch": { input: 2.5, output: 12.5, cacheRead: 0.25, cacheWrite: 3.125 },
"anthropic/claude-sonnet-4": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
"anthropic/claude-sonnet-4.5": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
"anthropic/claude-sonnet-4.5:batch": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 1.875 },
"anthropic/claude-sonnet-4.6": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
"anthropic/claude-sonnet-4.6:batch": { input: 1.5, output: 7.5, cacheRead: 0.15, cacheWrite: 1.875 },
"anthropic/claude-sonnet-5": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"anthropic/claude-sonnet-5:batch": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"arcee-ai/trinity-large-thinking": { input: 0.25, output: 0.8, cacheRead: 0.06, cacheWrite: 0 },
"auto": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-1.6": { input: 0.25, output: 2, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-1.6-flash": { input: 0.075, output: 0.3, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-2-1-turbo": { input: 0.5, output: 2.5, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-2.0-code": { input: 0.5, output: 3, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-2.0-lite": { input: 0.25, output: 2, cacheRead: 0, cacheWrite: 0 },
"bytedance-seed/seed-2.0-mini": { input: 0.1, output: 0.4, cacheRead: 0, cacheWrite: 0 },
"cohere/command-r-08-2024": { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 },
"cohere/command-r-plus-08-2024": { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
"cohere/north-mini-code:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"deepseek/deepseek-chat": { input: 0.32, output: 0.89, cacheRead: 0, cacheWrite: 0 },
"deepseek/deepseek-chat-v3-0324": { input: 0.25, output: 1, cacheRead: 0, cacheWrite: 0 },
"deepseek/deepseek-chat-v3.1": { input: 0.55, output: 1.65, cacheRead: 0.55, cacheWrite: 0 },
"deepseek/deepseek-r1": { input: 0.7, output: 2.5, cacheRead: 0, cacheWrite: 0 },
"deepseek/deepseek-r1-0528": { input: 0.5, output: 2.15, cacheRead: 0.35, cacheWrite: 0 },
"deepseek/deepseek-v3.1-terminus": { input: 0.27, output: 1, cacheRead: 0.135, cacheWrite: 0 },
"deepseek/deepseek-v3.2": { input: 0.269, output: 0.4, cacheRead: 0.1345, cacheWrite: 0 },
"deepseek/deepseek-v3.2-exp": { input: 0.27, output: 0.41, cacheRead: 0, cacheWrite: 0 },
"deepseek/deepseek-v4-flash": { input: 0.08526, output: 0.17052, cacheRead: 0.017052, cacheWrite: 0 },
"deepseek/deepseek-v4-flash-0731": { input: 0.065, output: 0.18, cacheRead: 0.016, cacheWrite: 0 },
"deepseek/deepseek-v4-flash-0731:batch": { input: 0.14, output: 0.28, cacheRead: 0.03, cacheWrite: 0 },
"deepseek/deepseek-v4-flash-vision-exp": { input: 0.22, output: 0.66, cacheRead: 0.007, cacheWrite: 0 },
"deepseek/deepseek-v4-pro": { input: 0.890358, output: 1.780716, cacheRead: 0.074196, cacheWrite: 0 },
"deepseek/deepseek-v4-pro-0813": { input: 1.12068, output: 3.36204, cacheRead: 0.037356, cacheWrite: 0 },
"deepseek/deepseek-v4-pro-0813:batch": { input: 1.32, output: 3.96, cacheRead: 0.13, cacheWrite: 0 },
"dots-studio/dots-3-note-preview:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"google/gemini-2.5-flash": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.083333 },
"google/gemini-2.5-flash-lite": { input: 0.1, output: 0.4, cacheRead: 0.01, cacheWrite: 0.083333 },
"google/gemini-2.5-flash-lite:batch": { input: 0.05, output: 0.2, cacheRead: 0.01, cacheWrite: 0 },
"google/gemini-2.5-flash:batch": { input: 0.15, output: 1.25, cacheRead: 0.03, cacheWrite: 0 },
"google/gemini-2.5-pro": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0.375 },
"google/gemini-2.5-pro-preview": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0.375 },
"google/gemini-2.5-pro-preview-05-06": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0.375 },
"google/gemini-2.5-pro:batch": { input: 0.625, output: 5, cacheRead: 0.125, cacheWrite: 0 },
"google/gemini-3-flash-preview": { input: 0.5, output: 3, cacheRead: 0.05, cacheWrite: 0.083333 },
"google/gemini-3-flash-preview:batch": { input: 0.25, output: 1.5, cacheRead: 0, cacheWrite: 0 },
"google/gemini-3-pro-image": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0.375 },
"google/gemini-3.1-flash-lite": { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite: 0.083333 },
"google/gemini-3.1-flash-lite-preview": { input: 0.25, output: 1.5, cacheRead: 0.025, cacheWrite: 0.083333 },
"google/gemini-3.1-flash-lite:batch": { input: 0.125, output: 0.75, cacheRead: 0.0125, cacheWrite: 0 },
"google/gemini-3.1-pro-preview": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0.375 },
"google/gemini-3.1-pro-preview-customtools": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 0.375 },
"google/gemini-3.1-pro-preview:batch": { input: 1, output: 6, cacheRead: 0, cacheWrite: 0 },
"google/gemini-3.5-flash": { input: 1.5, output: 9, cacheRead: 0.15, cacheWrite: 0.083333 },
"google/gemini-3.5-flash-lite": { input: 0.3, output: 2.5, cacheRead: 0.03, cacheWrite: 0.083333 },
"google/gemini-3.5-flash-lite:batch": { input: 0.15, output: 1.25, cacheRead: 0.015, cacheWrite: 0 },
"google/gemini-3.5-flash:batch": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
"google/gemini-3.6-flash": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0.041667 },
"google/gemini-3.6-flash:batch": { input: 0.375, output: 1.875, cacheRead: 0.0375, cacheWrite: 0.041667 },
"google/gemini-3.7-flash": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0.041667 },
"google/gemini-3.7-flash:batch": { input: 0.375, output: 1.875, cacheRead: 0.0375, cacheWrite: 0.041667 },
"google/gemini-3.8-flash": { input: 0.75, output: 3.75, cacheRead: 0.075, cacheWrite: 0.041667 },
"google/gemini-3.8-flash:batch": { input: 0.375, output: 1.875, cacheRead: 0.0375, cacheWrite: 0.041667 },
"google/gemma-3-12b-it": { input: 0.05, output: 0.15, cacheRead: 0, cacheWrite: 0 },
"google/gemma-3-27b-it": { input: 0.08, output: 0.45, cacheRead: 0.04, cacheWrite: 0 },
"google/gemma-4-26b-a4b-it": { input: 0.07, output: 0.34, cacheRead: 0, cacheWrite: 0 },
"google/gemma-4-26b-a4b-it:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"google/gemma-4-31b-it": { input: 0.09, output: 0.34, cacheRead: 0.05, cacheWrite: 0 },
"google/gemma-4-31b-it:batch": { input: 0.39, output: 0.97, cacheRead: 0, cacheWrite: 0 },
"google/gemma-4-31b-it:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"ibm-granite/granite-4.2-8b": { input: 0.1, output: 0.15, cacheRead: 0.05, cacheWrite: 0 },
"inception/mercury-2": { input: 0.25, output: 0.75, cacheRead: 0.025, cacheWrite: 0 },
"inception/mercury-2.5-preview": { input: 0.04, output: 0.15, cacheRead: 0.004, cacheWrite: 0 },
"inclusionai/ling-3.0-flash": { input: 0.021, output: 0.063, cacheRead: 0.0042, cacheWrite: 0 },
"inclusionai/ling-3.0-flash-fin": { input: 0.06, output: 0.18, cacheRead: 0.012, cacheWrite: 0 },
"inclusionai/ling-3.0-flash-fin:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"inclusionai/ling-3.0-flash-sante:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"kwaipilot/kat-coder-pro-v2": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"kwaipilot/kat-coder-pro-v2.5": { input: 0.74, output: 2.96, cacheRead: 0.15, cacheWrite: 0 },
"liquid/lfm-2.5-2.6b:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"meituan/longcat-2.0": { input: 0.3, output: 1.2, cacheRead: 0.006, cacheWrite: 0 },
"meta-llama/llama-3.1-70b-instruct": { input: 0.4, output: 0.4, cacheRead: 0, cacheWrite: 0 },
"meta-llama/llama-3.1-8b-instruct": { input: 0.05, output: 0.08, cacheRead: 0.025, cacheWrite: 0 },
"meta-llama/llama-3.3-70b-instruct": { input: 0.1, output: 0.32, cacheRead: 0, cacheWrite: 0 },
"meta-llama/llama-4-maverick": { input: 0.2, output: 0.696, cacheRead: 0, cacheWrite: 0 },
"meta-llama/llama-4-scout": { input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0 },
"meta/muse-glimmer-30b": { input: 0.3, output: 1.1, cacheRead: 0.04, cacheWrite: 0 },
"meta/muse-glimmer-30b:batch": { input: 0.35, output: 1.5, cacheRead: 0.04, cacheWrite: 0 },
"meta/muse-spark-1.1": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
"meta/muse-spark-1.2": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
"meta/muse-spark-1.2-contributor": { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
"meta/muse-spark-1.3": { input: 1.25, output: 4.25, cacheRead: 0.15, cacheWrite: 0 },
"meta/muse-spark-1.3-contributor": { input: 0.1, output: 0.2, cacheRead: 0.002, cacheWrite: 0 },
"minimax/minimax-m1": { input: 0.55, output: 2.2, cacheRead: 0, cacheWrite: 0 },
"minimax/minimax-m2": { input: 0.255, output: 1.02, cacheRead: 0, cacheWrite: 0 },
"minimax/minimax-m2.1": { input: 0.3, output: 1.2, cacheRead: 0.03, cacheWrite: 0 },
"minimax/minimax-m2.5": { input: 0.27, output: 1.08, cacheRead: 0.027, cacheWrite: 0 },
"minimax/minimax-m2.7": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"minimax/minimax-m2.7:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"minimax/minimax-m3": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"minimax/minimax-m3:batch": { input: 0.3, output: 1.2, cacheRead: 0.06, cacheWrite: 0 },
"minimax/minimax-m3:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"mistralai/codestral-2508": { input: 0.3, output: 0.9, cacheRead: 0.03, cacheWrite: 0 },
"mistralai/devstral-2512": { input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0 },
"mistralai/ministral-14b-2512": { input: 0.2, output: 0.2, cacheRead: 0.02, cacheWrite: 0 },
"mistralai/ministral-3b-2512": { input: 0.1, output: 0.1, cacheRead: 0.01, cacheWrite: 0 },
"mistralai/ministral-8b-2512": { input: 0.15, output: 0.15, cacheRead: 0.015, cacheWrite: 0 },
"mistralai/mistral-large": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
"mistralai/mistral-large-2407": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
"mistralai/mistral-large-2512": { input: 0.5, output: 1.5, cacheRead: 0.05, cacheWrite: 0 },
"mistralai/mistral-medium-3": { input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0 },
"mistralai/mistral-medium-3-5": { input: 1.5, output: 7.5, cacheRead: 0, cacheWrite: 0 },
"mistralai/mistral-medium-3-5:batch": { input: 0.75, output: 3.75, cacheRead: 0, cacheWrite: 0 },
"mistralai/mistral-medium-3.1": { input: 0.4, output: 2, cacheRead: 0.04, cacheWrite: 0 },
"mistralai/mistral-nemo": { input: 0.019, output: 0.03, cacheRead: 0, cacheWrite: 0 },
"mistralai/mistral-saba": { input: 0.2, output: 0.6, cacheRead: 0.02, cacheWrite: 0 },
"mistralai/mistral-small-2603": { input: 0.15, output: 0.6, cacheRead: 0.015, cacheWrite: 0 },
"mistralai/mistral-small-3.2-24b-instruct": { input: 0.075, output: 0.2, cacheRead: 0, cacheWrite: 0 },
"mistralai/mixtral-8x22b-instruct": { input: 2, output: 6, cacheRead: 0.2, cacheWrite: 0 },
"mistralai/voxtral-small-24b-2507": { input: 0.1, output: 0.3, cacheRead: 0.01, cacheWrite: 0 },
"moonshotai/kimi-k2": { input: 0.57, output: 2.3, cacheRead: 0, cacheWrite: 0 },
"moonshotai/kimi-k2-0905": { input: 0.6, output: 2.5, cacheRead: 0, cacheWrite: 0 },
"moonshotai/kimi-k2-thinking": { input: 0.6, output: 2.5, cacheRead: 0.15, cacheWrite: 0 },
"moonshotai/kimi-k2.5": { input: 0.41, output: 2.06, cacheRead: 0.07, cacheWrite: 0 },
"moonshotai/kimi-k2.6": { input: 0.95, output: 4, cacheRead: 0.16, cacheWrite: 0 },
"moonshotai/kimi-k2.7-code": { input: 0.66, output: 3.4, cacheRead: 0.18, cacheWrite: 0 },
"moonshotai/kimi-k3": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
"moonshotai/kimi-k3:batch": { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 },
"nex-agi/nex-n2-mini": { input: 0.025, output: 0.1, cacheRead: 0.0025, cacheWrite: 0 },
"nex-agi/nex-n2-pro": { input: 0.25, output: 1, cacheRead: 0.025, cacheWrite: 0 },
"nvidia/nemotron-3-nano-30b-a3b": { input: 0.05, output: 0.2, cacheRead: 0.03, cacheWrite: 0 },
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"nvidia/nemotron-3-super-120b-a12b": { input: 0.085, output: 0.4, cacheRead: 0, cacheWrite: 0 },
"nvidia/nemotron-3-super-120b-a12b:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"nvidia/nemotron-3-ultra-550b-a55b": { input: 0.625, output: 3.125, cacheRead: 0.1875, cacheWrite: 0 },
"nvidia/nemotron-3-ultra-550b-a55b:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"nvidia/nemotron-3.5-lightning": { input: 0.08, output: 0.2, cacheRead: 0.04, cacheWrite: 0 },
"nvidia/nemotron-3.5-lightning:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-3.5-turbo": { input: 0.5, output: 1.5, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-3.5-turbo-0613": { input: 1, output: 2, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-3.5-turbo-16k": { input: 3, output: 4, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-3.5-turbo:batch": { input: 0.25, output: 0.75, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4": { input: 30, output: 60, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4-turbo": { input: 10, output: 30, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4-turbo-preview": { input: 10, output: 30, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4-turbo:batch": { input: 5, output: 15, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4.1": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
"openai/gpt-4.1-mini": { input: 0.4, output: 1.6, cacheRead: 0.1, cacheWrite: 0 },
"openai/gpt-4.1-mini:batch": { input: 0.2, output: 0.8, cacheRead: 0.05, cacheWrite: 0 },
"openai/gpt-4.1-nano": { input: 0.1, output: 0.4, cacheRead: 0.025, cacheWrite: 0 },
"openai/gpt-4.1-nano:batch": { input: 0.05, output: 0.2, cacheRead: 0.0125, cacheWrite: 0 },
"openai/gpt-4.1:batch": { input: 1, output: 4, cacheRead: 0.25, cacheWrite: 0 },
"openai/gpt-4o": { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 0 },
"openai/gpt-4o-2024-05-13": { input: 5, output: 15, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-4o-2024-08-06": { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 0 },
"openai/gpt-4o-2024-11-20": { input: 2.5, output: 10, cacheRead: 1.25, cacheWrite: 0 },
"openai/gpt-4o-mini": { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0 },
"openai/gpt-4o-mini-2024-07-18": { input: 0.15, output: 0.6, cacheRead: 0.075, cacheWrite: 0 },
"openai/gpt-4o-mini:batch": { input: 0.075, output: 0.3, cacheRead: 0.0375, cacheWrite: 0 },
"openai/gpt-4o:batch": { input: 1.25, output: 5, cacheRead: 0.625, cacheWrite: 0 },
"openai/gpt-5": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
"openai/gpt-5-mini": { input: 0.25, output: 2, cacheRead: 0.025, cacheWrite: 0 },
"openai/gpt-5-mini:batch": { input: 0.125, output: 1, cacheRead: 0.0125, cacheWrite: 0 },
"openai/gpt-5-nano": { input: 0.05, output: 0.4, cacheRead: 0.005, cacheWrite: 0 },
"openai/gpt-5-nano:batch": { input: 0.025, output: 0.2, cacheRead: 0.0025, cacheWrite: 0 },
"openai/gpt-5-pro": { input: 15, output: 120, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5-pro:batch": { input: 7.5, output: 60, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5:batch": { input: 0.625, output: 5, cacheRead: 0.0625, cacheWrite: 0 },
"openai/gpt-5.1": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
"openai/gpt-5.1-codex": { input: 1.25, output: 10, cacheRead: 0.13, cacheWrite: 0 },
"openai/gpt-5.1-codex-max": { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
"openai/gpt-5.1-codex-mini": { input: 0.25, output: 2, cacheRead: 0.03, cacheWrite: 0 },
"openai/gpt-5.1:batch": { input: 0.625, output: 5, cacheRead: 0.0625, cacheWrite: 0 },
"openai/gpt-5.2": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
"openai/gpt-5.2-chat": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
"openai/gpt-5.2-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
"openai/gpt-5.2-pro": { input: 21, output: 168, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.2-pro:batch": { input: 10.5, output: 84, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.2:batch": { input: 0.875, output: 7, cacheRead: 0.0875, cacheWrite: 0 },
"openai/gpt-5.3-codex": { input: 1.75, output: 14, cacheRead: 0.175, cacheWrite: 0 },
"openai/gpt-5.4": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
"openai/gpt-5.4-mini": { input: 0.75, output: 4.5, cacheRead: 0.075, cacheWrite: 0 },
"openai/gpt-5.4-mini:batch": { input: 0.375, output: 2.25, cacheRead: 0.0375, cacheWrite: 0 },
"openai/gpt-5.4-nano": { input: 0.2, output: 1.25, cacheRead: 0.02, cacheWrite: 0 },
"openai/gpt-5.4-nano:batch": { input: 0.1, output: 0.625, cacheRead: 0.01, cacheWrite: 0 },
"openai/gpt-5.4-pro": { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.4-pro:batch": { input: 15, output: 90, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.4:batch": { input: 1.25, output: 7.5, cacheRead: 0.125, cacheWrite: 0 },
"openai/gpt-5.5": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
"openai/gpt-5.5-pro": { input: 30, output: 180, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.5-pro:batch": { input: 15, output: 90, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-5.5:batch": { input: 2.5, output: 15, cacheRead: 0.25, cacheWrite: 0 },
"openai/gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
"openai/gpt-5.6-luna-pro": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
"openai/gpt-5.6-luna-pro:batch": { input: 0.1, output: 0.6, cacheRead: 0.01, cacheWrite: 0 },
"openai/gpt-5.6-luna:batch": { input: 0.1, output: 0.6, cacheRead: 0.01, cacheWrite: 0 },
"openai/gpt-5.6-sol": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"openai/gpt-5.6-sol-pro": { input: 2, output: 10, cacheRead: 0.2, cacheWrite: 2.5 },
"openai/gpt-5.6-sol-pro:batch": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"openai/gpt-5.6-sol:batch": { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
"openai/gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
"openai/gpt-5.6-terra-pro": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
"openai/gpt-5.6-terra-pro:batch": { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 0 },
"openai/gpt-5.6-terra:batch": { input: 1, output: 6, cacheRead: 0.1, cacheWrite: 0 },
"openai/gpt-6-astra": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
"openai/gpt-6-astra-pro": { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 },
"openai/gpt-6-astra-pro:batch": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"openai/gpt-6-astra:batch": { input: 5, output: 25, cacheRead: 0.5, cacheWrite: 6.25 },
"openai/gpt-audio": { input: 2.5, output: 10, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-audio-mini": { input: 0.6, output: 2.4, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-chat-latest": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
"openai/gpt-oss-120b": { input: 0.037, output: 0.17, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-oss-120b:batch": { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 },
"openai/gpt-oss-20b": { input: 0.03, output: 0.13, cacheRead: 0.03, cacheWrite: 0 },
"openai/gpt-oss-safeguard-20b": { input: 0.075, output: 0.3, cacheRead: 0.0375, cacheWrite: 0 },
"openai/o1": { input: 15, output: 60, cacheRead: 7.5, cacheWrite: 0 },
"openai/o3": { input: 2, output: 8, cacheRead: 0.5, cacheWrite: 0 },
"openai/o3-mini": { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 0 },
"openai/o3-mini-high": { input: 1.1, output: 4.4, cacheRead: 0.55, cacheWrite: 0 },
"openai/o3-mini:batch": { input: 0.55, output: 2.2, cacheRead: 0.275, cacheWrite: 0 },
"openai/o3-pro": { input: 20, output: 80, cacheRead: 0, cacheWrite: 0 },
"openai/o3:batch": { input: 1, output: 4, cacheRead: 0.25, cacheWrite: 0 },
"openai/o4-mini": { input: 1.1, output: 4.4, cacheRead: 0.275, cacheWrite: 0 },
"openai/o4-mini-high": { input: 1.1, output: 4.4, cacheRead: 0.275, cacheWrite: 0 },
"openai/o4-mini:batch": { input: 0.55, output: 2.2, cacheRead: 0.1375, cacheWrite: 0 },
"openrouter/auto": { input: -1000000, output: -1000000, cacheRead: 0, cacheWrite: 0 },
"openrouter/auto-beta": { input: -1000000, output: -1000000, cacheRead: 0, cacheWrite: 0 },
"openrouter/free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"openrouter/fusion": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"poolside/laguna-s-2.1": { input: 0.09, output: 0.18, cacheRead: 0.009, cacheWrite: 0 },
"poolside/laguna-s-2.1:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"poolside/laguna-xs-2.1": { input: 0.06, output: 0.12, cacheRead: 0.03, cacheWrite: 0 },
"poolside/laguna-xs-2.1:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen-2.5-72b-instruct": { input: 0.36, output: 0.4, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen-2.5-7b-instruct": { input: 0.1, output: 0.2, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen-plus": { input: 0.26, output: 0.78, cacheRead: 0.052, cacheWrite: 0.325 },
"qwen/qwen-plus-2025-07-28": { input: 0.26, output: 0.78, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-14b": { input: 0.12, output: 0.24, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-235b-a22b": { input: 0.455, output: 1.82, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-235b-a22b-2507": { input: 0.09, output: 0.55, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-235b-a22b-thinking-2507": { input: 0.23, output: 2.3, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-30b-a3b": { input: 0.12, output: 0.5, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-30b-a3b-instruct-2507": { input: 0.04815, output: 0.19305, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-30b-a3b-thinking-2507": { input: 0.2, output: 2.4, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-32b": { input: 0.08, output: 0.28, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-8b": { input: 0.117, output: 0.455, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-coder": { input: 0.3, output: 1, cacheRead: 0.1, cacheWrite: 0 },
"qwen/qwen3-coder-30b-a3b-instruct": { input: 0.07, output: 0.28, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-coder-flash": { input: 0.195, output: 0.975, cacheRead: 0.039, cacheWrite: 0.24375 },
"qwen/qwen3-coder-next": { input: 0.12, output: 0.8, cacheRead: 0.07, cacheWrite: 0 },
"qwen/qwen3-coder-plus": { input: 0.65, output: 3.25, cacheRead: 0.13, cacheWrite: 0.8125 },
"qwen/qwen3-max": { input: 0.78, output: 3.9, cacheRead: 0.156, cacheWrite: 0.975 },
"qwen/qwen3-max-thinking": { input: 0.78, output: 3.9, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-next-80b-a3b-instruct": { input: 0.1, output: 1.1, cacheRead: 0.07, cacheWrite: 0 },
"qwen/qwen3-next-80b-a3b-thinking": { input: 0.15, output: 1.2, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-235b-a22b-instruct": { input: 0.21, output: 1.9, cacheRead: 0.1, cacheWrite: 0 },
"qwen/qwen3-vl-235b-a22b-thinking": { input: 0.4, output: 4, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-30b-a3b-instruct": { input: 0.15, output: 0.6, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-30b-a3b-thinking": { input: 0.2, output: 2.4, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-32b-instruct": { input: 0.104, output: 0.416, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-8b-instruct": { input: 0.117, output: 0.455, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3-vl-8b-thinking": { input: 0.18, output: 2.1, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-122b-a10b": { input: 0.29, output: 2.4, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-27b": { input: 0.195, output: 1.56, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-35b-a3b": { input: 0.08, output: 0.75, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-397b-a17b": { input: 0.55, output: 3.5, cacheRead: 0.225, cacheWrite: 0 },
"qwen/qwen3.5-9b": { input: 0.1, output: 0.15, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-9b:batch": { input: 0.17, output: 0.25, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-flash-02-23": { input: 0.065, output: 0.26, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-plus-02-15": { input: 0.26, output: 1.56, cacheRead: 0, cacheWrite: 0 },
"qwen/qwen3.5-plus-20260420": { input: 0.3, output: 1.8, cacheRead: 0, cacheWrite: 0.375 },
"qwen/qwen3.6-27b": { input: 0.3, output: 2, cacheRead: 0.03, cacheWrite: 0 },
"qwen/qwen3.6-35b-a3b": { input: 0.1, output: 0.9, cacheRead: 0.05, cacheWrite: 0 },
"qwen/qwen3.6-flash": { input: 0.1875, output: 1.125, cacheRead: 0, cacheWrite: 0.234375 },
"qwen/qwen3.6-max-preview": { input: 1.027, output: 6.162, cacheRead: 0, cacheWrite: 1.28375 },
"qwen/qwen3.6-plus": { input: 0.325, output: 1.95, cacheRead: 0, cacheWrite: 0.40625 },
"qwen/qwen3.7-flash": { input: 0.03, output: 0.13, cacheRead: 0.006, cacheWrite: 0.038 },
"qwen/qwen3.7-max": { input: 1.475, output: 4.425, cacheRead: 0.295, cacheWrite: 1.84375 },
"qwen/qwen3.7-plus": { input: 0.32, output: 1.28, cacheRead: 0.064, cacheWrite: 0.4 },
"qwen/qwen3.8-2.4t-a95b": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 0 },
"qwen/qwen3.8-2.4t-a95b:batch": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 0 },
"qwen/qwen3.8-27b": { input: 0.42, output: 3, cacheRead: 0.085, cacheWrite: 0 },
"qwen/qwen3.8-flash": { input: 0.15, output: 0.47, cacheRead: 0.016, cacheWrite: 0.2 },
"qwen/qwen3.8-max-0902": { input: 2, output: 6, cacheRead: 0.25, cacheWrite: 2.5 },
"rekaai/reka-edge": { input: 0.1, output: 0.1, cacheRead: 0, cacheWrite: 0 },
"relace/relace-search": { input: 1, output: 3, cacheRead: 0, cacheWrite: 0 },
"sakana/fugu-ultra": { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 0 },
"sakana/sakana-namazu": { input: 0.95, output: 4, cacheRead: 0.15, cacheWrite: 0 },
"sao10k/l3.1-euryale-70b": { input: 0.85, output: 0.85, cacheRead: 0, cacheWrite: 0 },
"stepfun/step-3.5-flash": { input: 0.1, output: 0.3, cacheRead: 0, cacheWrite: 0 },
"stepfun/step-3.7-flash": { input: 0.2, output: 1.15, cacheRead: 0.04, cacheWrite: 0 },
"tencent/hy3": { input: 0.132, output: 0.528, cacheRead: 0.033, cacheWrite: 0 },
"tencent/hy3-preview": { input: 0.18, output: 0.6, cacheRead: 0.06, cacheWrite: 0 },
"tencent/hy4-preview": { input: 0.834, output: 2.501, cacheRead: 0.042, cacheWrite: 0 },
"thedrummer/unslopnemo-12b": { input: 0.4, output: 0.4, cacheRead: 0, cacheWrite: 0 },
"thinkingmachines/inkling": { input: 1, output: 4.05, cacheRead: 0.17, cacheWrite: 0 },
"thinkingmachines/inkling-small": { input: 0.45, output: 1.2, cacheRead: 0.1, cacheWrite: 0 },
"thinkingmachines/inkling-small:batch": { input: 0.5, output: 1.2, cacheRead: 0.1, cacheWrite: 0 },
"thinkingmachines/inkling-small:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"thinkingmachines/inkling:batch": { input: 1, output: 4.05, cacheRead: 0.17, cacheWrite: 0 },
"thinkingmachines/inkling:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"upstage/solar-pro-3": { input: 0.15, output: 0.6, cacheRead: 0.015, cacheWrite: 0 },
"upstage/solar-pro4": { input: 0.03, output: 0.12, cacheRead: 0.006, cacheWrite: 0 },
"x-ai/grok-4.20": { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
"x-ai/grok-4.3": { input: 1.25, output: 2.5, cacheRead: 0.2, cacheWrite: 0 },
"x-ai/grok-4.3:batch": { input: 1, output: 2, cacheRead: 0.16, cacheWrite: 0 },
"x-ai/grok-4.5": { input: 2, output: 6, cacheRead: 0.3, cacheWrite: 0 },
"x-ai/grok-4.6": { input: 2, output: 6, cacheRead: 0.5, cacheWrite: 0 },
"x-ai/grok-build-0.1": { input: 1, output: 2, cacheRead: 0.2, cacheWrite: 0 },
"xiaomi/mimo-v2.5": { input: 0.14, output: 0.28, cacheRead: 0.0028, cacheWrite: 0 },
"xiaomi/mimo-v2.5-pro": { input: 0.435, output: 0.87, cacheRead: 0.0036, cacheWrite: 0 },
"z-ai/glm-4.5": { input: 0.6, output: 2.2, cacheRead: 0.11, cacheWrite: 0 },
"z-ai/glm-4.5-air": { input: 0.13, output: 0.85, cacheRead: 0.025, cacheWrite: 0 },
"z-ai/glm-4.5v": { input: 0.6, output: 1.8, cacheRead: 0.11, cacheWrite: 0 },
"z-ai/glm-4.6": { input: 0.55, output: 2.2, cacheRead: 0.11, cacheWrite: 0 },
"z-ai/glm-4.6v": { input: 0.3, output: 0.9, cacheRead: 0.055, cacheWrite: 0 },
"z-ai/glm-4.7": { input: 0.4, output: 1.75, cacheRead: 0.08, cacheWrite: 0 },
"z-ai/glm-4.7-flash": { input: 0.06, output: 0.4, cacheRead: 0.01, cacheWrite: 0 },
"z-ai/glm-5": { input: 0.6, output: 1.9, cacheRead: 0.119, cacheWrite: 0 },
"z-ai/glm-5-turbo": { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
"z-ai/glm-5.1": { input: 0.966, output: 3.036, cacheRead: 0.1794, cacheWrite: 0 },
"z-ai/glm-5.2": { input: 0.966, output: 3.036, cacheRead: 0.1932, cacheWrite: 0 },
"z-ai/glm-5.2:free": { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
"z-ai/glm-5.3": { input: 1.4, output: 4.4, cacheRead: 0.14, cacheWrite: 0 },
"z-ai/glm-5.3-flash": { input: 0.075, output: 0.25, cacheRead: 0.015, cacheWrite: 0 },
"z-ai/glm-5.3-flash:batch": { input: 0.15, output: 0.5, cacheRead: 0.03, cacheWrite: 0 },
"z-ai/glm-5v-turbo": { input: 1.2, output: 4, cacheRead: 0.24, cacheWrite: 0 },
};
/** Context-window limits (tokens) per model — generated alongside PRICES. */
export const CONTEXT_LIMITS: Record<string, number> = {
"z-ai/glm-5.3-flash": 1310720,
"~anthropic/claude-fable-latest": 1000000,
"~anthropic/claude-haiku-latest": 200000,
"~anthropic/claude-opus-latest": 1000000,
"~anthropic/claude-sonnet-latest": 1000000,
"~deepseek/deepseek-v4-flash-latest": 1048576,
"~google/gemini-flash-latest": 1048576,
"~google/gemini-pro-latest": 1048576,
"~moonshotai/kimi-latest": 1048576,
"~openai/gpt-latest": 1050000,
"~openai/gpt-mini-latest": 400000,
"~x-ai/grok-latest": 500000,
"~z-ai/glm-flash-latest": 1048576,
"~z-ai/glm-latest": 262144,
"aion-labs/aion-2.0": 131072,
"aion-labs/aion-3.0": 131072,
"aion-labs/aion-3.0-mini": 131072,
"amazon/nova-2-lite-v1": 1000000,
"amazon/nova-lite-v1": 300000,
"amazon/nova-micro-v1": 128000,
"amazon/nova-premier-v1": 1000000,
"amazon/nova-pro-v1": 300000,
"anthropic/claude-3-haiku": 200000,
"anthropic/claude-fable-5": 1000000,
"anthropic/claude-fable-5:batch": 1000000,
"anthropic/claude-fable-5.1": 1000000,
"anthropic/claude-fable-5.1:batch": 1000000,
"anthropic/claude-haiku-4.5": 200000,
"anthropic/claude-haiku-4.5:batch": 200000,
"anthropic/claude-opus-4": 200000,
"anthropic/claude-opus-4.1": 200000,
"anthropic/claude-opus-4.1:batch": 200000,
"anthropic/claude-opus-4.5": 200000,
"anthropic/claude-opus-4.5:batch": 200000,
"anthropic/claude-opus-4.6": 1000000,
"anthropic/claude-opus-4.6:batch": 1000000,
"anthropic/claude-opus-4.7": 1000000,
"anthropic/claude-opus-4.7:batch": 1000000,
"anthropic/claude-opus-4.8": 1000000,
"anthropic/claude-opus-4.8:batch": 1000000,
"anthropic/claude-opus-5": 1000000,
"anthropic/claude-opus-5:batch": 1000000,
"anthropic/claude-sonnet-4": 200000,
"anthropic/claude-sonnet-4.5": 1000000,
"anthropic/claude-sonnet-4.5:batch": 1000000,
"anthropic/claude-sonnet-4.6": 1000000,
"anthropic/claude-sonnet-4.6:batch": 1000000,
"anthropic/claude-sonnet-5": 1000000,
"anthropic/claude-sonnet-5:batch": 1000000,
"arcee-ai/trinity-large-thinking": 262144,
"auto": 2000000,
"bytedance-seed/seed-1.6": 262144,
"bytedance-seed/seed-1.6-flash": 262144,
"bytedance-seed/seed-2-1-turbo": 262144,
"bytedance-seed/seed-2.0-code": 262144,
"bytedance-seed/seed-2.0-lite": 262144,
"bytedance-seed/seed-2.0-mini": 262144,
"cohere/command-r-08-2024": 128000,
"cohere/command-r-plus-08-2024": 128000,
"cohere/north-mini-code:free": 256000,
"deepseek/deepseek-chat": 163840,
"deepseek/deepseek-chat-v3-0324": 163840,
"deepseek/deepseek-chat-v3.1": 161000,
"deepseek/deepseek-r1": 64000,
"deepseek/deepseek-r1-0528": 163840,
"deepseek/deepseek-v3.1-terminus": 131072,
"deepseek/deepseek-v3.2": 163840,
"deepseek/deepseek-v3.2-exp": 163840,
"deepseek/deepseek-v4-flash": 1024000,
"deepseek/deepseek-v4-flash-0731": 1048576,
"deepseek/deepseek-v4-flash-0731:batch": 1048576,
"deepseek/deepseek-v4-flash-vision-exp": 1048576,
"deepseek/deepseek-v4-pro": 1024000,
"deepseek/deepseek-v4-pro-0813": 1024000,
"deepseek/deepseek-v4-pro-0813:batch": 1048576,
"dots-studio/dots-3-note-preview:free": 512000,
"google/gemini-2.5-flash": 1048576,
"google/gemini-2.5-flash-lite": 1048576,
"google/gemini-2.5-flash-lite:batch": 1048576,
"google/gemini-2.5-flash:batch": 1048576,
"google/gemini-2.5-pro": 1048576,
"google/gemini-2.5-pro-preview": 1048576,
"google/gemini-2.5-pro-preview-05-06": 1048576,
"google/gemini-2.5-pro:batch": 1048576,
"google/gemini-3-flash-preview": 1048576,
"google/gemini-3-flash-preview:batch": 1048576,
"google/gemini-3-pro-image": 65536,
"google/gemini-3.1-flash-lite": 1048576,
"google/gemini-3.1-flash-lite-preview": 1048576,
"google/gemini-3.1-flash-lite:batch": 1048576,
"google/gemini-3.1-pro-preview": 1048576,
"google/gemini-3.1-pro-preview-customtools": 1048576,
"google/gemini-3.1-pro-preview:batch": 1048576,
"google/gemini-3.5-flash": 1048576,
"google/gemini-3.5-flash-lite": 1048576,
"google/gemini-3.5-flash-lite:batch": 1048576,
"google/gemini-3.5-flash:batch": 1048576,
"google/gemini-3.6-flash": 1048576,
"google/gemini-3.6-flash:batch": 1048576,
"google/gemini-3.7-flash": 1048576,
"google/gemini-3.7-flash:batch": 1048576,
"google/gemini-3.8-flash": 1048576,
"google/gemini-3.8-flash:batch": 1048576,
"google/gemma-3-12b-it": 131072,
"google/gemma-3-27b-it": 131072,
"google/gemma-4-26b-a4b-it": 262144,
"google/gemma-4-26b-a4b-it:free": 262144,
"google/gemma-4-31b-it": 262144,
"google/gemma-4-31b-it:batch": 262144,
"google/gemma-4-31b-it:free": 262144,
"ibm-granite/granite-4.2-8b": 131072,
"inception/mercury-2": 128000,
"inception/mercury-2.5-preview": 260000,
"inclusionai/ling-3.0-flash": 262144,
"inclusionai/ling-3.0-flash-fin": 262144,
"inclusionai/ling-3.0-flash-fin:free": 262144,
"inclusionai/ling-3.0-flash-sante:free": 262144,
"kwaipilot/kat-coder-pro-v2": 262144,
"kwaipilot/kat-coder-pro-v2.5": 262144,
"liquid/lfm-2.5-2.6b:free": 65536,
"meituan/longcat-2.0": 1048756,
"meta-llama/llama-3.1-70b-instruct": 131072,
"meta-llama/llama-3.1-8b-instruct": 131072,
"meta-llama/llama-3.3-70b-instruct": 131072,
"meta-llama/llama-4-maverick": 128000,
"meta-llama/llama-4-scout": 327680,
"meta/muse-glimmer-30b": 131072,
"meta/muse-glimmer-30b:batch": 131072,
"meta/muse-spark-1.1": 1048576,
"meta/muse-spark-1.2": 1048576,
"meta/muse-spark-1.2-contributor": 1048576,
"meta/muse-spark-1.3": 1048576,
"meta/muse-spark-1.3-contributor": 1048576,
"minimax/minimax-m1": 1000000,
"minimax/minimax-m2": 204800,
"minimax/minimax-m2.1": 204800,
"minimax/minimax-m2.5": 200000,
"minimax/minimax-m2.7": 204800,
"minimax/minimax-m2.7:free": 196608,
"minimax/minimax-m3": 524288,
"minimax/minimax-m3:batch": 524288,
"minimax/minimax-m3:free": 1048576,
"mistralai/codestral-2508": 256000,
"mistralai/devstral-2512": 262144,
"mistralai/ministral-14b-2512": 262144,
"mistralai/ministral-3b-2512": 131072,
"mistralai/ministral-8b-2512": 262144,
"mistralai/mistral-large": 128000,
"mistralai/mistral-large-2407": 131072,
"mistralai/mistral-large-2512": 262144,
"mistralai/mistral-medium-3": 131072,
"mistralai/mistral-medium-3-5": 262144,
"mistralai/mistral-medium-3-5:batch": 32768,
"mistralai/mistral-medium-3.1": 131072,
"mistralai/mistral-nemo": 131072,
"mistralai/mistral-saba": 32768,
"mistralai/mistral-small-2603": 262144,
"mistralai/mistral-small-3.2-24b-instruct": 128000,
"mistralai/mixtral-8x22b-instruct": 65536,
"mistralai/voxtral-small-24b-2507": 32768,
"moonshotai/kimi-k2": 131072,
"moonshotai/kimi-k2-0905": 262144,
"moonshotai/kimi-k2-thinking": 262144,
"moonshotai/kimi-k2.5": 262144,
"moonshotai/kimi-k2.6": 262144,
"moonshotai/kimi-k2.7-code": 262144,
"moonshotai/kimi-k3": 1048576,
"moonshotai/kimi-k3:batch": 1048576,
"nex-agi/nex-n2-mini": 262144,
"nex-agi/nex-n2-pro": 262144,
"nvidia/nemotron-3-nano-30b-a3b": 262144,
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free": 256000,
"nvidia/nemotron-3-super-120b-a12b": 262144,
"nvidia/nemotron-3-super-120b-a12b:free": 262144,
"nvidia/nemotron-3-ultra-550b-a55b": 256000,
"nvidia/nemotron-3-ultra-550b-a55b:free": 1000000,
"nvidia/nemotron-3.5-lightning": 262144,
"nvidia/nemotron-3.5-lightning:free": 1000000,
"openai/gpt-3.5-turbo": 16385,
"openai/gpt-3.5-turbo-0613": 4095,
"openai/gpt-3.5-turbo-16k": 16385,
"openai/gpt-3.5-turbo:batch": 16385,
"openai/gpt-4": 8191,
"openai/gpt-4-turbo": 128000,
"openai/gpt-4-turbo-preview": 128000,
"openai/gpt-4-turbo:batch": 128000,
"openai/gpt-4.1": 1047576,
"openai/gpt-4.1-mini": 1047576,
"openai/gpt-4.1-mini:batch": 1047576,
"openai/gpt-4.1-nano": 1047576,
"openai/gpt-4.1-nano:batch": 1047576,
"openai/gpt-4.1:batch": 1047576,
"openai/gpt-4o": 128000,
"openai/gpt-4o-2024-05-13": 128000,
"openai/gpt-4o-2024-08-06": 128000,
"openai/gpt-4o-2024-11-20": 128000,
"openai/gpt-4o-mini": 128000,
"openai/gpt-4o-mini-2024-07-18": 128000,
"openai/gpt-4o-mini:batch": 128000,
"openai/gpt-4o:batch": 128000,
"openai/gpt-5": 400000,
"openai/gpt-5-mini": 400000,
"openai/gpt-5-mini:batch": 400000,
"openai/gpt-5-nano": 400000,
"openai/gpt-5-nano:batch": 400000,
"openai/gpt-5-pro": 400000,
"openai/gpt-5-pro:batch": 400000,
"openai/gpt-5:batch": 400000,
"openai/gpt-5.1": 400000,
"openai/gpt-5.1-codex": 400000,
"openai/gpt-5.1-codex-max": 400000,
"openai/gpt-5.1-codex-mini": 400000,
"openai/gpt-5.1:batch": 400000,
"openai/gpt-5.2": 400000,
"openai/gpt-5.2-chat": 128000,
"openai/gpt-5.2-codex": 400000,
"openai/gpt-5.2-pro": 400000,
"openai/gpt-5.2-pro:batch": 400000,
"openai/gpt-5.2:batch": 400000,
"openai/gpt-5.3-codex": 400000,
"openai/gpt-5.4": 1050000,
"openai/gpt-5.4-mini": 400000,
"openai/gpt-5.4-mini:batch": 400000,
"openai/gpt-5.4-nano": 400000,
"openai/gpt-5.4-nano:batch": 400000,
"openai/gpt-5.4-pro": 1050000,
"openai/gpt-5.4-pro:batch": 1050000,
"openai/gpt-5.4:batch": 1050000,
"openai/gpt-5.5": 1050000,
"openai/gpt-5.5-pro": 1050000,
"openai/gpt-5.5-pro:batch": 1050000,
"openai/gpt-5.5:batch": 1050000,
"openai/gpt-5.6-luna": 1050000,
"openai/gpt-5.6-luna-pro": 1050000,
"openai/gpt-5.6-luna-pro:batch": 1050000,
"openai/gpt-5.6-luna:batch": 1050000,
"openai/gpt-5.6-sol": 1050000,
"openai/gpt-5.6-sol-pro": 1050000,
"openai/gpt-5.6-sol-pro:batch": 1050000,
"openai/gpt-5.6-sol:batch": 1050000,
"openai/gpt-5.6-terra": 1050000,
"openai/gpt-5.6-terra-pro": 1050000,
"openai/gpt-5.6-terra-pro:batch": 1050000,
"openai/gpt-5.6-terra:batch": 1050000,
"openai/gpt-6-astra": 1050000,
"openai/gpt-6-astra-pro": 1050000,
"openai/gpt-6-astra-pro:batch": 1050000,
"openai/gpt-6-astra:batch": 1050000,
"openai/gpt-audio": 128000,
"openai/gpt-audio-mini": 128000,
"openai/gpt-chat-latest": 400000,
"openai/gpt-oss-120b": 131072,
"openai/gpt-oss-120b:batch": 131072,
"openai/gpt-oss-20b": 131072,
"openai/gpt-oss-safeguard-20b": 131072,
"openai/o1": 200000,
"openai/o3": 200000,
"openai/o3-mini": 200000,
"openai/o3-mini-high": 200000,
"openai/o3-mini:batch": 200000,
"openai/o3-pro": 200000,
"openai/o3:batch": 200000,
"openai/o4-mini": 200000,
"openai/o4-mini-high": 200000,
"openai/o4-mini:batch": 200000,
"openrouter/auto": 2000000,
"openrouter/auto-beta": 2000000,
"openrouter/free": 200000,
"openrouter/fusion": 1000000,
"poolside/laguna-s-2.1": 1048576,
"poolside/laguna-s-2.1:free": 262144,
"poolside/laguna-xs-2.1": 262144,
"poolside/laguna-xs-2.1:free": 262144,
"qwen/qwen-2.5-72b-instruct": 32768,
"qwen/qwen-2.5-7b-instruct": 32768,
"qwen/qwen-plus": 1000000,
"qwen/qwen-plus-2025-07-28": 1000000,
"qwen/qwen3-14b": 40960,
"qwen/qwen3-235b-a22b": 131072,
"qwen/qwen3-235b-a22b-2507": 262144,
"qwen/qwen3-235b-a22b-thinking-2507": 131072,
"qwen/qwen3-30b-a3b": 40960,
"qwen/qwen3-30b-a3b-instruct-2507": 128000,
"qwen/qwen3-30b-a3b-thinking-2507": 81920,
"qwen/qwen3-32b": 40960,
"qwen/qwen3-8b": 131072,
"qwen/qwen3-coder": 262144,
"qwen/qwen3-coder-30b-a3b-instruct": 262144,
"qwen/qwen3-coder-flash": 1000000,
"qwen/qwen3-coder-next": 262144,
"qwen/qwen3-coder-plus": 1000000,
"qwen/qwen3-max": 262144,
"qwen/qwen3-max-thinking": 262144,
"qwen/qwen3-next-80b-a3b-instruct": 262144,
"qwen/qwen3-next-80b-a3b-thinking": 131072,
"qwen/qwen3-vl-235b-a22b-instruct": 131072,
"qwen/qwen3-vl-235b-a22b-thinking": 131072,
"qwen/qwen3-vl-30b-a3b-instruct": 262144,
"qwen/qwen3-vl-30b-a3b-thinking": 131072,
"qwen/qwen3-vl-32b-instruct": 131072,
"qwen/qwen3-vl-8b-instruct": 131072,
"qwen/qwen3-vl-8b-thinking": 131072,
"qwen/qwen3.5-122b-a10b": 262144,
"qwen/qwen3.5-27b": 262144,
"qwen/qwen3.5-35b-a3b": 262144,
"qwen/qwen3.5-397b-a17b": 262144,
"qwen/qwen3.5-9b": 262144,
"qwen/qwen3.5-9b:batch": 262144,
"qwen/qwen3.5-flash-02-23": 1000000,
"qwen/qwen3.5-plus-02-15": 1000000,
"qwen/qwen3.5-plus-20260420": 1000000,
"qwen/qwen3.6-27b": 262144,
"qwen/qwen3.6-35b-a3b": 262144,
"qwen/qwen3.6-flash": 1000000,
"qwen/qwen3.6-max-preview": 262144,
"qwen/qwen3.6-plus": 1000000,
"qwen/qwen3.7-flash": 1000000,
"qwen/qwen3.7-max": 1000000,
"qwen/qwen3.7-plus": 1000000,
"qwen/qwen3.8-2.4t-a95b": 1000000,
"qwen/qwen3.8-2.4t-a95b:batch": 1010000,
"qwen/qwen3.8-27b": 1000000,
"qwen/qwen3.8-flash": 1000000,
"qwen/qwen3.8-max-0902": 1000000,
"rekaai/reka-edge": 16384,
"relace/relace-search": 256000,
"sakana/fugu-ultra": 1000000,
"sakana/sakana-namazu": 262144,
"sao10k/l3.1-euryale-70b": 131072,
"stepfun/step-3.5-flash": 262144,
"stepfun/step-3.7-flash": 256000,
"tencent/hy3": 262144,
"tencent/hy3-preview": 262144,
"tencent/hy4-preview": 1048576,
"thedrummer/unslopnemo-12b": 32768,
"thinkingmachines/inkling": 524288,
"thinkingmachines/inkling-small": 524288,
"thinkingmachines/inkling-small:batch": 524288,
"thinkingmachines/inkling-small:free": 1048576,
"thinkingmachines/inkling:batch": 524288,
"thinkingmachines/inkling:free": 1048576,
"upstage/solar-pro-3": 131072,
"upstage/solar-pro4": 524288,
"x-ai/grok-4.20": 2000000,
"x-ai/grok-4.3": 1000000,
"x-ai/grok-4.3:batch": 1000000,
"x-ai/grok-4.5": 500000,
"x-ai/grok-4.6": 500000,
"x-ai/grok-build-0.1": 256000,
"xiaomi/mimo-v2.5": 1048576,
"xiaomi/mimo-v2.5-pro": 1048576,
"z-ai/glm-4.5": 131072,
"z-ai/glm-4.5-air": 131072,
"z-ai/glm-4.5v": 65536,
"z-ai/glm-4.6": 204800,
"z-ai/glm-4.6v": 131072,
"z-ai/glm-4.7": 202752,
"z-ai/glm-4.7-flash": 202752,
"z-ai/glm-5": 198000,
"z-ai/glm-5-turbo": 202752,
"z-ai/glm-5.1": 200000,
"z-ai/glm-5.2": 1048576,
"z-ai/glm-5.2:free": 256000,
"z-ai/glm-5.3": 1048576,
"z-ai/glm-5.3-flash": 1048576,
"z-ai/glm-5.3-flash:batch": 1048575,
"z-ai/glm-5v-turbo": 202752,
};
// ─── Pure helpers (folds + cost inference) ───────────────────────────────────
+11 -4
View File
@@ -1,9 +1,10 @@
import type { UIMessage } from "ai";
import type { Usage } from "@earendil-works/pi-ai";
import { createProberAgent, createTeacherAgent, type TeachingAgent } from "../agent/teacher-agent.ts";
import { piEngineFor } from "../agent/models.ts";
import { createJobRunner } from "../agent/subagents/runner.ts";
import { createExtractionPipeline } from "../agent/subagents/extraction.ts";
import { MODELS } from "../../config.ts";
import type { ModelRole } from "../../config.ts";
import { claimSteers, deliverJobResult, setJobRespawn, setTurnRunner } from "./jobs.ts";
import { recordToPiMessages } from "./pi/record-adapter.ts";
import { createPiTurnResponse } from "./pi/ui-stream.ts";
@@ -147,7 +148,13 @@ export async function handleChatRequest(
// Both share this whole composition (write-ahead persist, liveness,
// steers, usage, extraction) — only the agent differs.
const teaching = outgoing.some((m) => m.parts.some((p) => p.type === "tool-probe_done"));
const modelId = teaching ? MODELS.teacher.model : MODELS.prober.model;
const role: ModelRole = teaching ? "teacher" : "prober";
// The chat's model-control settings (prober/teacher only — one controller
// for the conversation agents; piEngineFor falls back to config on absent
// fields and clamps the thinking level to the model's supported list).
const settings = stores.chatStore.getChat(chatId)?.settings;
const engine = piEngineFor(role, settings);
const modelId = engine.model.id;
// Tests inject a factory: the agent is stateless per turn (docs/
// pi-engine-swap.md §2), so each request builds a fresh one off the
// transcript — the factory mirrors that seam exactly.
@@ -161,7 +168,7 @@ export async function handleChatRequest(
jobStore: stores.jobStore,
spawn: (job, input) => void jobRunner.runJob(job.id, input),
},
{ messages: recordToPiMessages(outgoing), onUsage: collectUsage },
{ engine, messages: recordToPiMessages(outgoing), onUsage: collectUsage },
)
: createProberAgent(
{
@@ -169,7 +176,7 @@ export async function handleChatRequest(
chatStore: stores.chatStore,
deliver: (steer) => void deliverJobResult(steer),
},
{ messages: recordToPiMessages(outgoing), onUsage: collectUsage },
{ engine, messages: recordToPiMessages(outgoing), onUsage: collectUsage },
);
// A test-injected agent gets no usage collector (tests assert the
+1
View File
@@ -11,3 +11,4 @@ export * from "./jobs.ts";
export * from "./stores.ts";
export * from "./turn-liveness.ts";
export * from "./observability.ts";
export * from "./model-catalog.ts";
+108
View File
@@ -0,0 +1,108 @@
/**
* The model catalog's server surface (model-control wave, 2026-09-12):
* pi-ai's shipped OpenRouter catalog projected to client-safe DTOs for the
* picker (GET /api/models), plus the validation gate for per-chat settings
* (PUT /api/chats/[id]/settings).
*
* pi-ai owns ALL the data: the catalog (id/name/cost/context/modality/
* thinkingLevelMap) and the per-model thinking truth
* (getSupportedThinkingLevels / clampThinkingLevel). This module only
* projects and validates — no second registry to drift out of sync.
*
* Multi-provider future: `getModels()` is called per configured provider,
* so a second provider (pi-ai ships ~30) appears in the picker by adding
* it to piModels() in src/agent/models.ts — nothing here changes.
*/
import { getSupportedThinkingLevels, type Api, type Model } from "@earendil-works/pi-ai";
import type { ThinkingLevel } from "@earendil-works/pi-agent-core";
import { MODELS } from "../../config.ts";
import { piModels } from "../agent/models.ts";
import type { ChatSettings } from "../core/contracts/chat.ts";
import type {
CatalogDefaults,
ModelCatalogEntry,
ModelCatalogResponse,
} from "../core/contracts/models.ts";
/** The picker's client-safe model entry (contract: core/contracts/models.ts). */
export type ModelDto = ModelCatalogEntry;
/** The conversation agents' config pin — what a settings-less chat runs. */
export function catalogDefaults(): CatalogDefaults {
return {
model: { provider: "openrouter", id: MODELS.prober.model },
thinkingLevel: MODELS.prober.effort as ThinkingLevel,
};
}
/** Project one catalog model to the client DTO. */
export function toModelDto(model: Model<Api>): ModelDto {
return {
id: model.id,
name: model.name,
provider: model.provider,
reasoning: model.reasoning,
input: [...model.input],
supportedLevels: getSupportedThinkingLevels(model),
cost: {
input: model.cost.input,
output: model.cost.output,
cacheRead: model.cost.cacheRead,
cacheWrite: model.cost.cacheWrite,
},
contextWindow: model.contextWindow,
maxTokens: model.maxTokens,
};
}
/** The full picker payload: every model of every configured provider
* (sorted by provider then id — stable, diff-friendly) + the config
* defaults a settings-less chat runs. */
export function modelCatalog(): ModelCatalogResponse {
const models = piModels()
.getProviders()
.flatMap((p) => piModels().getModels(p.id))
.map(toModelDto)
.sort((a, b) => (a.provider === b.provider ? a.id.localeCompare(b.id) : a.provider.localeCompare(b.provider)));
return { models, defaults: catalogDefaults() };
}
/** One validated outcome for a settings write. */
export type SettingsValidation =
| { ok: true }
| { ok: false; error: string; supported?: ThinkingLevel[] };
/** Validate a picker write against the catalog (the settings route's gate).
* `settings` is replace-whole: absent fields mean "config default".
* - model: provider must be configured, id must exist in its catalog.
* - thinkingLevel: must be in the resolved model's supported list —
* the model the picker writes against (settings.model, else config's
* prober/teacher pin; one controller, so the two specs share it). */
export function validateChatSettings(settings: ChatSettings): SettingsValidation {
const providers = piModels().getProviders().map((p) => p.id);
if (settings.model) {
const { provider, id } = settings.model;
if (!providers.includes(provider)) {
return { ok: false, error: `Unknown provider "${provider}" — configured: ${providers.join(", ")}` };
}
if (!piModels().getModel(provider, id)) {
return { ok: false, error: `No model "${id}" in the ${provider} catalog` };
}
}
if (settings.thinkingLevel !== undefined) {
const provider = settings.model?.provider ?? "openrouter";
const id = settings.model?.id ?? MODELS.prober.model;
const model = piModels().getModel(provider, id);
if (!model) return { ok: false, error: `No model "${id}" in the ${provider} catalog` };
const supported = getSupportedThinkingLevels(model);
if (!supported.includes(settings.thinkingLevel)) {
return {
ok: false,
error: `${id} does not support thinking level "${settings.thinkingLevel}"`,
supported,
};
}
}
return { ok: true };
}
+13 -1
View File
@@ -11,7 +11,7 @@ import * as fs from "node:fs";
import * as path from "node:path";
import { PATHS } from "../../config.ts";
import type { ChatStore } from "../core/ports.ts";
import type { ChatRecord, ChatSummary, ExtractionRunUsage, PlanTree, TurnUsage } from "../core/contracts.ts";
import type { ChatRecord, ChatSettings, ChatSummary, ExtractionRunUsage, PlanTree, TurnUsage } from "../core/contracts.ts";
import type { UIMessage } from "ai";
import { findPendingToolCall } from "../core/pending.ts";
import { phaseAt3 } from "../core/phases.ts";
@@ -105,6 +105,17 @@ function appendExtractionUsage(id: string, entry: ExtractionRunUsage): void {
fs.writeFileSync(chatFile(id), JSON.stringify(record, null, "\t"), "utf-8");
}
/** Persist the per-chat model/thinking settings — replace-whole (the
* picker sends the complete object; absent = config defaults). */
function saveSettings(id: string, settings: ChatSettings): void {
const record = getChat(id);
if (!record) throw new Error(`Chat not found: ${id}`);
// Like saveTree: a settings tweak is not conversation activity, so it
// deliberately does NOT bump updatedAt (sidebar order must not churn).
record.settings = settings;
fs.writeFileSync(chatFile(id), JSON.stringify(record, null, "\t"), "utf-8");
}
/** Persist the session plan tree (plan tools call this in execute). */
function saveTree(id: string, tree: PlanTree): void {
const record = getChat(id);
@@ -121,6 +132,7 @@ export const chatStore: ChatStore = {
listChats,
saveMessages,
saveTree,
saveSettings,
appendTurnUsage,
appendExtractionUsage,
};