pi-mono/packages/ai/scripts/model-generation/providers/openai.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

97 lines
3.4 KiB
TypeScript

import type { Model } from "../../../src/types.ts";
import { defineProvider, rule } from "../dsl.ts";
import { modelsDev } from "../models-dev.ts";
import {
OPENAI_BASE_URL,
OPENAI_GPT_6_ASTRA_COST,
OPENAI_GPT_56_SOL_COST,
OPENAI_GPT_56_STANDARD_COSTS,
OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS,
withOpenAiLongContextPricing,
} from "../shared.ts";
// models.dev lists this alias, but it is not accepted by OpenAI APIs.
const MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS = new Set(["gpt-5.6"]);
// Keep direct OpenAI requests in the short-context pricing tier by default. Users can opt into the
// larger context through model overrides, so retain long-context cost metadata on the capped models.
const OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS = [
"gpt-5.4",
"gpt-5.5",
"gpt-5.6-sol",
"gpt-5.6-terra",
"gpt-5.6-luna",
"gpt-6-astra",
];
function openAiResponsesModel(
id: string,
name: string,
model: Pick<Model<"openai-responses">, "reasoning" | "input" | "cost" | "contextWindow" | "maxTokens">,
): Model<"openai-responses"> {
return { id, name, api: "openai-responses", provider: "openai", baseUrl: OPENAI_BASE_URL, ...model };
}
const shortContextFrontier: Pick<Model<"openai-responses">, "reasoning" | "input" | "contextWindow" | "maxTokens"> = {
reasoning: true,
input: ["text", "image"],
contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
maxTokens: 128000,
};
export const openai = defineProvider({
id: "openai",
source: modelsDev({
provider: "openai",
key: "openai",
api: "openai-responses",
baseUrl: OPENAI_BASE_URL,
include: (id) => !MODELS_DEV_OPENAI_UNSUPPORTED_MODEL_IDS.has(id),
}),
// Models missing from models.dev.
models: [
openAiResponsesModel("gpt-6-astra", "GPT-6 Astra", {
...shortContextFrontier,
cost: withOpenAiLongContextPricing(OPENAI_GPT_6_ASTRA_COST),
}),
openAiResponsesModel("gpt-5.6-sol", "GPT-5.6 Sol", {
...shortContextFrontier,
cost: withOpenAiLongContextPricing(OPENAI_GPT_56_SOL_COST),
}),
openAiResponsesModel("gpt-5.6-terra", "GPT-5.6 Terra", {
...shortContextFrontier,
cost: withOpenAiLongContextPricing(OPENAI_GPT_56_STANDARD_COSTS["gpt-5.6-terra"]),
}),
openAiResponsesModel("gpt-5.6-luna", "GPT-5.6 Luna", {
...shortContextFrontier,
cost: withOpenAiLongContextPricing(OPENAI_GPT_56_STANDARD_COSTS["gpt-5.6-luna"]),
}),
openAiResponsesModel("gpt-5-chat-latest", "GPT-5 Chat Latest", {
reasoning: false,
input: ["text", "image"],
cost: { input: 1.25, output: 10, cacheRead: 0.125, cacheWrite: 0 },
contextWindow: 128000,
maxTokens: 16384,
}),
],
rules: [
rule({
why: "Keep direct OpenAI requests in the short-context pricing tier by default; users opt into the larger context via model overrides.",
match: { id: OPENAI_SHORT_CONTEXT_CAPPED_MODEL_IDS },
set: { contextWindow: OPENAI_LONG_CONTEXT_INPUT_THRESHOLD, maxTokens: 128000 },
}),
rule({
why: "Long-context requests are billed at higher rates; models.dev only carries the short-context price. GPT-5.6 Terra/Luna use the 2026-07-30 price cut.",
match: { id: OPENAI_LONG_CONTEXT_PRICING_MODEL_IDS },
apply: (model) => {
model.cost = withOpenAiLongContextPricing(OPENAI_GPT_56_STANDARD_COSTS[model.id] ?? model.cost);
},
}),
rule({
why: "models.dev reports gpt-5-pro output as 272000 (a duplicate of the input sub-limit); the actual max output is 128000.",
match: { id: "gpt-5-pro" },
set: { maxTokens: 128000 },
}),
],
});