feat(ai): provider factories, per-provider catalogs, createProvider (phase 3)
Auth helpers in src/auth/helpers.ts: envApiKeyAuth() (stored key wins, then env vars in order, with secret-prompt login) and lazyOAuth() (flow loads on first use through bundler-opaque dynamic imports in utils/oauth/load.ts; the OAuthAuth flow exports land in phase 4). There is no OAuth factory toggle: providers that support OAuth always attach it, advertising costs nothing until login/refresh runs. createProvider() in models.ts builds providers from parts: single API implementation or a map dispatched on model.api (mixed-API providers like opencode and github-copilot); unknown api yields a stream error. generate-models.ts now emits one providers/<id>.models.ts catalog per provider (35 files, biome-excluded like models.generated.ts) and models.generated.ts becomes a generated aggregator, so importing one provider factory pulls one catalog. Typed getModel globals unchanged. One factory per built-in provider under src/providers/: envApiKeyAuth for standard providers, OAuth for anthropic/openai-codex/github-copilot, ambient ApiKeyAuth for amazon-bedrock (AWS env/profile/IAM) and google-vertex (explicit key or ADC+project+location). providers/all.ts: builtinProviders(), builtinModels(), getBuiltin* re-exports. fauxProvider() factory returns a real Provider for tests; legacy registerFauxProvider() unchanged.
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { writeFileSync } from "fs";
|
||||
import { readdirSync, rmSync, writeFileSync } from "fs";
|
||||
import { join, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import {
|
||||
@@ -2103,62 +2103,80 @@ async function generateModels() {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate TypeScript file
|
||||
let output = `// This file is auto-generated by scripts/generate-models.ts
|
||||
// Generate TypeScript files: one catalog per provider plus an aggregator
|
||||
const generatedHeader = `// This file is auto-generated by scripts/generate-models.ts
|
||||
// Do not edit manually - run 'npm run generate-models' to update
|
||||
|
||||
import type { Model } from "./types.ts";
|
||||
|
||||
export const MODELS = {
|
||||
`;
|
||||
const catalogConstName = (providerId: string) => `${providerId.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_MODELS`;
|
||||
|
||||
// Generate provider sections (sorted for deterministic output)
|
||||
const sortedProviderIds = Object.keys(providers).sort();
|
||||
for (const providerId of sortedProviderIds) {
|
||||
const models = providers[providerId];
|
||||
output += `\t${JSON.stringify(providerId)}: {\n`;
|
||||
|
||||
const sortedModelIds = Object.keys(models).sort();
|
||||
for (const modelId of sortedModelIds) {
|
||||
const model = models[modelId];
|
||||
output += `\t\t"${model.id}": {\n`;
|
||||
output += `\t\t\tid: "${model.id}",\n`;
|
||||
output += `\t\t\tname: "${model.name}",\n`;
|
||||
output += `\t\t\tapi: "${model.api}",\n`;
|
||||
output += `\t\t\tprovider: "${model.provider}",\n`;
|
||||
if (model.baseUrl !== undefined) {
|
||||
output += `\t\t\tbaseUrl: "${model.baseUrl}",\n`;
|
||||
}
|
||||
if (model.headers) {
|
||||
output += `\t\t\theaders: ${JSON.stringify(model.headers)},\n`;
|
||||
}
|
||||
if (model.compat) {
|
||||
output += ` compat: ${JSON.stringify(model.compat)},
|
||||
`;
|
||||
}
|
||||
output += `\t\t\treasoning: ${model.reasoning},\n`;
|
||||
if (model.thinkingLevelMap) {
|
||||
output += `\t\t\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
|
||||
}
|
||||
output += `\t\t\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
|
||||
output += `\t\t\tcost: {\n`;
|
||||
output += `\t\t\t\tinput: ${model.cost.input},\n`;
|
||||
output += `\t\t\t\toutput: ${model.cost.output},\n`;
|
||||
output += `\t\t\t\tcacheRead: ${model.cost.cacheRead},\n`;
|
||||
output += `\t\t\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
|
||||
output += `\t\t\t},\n`;
|
||||
output += `\t\t\tcontextWindow: ${model.contextWindow},\n`;
|
||||
output += `\t\t\tmaxTokens: ${model.maxTokens},\n`;
|
||||
output += `\t\t} satisfies Model<"${model.api}">,\n`;
|
||||
function emitModel(model: Model<any>, indent: string): string {
|
||||
let output = `${indent}"${model.id}": {\n`;
|
||||
output += `${indent}\tid: "${model.id}",\n`;
|
||||
output += `${indent}\tname: "${model.name}",\n`;
|
||||
output += `${indent}\tapi: "${model.api}",\n`;
|
||||
output += `${indent}\tprovider: "${model.provider}",\n`;
|
||||
if (model.baseUrl !== undefined) {
|
||||
output += `${indent}\tbaseUrl: "${model.baseUrl}",\n`;
|
||||
}
|
||||
|
||||
output += `\t},\n`;
|
||||
if (model.headers) {
|
||||
output += `${indent}\theaders: ${JSON.stringify(model.headers)},\n`;
|
||||
}
|
||||
if (model.compat) {
|
||||
output += `${indent}\tcompat: ${JSON.stringify(model.compat)},\n`;
|
||||
}
|
||||
output += `${indent}\treasoning: ${model.reasoning},\n`;
|
||||
if (model.thinkingLevelMap) {
|
||||
output += `${indent}\tthinkingLevelMap: ${JSON.stringify(model.thinkingLevelMap)},\n`;
|
||||
}
|
||||
output += `${indent}\tinput: [${model.input.map(i => `"${i}"`).join(", ")}],\n`;
|
||||
output += `${indent}\tcost: {\n`;
|
||||
output += `${indent}\t\tinput: ${model.cost.input},\n`;
|
||||
output += `${indent}\t\toutput: ${model.cost.output},\n`;
|
||||
output += `${indent}\t\tcacheRead: ${model.cost.cacheRead},\n`;
|
||||
output += `${indent}\t\tcacheWrite: ${model.cost.cacheWrite},\n`;
|
||||
output += `${indent}\t},\n`;
|
||||
output += `${indent}\tcontextWindow: ${model.contextWindow},\n`;
|
||||
output += `${indent}\tmaxTokens: ${model.maxTokens},\n`;
|
||||
output += `${indent}} satisfies Model<"${model.api}">,\n`;
|
||||
return output;
|
||||
}
|
||||
|
||||
output += `} as const;
|
||||
`;
|
||||
const sortedProviderIds = Object.keys(providers).sort();
|
||||
const providersDir = join(packageRoot, "src/providers");
|
||||
|
||||
// Write file
|
||||
// Remove stale per-provider catalogs
|
||||
for (const entry of readdirSync(providersDir)) {
|
||||
if (entry.endsWith(".models.ts")) {
|
||||
rmSync(join(providersDir, entry));
|
||||
}
|
||||
}
|
||||
|
||||
// Per-provider catalogs (sorted for deterministic output)
|
||||
for (const providerId of sortedProviderIds) {
|
||||
const models = providers[providerId];
|
||||
let output = generatedHeader;
|
||||
output += `import type { Model } from "../types.ts";\n\n`;
|
||||
output += `export const ${catalogConstName(providerId)} = {\n`;
|
||||
const sortedModelIds = Object.keys(models).sort();
|
||||
for (const modelId of sortedModelIds) {
|
||||
output += emitModel(models[modelId], "\t");
|
||||
}
|
||||
output += `} as const;\n`;
|
||||
writeFileSync(join(providersDir, `${providerId}.models.ts`), output);
|
||||
}
|
||||
console.log(`Generated ${sortedProviderIds.length} catalogs under src/providers/`);
|
||||
|
||||
// Aggregator
|
||||
let output = generatedHeader;
|
||||
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 += `\t${JSON.stringify(providerId)}: ${catalogConstName(providerId)},\n`;
|
||||
}
|
||||
output += `} as const;\n`;
|
||||
writeFileSync(join(packageRoot, "src/models.generated.ts"), output);
|
||||
console.log("Generated src/models.generated.ts");
|
||||
|
||||
|
||||
Reference in New Issue
Block a user