pi-mono/packages/ai/scripts/model-generation/shared.ts
Claude 8d95fd138b
feat(ai): declare model catalog generation as providers and rules
Replace the single 3300-line generate-models.ts with declarative provider
definitions and ordered rule packs under scripts/model-generation/.

Each provider is a defineProvider() call: an upstream source (usually the
generic modelsDev() mapper), static models upstream lacks, and rules scoped
to that provider. Cross-provider facts live in rule packs applied in a fixed
order. Every rule() carries a why, matches models with plain data, and
patches them with typed set/replace/apply semantics.

Generated model data is unchanged: 1443 models across 41 providers compare
equal against a snapshot of the same upstream responses, apart from a now
stable key order. Committed models.generated.ts and provider shards are
byte-identical.

Also:
- Bring packages/ai/scripts under tsgo and biome; the old generator had 20+
  type errors that nothing checked.
- Add --upstream-snapshot <dir> to record and replay upstream catalogs and
  --explain <provider>:<model> to list the rules applied to a model.
- Report rules that matched no model so stale corrections get removed.
- Export parseRadiusGatewayConfig so the generator can replay Radius too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018ExCU9zaZnN8vB5fKpTAjP
2026-09-20 16:03:22 +00:00

136 lines
4.2 KiB
TypeScript

import type { ModelCost, OpenAICompletionsCompat, ThinkingLevelMap } from "../../src/types.ts";
import { roundCost } from "./dsl.ts";
// Values shared by more than one provider declaration or rule pack. Anything used in a
// single place belongs next to its provider or rule instead.
export const OPENAI_BASE_URL = "https://api.openai.com/v1";
export const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272000;
/** OpenAI models whose long-context requests are billed at higher rates. */
export const OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS = [
"gpt-5.4",
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.5-pro",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
] as const;
export function withOpenAiLongContextPricing(cost: ModelCost): ModelCost {
return {
...cost,
tiers: [
{
inputTokensAbove: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
input: roundCost(cost.input * 2),
output: roundCost(cost.output * 1.5),
cacheRead: roundCost(cost.cacheRead * 2),
cacheWrite: roundCost(cost.cacheWrite * 2),
},
],
};
}
// OpenAI reduced GPT-5.6 Terra and Luna prices on 2026-07-30. Keep these
// authoritative values until models.dev and passthrough catalogs catch up.
// https://developers.openai.com/api/docs/pricing
export const OPENAI_GPT_56_STANDARD_COSTS: Record<string, ModelCost> = {
"gpt-5.6-luna": { input: 0.2, output: 1.2, cacheRead: 0.02, cacheWrite: 0.25 },
"gpt-5.6-terra": { input: 2, output: 12, cacheRead: 0.2, cacheWrite: 2.5 },
};
export const OPENAI_GPT_56_SOL_COST: ModelCost = { input: 5, output: 30, cacheRead: 0.5, cacheWrite: 6.25 };
export const OPENAI_GPT_6_ASTRA_COST: ModelCost = { input: 10, output: 50, cacheRead: 1, cacheWrite: 12.5 };
/** OpenAI Responses models with client-executed tool search. */
export const OPENAI_TOOL_SEARCH_MODEL_IDS = [
"gpt-5.4",
"gpt-5.4-mini",
"gpt-5.4-pro",
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
] as const;
/** OpenAI models that accept developer messages after the conversation has started. */
export const OPENAI_MID_CONVO_SYSTEM_MESSAGE_MODEL_IDS = OPENAI_TOOL_SEARCH_MODEL_IDS;
export const OPENAI_XHIGH_MODEL_ID_PARTS = [
"gpt-5.2",
"gpt-5.3",
"gpt-5.4",
"gpt-5.5",
"gpt-5.6",
"gpt-6-astra",
] as const;
export const OPENAI_MAX_MODEL_ID_PARTS = ["gpt-5.6", "gpt-6-astra"] as const;
export function supportsOpenAiXhigh(modelId: string): boolean {
return OPENAI_XHIGH_MODEL_ID_PARTS.some((part) => modelId.includes(part));
}
export const KIMI_K3_MAX_TOKENS = 131072;
export const KIMI_K3_COST: ModelCost = { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 0 };
export const DEEPSEEK_COMPAT: OpenAICompletionsCompat = {
requiresReasoningContentOnAssistantMessages: true,
thinkingFormat: "deepseek",
};
export const DEEPSEEK_V4_THINKING_LEVEL_MAP: ThinkingLevelMap = {
minimal: null,
low: null,
medium: null,
high: "high",
max: "max",
};
export const DEEPSEEK_V4_FLASH_THINKING_LEVEL_MAP: ThinkingLevelMap = {
...DEEPSEEK_V4_THINKING_LEVEL_MAP,
low: "low",
};
export const QWEN_TOKEN_PLAN_PROVIDER_IDS = [
"qwen-token-plan",
"qwen-token-plan-cn",
"qwen-token-plan-individual",
] as const;
export const TOGETHER_BASE_URL = "https://api.together.ai/v1";
/** Together models that always reason and reject the reasoning toggle. */
export const TOGETHER_REASONING_ONLY_MODEL_IDS = ["deepseek-ai/DeepSeek-R1", "MiniMaxAI/MiniMax-M2.7"] as const;
/** Claude id fragments across providers; providers spell versions with `-` or `.`. */
export const ANTHROPIC_ADAPTIVE_THINKING_ID_PARTS = [
"opus-4-6",
"opus-4.6",
"opus-4-7",
"opus-4.7",
"opus-4-8",
"opus-4.8",
"opus-5",
"opus.5",
"sonnet-4-6",
"sonnet-4.6",
"sonnet-5",
"sonnet.5",
"fable-5",
"mythos-5",
] as const;
export function supportsAnthropicMidConvoEffort(modelId: string): boolean {
const id = modelId.toLowerCase().replace(/^~?anthropic\//, "");
return /^claude-opus-5(?:-\d{8})?$/.test(id) || /^claude-(?:fable|mythos)-5(?:[.-]1)(?:-\d{8})?$/.test(id);
}
export function supportsAnthropicMidConvoSystemMessages(modelId: string): boolean {
return (
/^claude-opus-(?:4[.-]8|5)(?:-\d{8})?$/.test(modelId) ||
/^claude-(?:fable|mythos)-5(?:[.-]1)?(?:-\d{8})?$/.test(modelId)
);
}
export function isGemma4Model(modelId: string): boolean {
return /gemma-?4/.test(modelId.toLowerCase());
}