mirror of
https://github.com/badlogic/pi-mono.git
synced 2026-11-05 23:37:25 +00:00
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
197 lines
7.8 KiB
TypeScript
197 lines
7.8 KiB
TypeScript
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
mkdtempSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
renameSync,
|
|
rmSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import { join } from "node:path";
|
|
import type { Api, Model } from "../../src/types.ts";
|
|
import {
|
|
createModelDataManifest,
|
|
MODEL_DATA_MANIFEST_FILE,
|
|
type ModelDataStructure,
|
|
readModelDataProviderIds,
|
|
validateGeneratedModelData,
|
|
validateModelDataDirectory,
|
|
} from "../model-data.ts";
|
|
import type { GeneratedModel } from "./dsl.ts";
|
|
|
|
export interface OutputOptions {
|
|
packageRoot: string;
|
|
/** Only refresh the ignored JSON data for providers already present in the committed catalog. */
|
|
dataOnly: boolean;
|
|
/** Skip the package sources and only write the public JSON catalog. */
|
|
jsonOnly: boolean;
|
|
jsonOutputDir: string | undefined;
|
|
pretty: boolean;
|
|
}
|
|
|
|
export type SortedCatalog = Record<string, Record<string, GeneratedModel>>;
|
|
|
|
/** Providers and models sorted by id, for deterministic output. */
|
|
export function sortCatalog(providers: ReadonlyMap<string, readonly GeneratedModel[]>): SortedCatalog {
|
|
const catalog: SortedCatalog = {};
|
|
for (const providerId of Array.from(providers.keys()).sort()) {
|
|
const models = providers.get(providerId) ?? [];
|
|
catalog[providerId] = {};
|
|
for (const model of [...models].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
catalog[providerId][model.id] = model;
|
|
}
|
|
}
|
|
return catalog;
|
|
}
|
|
|
|
const GENERATED_HEADER = `// This file is auto-generated by scripts/generate-models.ts
|
|
// Do not edit manually - run 'npm run generate-models' to update
|
|
|
|
`;
|
|
|
|
function catalogConstName(providerId: string): string {
|
|
return `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
|
|
}
|
|
|
|
export function writeCatalog(catalog: SortedCatalog, options: OutputOptions): void {
|
|
const sortedProviderIds = Object.keys(catalog);
|
|
const serializeJson = (value: unknown) => `${JSON.stringify(value, null, options.pretty ? 2 : undefined)}\n`;
|
|
const writeJson = (path: string, value: unknown) => writeFileSync(path, serializeJson(value));
|
|
const generatedDataProviderIds = options.dataOnly
|
|
? readModelDataProviderIds(options.packageRoot)
|
|
: sortedProviderIds;
|
|
const missingProviderIds = generatedDataProviderIds.filter((providerId) => !catalog[providerId]);
|
|
if (missingProviderIds.length > 0) {
|
|
throw new Error(`Cannot hydrate missing providers: ${missingProviderIds.join(", ")}`);
|
|
}
|
|
|
|
// Only the ignored internal data is grouped by API for type derivation. Public JSON catalog output stays flat.
|
|
const generatedDataProviders: Record<string, Record<string, Record<string, Model<Api>>>> = {};
|
|
const modelDataStructure: ModelDataStructure = {};
|
|
for (const providerId of generatedDataProviderIds) {
|
|
const models = catalog[providerId];
|
|
generatedDataProviders[providerId] = {};
|
|
modelDataStructure[providerId] = {};
|
|
const apiIds = Array.from(new Set(Object.values(models).map((model) => model.api))).sort();
|
|
for (const api of apiIds) {
|
|
generatedDataProviders[providerId][api] = {};
|
|
for (const [modelId, model] of Object.entries(models)) {
|
|
if (model.api !== api) continue;
|
|
generatedDataProviders[providerId][api][modelId] = model;
|
|
modelDataStructure[providerId][modelId] = api;
|
|
}
|
|
}
|
|
}
|
|
|
|
const generatedAt = new Date().toISOString();
|
|
|
|
if (!options.jsonOnly) {
|
|
// Stage and validate all provider values before replacing the current generated data.
|
|
const providersDir = join(options.packageRoot, "src/providers");
|
|
const dataDir = join(providersDir, "data");
|
|
const stagingRoot = mkdtempSync(join(providersDir, ".model-generation-"));
|
|
const stagedDataDir = join(stagingRoot, "data");
|
|
const previousDataDir = join(stagingRoot, "previous-data");
|
|
let restoreGeneratedCatalog: (() => void) | undefined;
|
|
try {
|
|
mkdirSync(stagedDataDir, { recursive: true });
|
|
const fileContents: Record<string, string> = {};
|
|
for (const providerId of generatedDataProviderIds) {
|
|
const filename = `${providerId}.json`;
|
|
const content = serializeJson(generatedDataProviders[providerId]);
|
|
fileContents[filename] = content;
|
|
writeFileSync(join(stagedDataDir, filename), content);
|
|
}
|
|
writeJson(
|
|
join(stagedDataDir, MODEL_DATA_MANIFEST_FILE),
|
|
createModelDataManifest(modelDataStructure, fileContents, generatedAt),
|
|
);
|
|
validateModelDataDirectory(modelDataStructure, stagedDataDir);
|
|
|
|
if (!options.dataOnly) {
|
|
const previousShardContents = new Map(
|
|
readdirSync(providersDir)
|
|
.filter((entry) => entry.endsWith(".models.ts"))
|
|
.map((entry) => [entry, readFileSync(join(providersDir, entry), "utf8")] as const),
|
|
);
|
|
const aggregatorPath = join(options.packageRoot, "src/models.generated.ts");
|
|
const previousAggregator = readFileSync(aggregatorPath, "utf8");
|
|
restoreGeneratedCatalog = () => {
|
|
for (const entry of readdirSync(providersDir)) {
|
|
if (entry.endsWith(".models.ts")) rmSync(join(providersDir, entry));
|
|
}
|
|
for (const [entry, content] of previousShardContents) {
|
|
writeFileSync(join(providersDir, entry), content);
|
|
}
|
|
writeFileSync(aggregatorPath, previousAggregator);
|
|
};
|
|
|
|
const generatedShardFiles = new Set<string>();
|
|
for (const providerId of sortedProviderIds) {
|
|
let output = GENERATED_HEADER;
|
|
output += `import values from "./data/${providerId}.json" with { type: "json" };\n`;
|
|
output += `import { flattenModelCatalog, type ModelCatalog } from "../model-catalog.ts";\n\n`;
|
|
output += `export const ${catalogConstName(providerId)}: ModelCatalog<typeof values, ${JSON.stringify(providerId)}> =\n`;
|
|
output += `\tflattenModelCatalog(${JSON.stringify(providerId)}, values);\n`;
|
|
const filename = `${providerId}.models.ts`;
|
|
generatedShardFiles.add(filename);
|
|
writeFileSync(join(providersDir, filename), output);
|
|
}
|
|
for (const entry of readdirSync(providersDir)) {
|
|
if (entry.endsWith(".models.ts") && !generatedShardFiles.has(entry)) rmSync(join(providersDir, entry));
|
|
}
|
|
|
|
let output = GENERATED_HEADER;
|
|
for (const providerId of sortedProviderIds) {
|
|
output += `import { ${catalogConstName(providerId)} } from "./providers/${providerId}.models.ts";\n`;
|
|
}
|
|
output += `\nexport const MODELS: {\n`;
|
|
for (const providerId of sortedProviderIds) {
|
|
output += `\treadonly ${JSON.stringify(providerId)}: typeof ${catalogConstName(providerId)};\n`;
|
|
}
|
|
output += `} = {\n`;
|
|
for (const providerId of sortedProviderIds) {
|
|
output += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
|
|
}
|
|
output += `};\n`;
|
|
writeFileSync(aggregatorPath, output);
|
|
console.log("Generated provider catalogs and src/models.generated.ts");
|
|
}
|
|
|
|
const hadPreviousData = existsSync(dataDir);
|
|
if (hadPreviousData) renameSync(dataDir, previousDataDir);
|
|
try {
|
|
renameSync(stagedDataDir, dataDir);
|
|
validateGeneratedModelData(options.packageRoot);
|
|
} catch (error) {
|
|
rmSync(dataDir, { recursive: true, force: true });
|
|
if (hadPreviousData && existsSync(previousDataDir)) renameSync(previousDataDir, dataDir);
|
|
throw error;
|
|
}
|
|
restoreGeneratedCatalog = undefined;
|
|
console.log(
|
|
options.dataOnly
|
|
? "Hydrated JSON model values under src/providers/data/"
|
|
: "Generated JSON model values under src/providers/data/",
|
|
);
|
|
} catch (error) {
|
|
restoreGeneratedCatalog?.();
|
|
throw error;
|
|
} finally {
|
|
rmSync(stagingRoot, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
if (options.jsonOutputDir) {
|
|
const providerOutputDir = join(options.jsonOutputDir, "providers");
|
|
rmSync(options.jsonOutputDir, { recursive: true, force: true });
|
|
mkdirSync(providerOutputDir, { recursive: true });
|
|
writeJson(join(options.jsonOutputDir, "models.json"), catalog);
|
|
writeJson(join(options.jsonOutputDir, "providers.json"), sortedProviderIds);
|
|
for (const providerId of sortedProviderIds) {
|
|
writeJson(join(providerOutputDir, `${providerId}.json`), catalog[providerId]);
|
|
}
|
|
console.log(`Generated JSON model catalog under ${options.jsonOutputDir}`);
|
|
}
|
|
}
|