| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| import assert from 'node:assert/strict'; |
| import { createHash, randomUUID } from 'node:crypto'; |
| import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises'; |
| import { stripTypeScriptTypes } from 'node:module'; |
| import { dirname } from 'node:path'; |
| import { fileURLToPath, pathToFileURL } from 'node:url'; |
| |
| // The projection lives in @maka/core because the Runtime Host runs it too. |
| // `prepare` fires before any workspace builds, so read the TypeScript source |
| // rather than a dist build that does not exist yet. |
| const projection = await loadTypeScriptModule( |
| await readFile( |
| fileURLToPath(new URL('../packages/core/src/models-dev-projection.ts', import.meta.url)), |
| 'utf8', |
| ), |
| ); |
| const { selectModelsDevCatalog, assertModelsDevProvider, collectProjectionRemovals } = projection; |
| const SOURCE_URL = projection.MODELS_DEV_SOURCE_URL; |
| export const PROVIDERS = projection.MODELS_DEV_PROVIDERS; |
| export const toMetadata = projection.projectModelsDevModel; |
| const DEFAULT_SNAPSHOT = 'scripts/model-metadata/models-dev-api.snapshot.json'; |
| const DEFAULT_OUTPUT = 'packages/core/src/model-metadata.generated.ts'; |
| const DEFAULT_PRICING_OUTPUT = 'packages/runtime/src/telemetry/model-pricing.generated.ts'; |
| // models.dev cost fields describe the catalog provider's public API. They are |
| // not automatically the rate a user pays on an OAuth, free, subscription, or |
| // plan access path. Such paths stay unpriced unless builtin-pricing.ts carries |
| // an explicit rate for that exact path. |
| export const PRICING_EXCLUDED_PROVIDER_TYPES = new Set([ |
| 'alibaba-coding-plan-cn', |
| 'alibaba-coding-plan', |
| 'alibaba-token-plan-cn', |
| 'alibaba-token-plan', |
| 'github-copilot', |
| 'kimi-coding-plan', |
| 'minimax-coding-plan', |
| 'MiniMax-cn', |
| 'opencode-free', |
| 'opencode-go', |
| 'stepfun-ai-step-plan', |
| 'stepfun-step-plan', |
| 'tencent-coding-plan', |
| 'tencent-token-plan', |
| 'volcengine-agent-plan', |
| 'volcengine-coding-plan', |
| 'xiaomi-token-plan-ams', |
| 'xiaomi-token-plan-cn', |
| 'xiaomi-token-plan-sgp', |
| 'zai-coding-plan', |
| ]); |
| |
| export async function main(argv = process.argv) { |
| const refreshInputPath = option('--refresh-input', argv); |
| const snapshotPath = option('--snapshot', argv) ?? DEFAULT_SNAPSHOT; |
| const outputPath = option('--output', argv) ?? DEFAULT_OUTPUT; |
| const pricingOutputPath = |
| option('--pricing-output', argv) ?? |
| (outputPath === DEFAULT_OUTPUT ? DEFAULT_PRICING_OUTPUT : undefined); |
| const refresh = argv.includes('--refresh'); |
| const check = argv.includes('--check'); |
| const drift = argv.includes('--drift'); |
| const acceptUpstreamRemovals = argv.includes('--accept-upstream-removals'); |
| if (drift && (refresh || check)) { |
| throw new Error('--drift reports without writing and cannot combine with --refresh or --check'); |
| } |
| if (refreshInputPath && !refresh && !drift) { |
| throw new Error('--refresh-input requires --refresh or --drift'); |
| } |
| if (acceptUpstreamRemovals && !refresh) { |
| throw new Error('--accept-upstream-removals requires --refresh'); |
| } |
| if (drift) { |
| const report = await collectDrift(await loadSnapshot(snapshotPath), refreshInputPath); |
| process.stdout.write(`${formatDrift(report)}\n`); |
| return report; |
| } |
| |
| const source = refresh |
| ? await refreshSnapshot(snapshotPath, refreshInputPath, { acceptUpstreamRemovals }) |
| : await loadSnapshot(snapshotPath); |
| const { |
| metadata: generated, |
| pricing: generatedPricing, |
| providerFacts: generatedProviders, |
| providerOverrides: generatedModelProviderOverrides, |
| } = source.projection; |
| if (check) { |
| await assertGeneratedOutputs(outputPath, pricingOutputPath, source); |
| if (source.snapshotWrite) await replaceFilesTransactionally([source.snapshotWrite]); |
| return; |
| } |
| |
| const providerTypeUnion = Object.keys(PROVIDERS).map(JSON.stringify).join(' | '); |
| const lines = [ |
| ...snapshotHeader( |
| '// Do not edit by hand; put access-path-specific facts in model-metadata.ts.', |
| source, |
| ), |
| "import type { ModelMetadata } from './model-metadata.js';", |
| '', |
| `export const GENERATED_MODELS_DEV_METADATA: Record<${providerTypeUnion}, Record<string, ModelMetadata>> = {`, |
| ]; |
| for (const [provider, models] of Object.entries(generated)) { |
| lines.push(` ${JSON.stringify(provider)}: {`); |
| for (const [id, metadata] of Object.entries(models)) { |
| lines.push(` ${JSON.stringify(id)}: ${JSON.stringify(metadata)},`); |
| } |
| lines.push(' },'); |
| } |
| lines.push('};', ''); |
| lines.push( |
| `export const GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES: Record<${providerTypeUnion}, Record<string, { adapter: import('./provider-registry.js').ProviderRuntimeAdapter; baseUrl?: string }>> = {`, |
| ); |
| for (const [provider, overrides] of Object.entries(generatedModelProviderOverrides)) { |
| lines.push( |
| ` ${JSON.stringify(provider)}: ${JSON.stringify(normalizeRuntimeOverrides(overrides))},`, |
| ); |
| } |
| lines.push('};', ''); |
| lines.push( |
| `export const GENERATED_MODELS_DEV_PROVIDER_FACTS: Record<${providerTypeUnion}, { id: string; name: string; api?: string; doc: string }> = {`, |
| ); |
| for (const [provider, facts] of Object.entries(generatedProviders)) { |
| lines.push(` ${JSON.stringify(provider)}: ${JSON.stringify(facts)},`); |
| } |
| lines.push('};', ''); |
| const metadataText = completeGeneratedModule(lines.join('\n')); |
| const writes = [{ path: outputPath, text: metadataText }]; |
| if (pricingOutputPath) { |
| const pricingText = completeGeneratedModule(buildPricingModule(generatedPricing, source)); |
| writes.push({ path: pricingOutputPath, text: pricingText }); |
| } |
| if (source.snapshotWrite) writes.push(source.snapshotWrite); |
| await replaceFilesTransactionally(writes); |
| } |
| |
| // `options.onReject` decides what an unprojectable provider or model costs. A |
| // refresh has none, so the first bad shape aborts the whole snapshot rather |
| // than silently committing a catalog with a hole in it. The drift report |
| // passes one, because there a bad shape is the finding it exists to print and |
| // must not stop it comparing everything else. |
| function buildProjection(catalog, options = {}) { |
| const onReject = options.onReject; |
| const metadata = {}; |
| const pricing = []; |
| const providerFacts = {}; |
| const providerOverrides = {}; |
| for (const [providerType, sourceId] of Object.entries(PROVIDERS)) { |
| const provider = catalog[sourceId]; |
| try { |
| assertModelsDevProvider(sourceId, provider); |
| } catch (error) { |
| if (!onReject) throw error; |
| onReject('provider', providerType, error); |
| continue; |
| } |
| providerFacts[providerType] = { |
| id: provider.id, |
| name: provider.name, |
| ...(typeof provider.api === 'string' ? { api: provider.api } : {}), |
| doc: provider.doc, |
| }; |
| metadata[providerType] = {}; |
| providerOverrides[providerType] = {}; |
| const priced = !PRICING_EXCLUDED_PROVIDER_TYPES.has(providerType); |
| const models = Object.entries(provider.models).sort(([left], [right]) => |
| left.localeCompare(right), |
| ); |
| for (const [id, model] of models) { |
| let projected; |
| try { |
| projected = { |
| metadata: toMetadata(sourceId, id, provider, model), |
| override: |
| model.provider === undefined |
| ? undefined |
| : toModelProviderOverride(sourceId, id, model.provider), |
| pricing: priced ? toPricing(providerType, id, model) : undefined, |
| }; |
| } catch (error) { |
| if (!onReject) throw error; |
| onReject('model', `${providerType}/${id}`, error); |
| continue; |
| } |
| metadata[providerType][id] = projected.metadata; |
| if (projected.override !== undefined) |
| providerOverrides[providerType][id] = projected.override; |
| if (projected.pricing !== undefined) pricing.push(projected.pricing); |
| } |
| } |
| |
| return { metadata, pricing, providerFacts, providerOverrides }; |
| } |
| |
| async function readUpstream(refreshInputPath) { |
| if (refreshInputPath) { |
| return { |
| text: await readFile(refreshInputPath, 'utf8'), |
| etag: null, |
| retrievedAt: new Date().toISOString(), |
| }; |
| } |
| const response = await fetch(SOURCE_URL, { signal: AbortSignal.timeout(10_000) }); |
| if (!response.ok) throw new Error(`models.dev returned HTTP ${response.status}`); |
| return { |
| text: await response.text(), |
| etag: response.headers.get('etag'), |
| retrievedAt: new Date(response.headers.get('date') ?? Date.now()).toISOString(), |
| }; |
| } |
| |
| async function refreshSnapshot(snapshotPath, refreshInputPath, options = {}) { |
| const { text: sourceText, etag: sourceEtag, retrievedAt } = await readUpstream(refreshInputPath); |
| const projection = buildProjection(selectModelsDevCatalog(JSON.parse(sourceText))); |
| if (!options.acceptUpstreamRemovals) { |
| const previous = await loadSnapshotIfPresent(snapshotPath); |
| if (previous) assertProjectionDoesNotShrink(previous.projection, projection); |
| } |
| const projectionText = JSON.stringify(projection); |
| const snapshot = { |
| formatVersion: 1, |
| sourceUrl: SOURCE_URL, |
| origin: { |
| kind: 'models-dev-response', |
| retrievedAt, |
| etag: sourceEtag, |
| responseSha256: sha256(sourceText), |
| }, |
| projectionSha256: sha256(projectionText), |
| projection, |
| }; |
| await mkdir(dirname(snapshotPath), { recursive: true }); |
| return { |
| projection, |
| snapshotDigest: snapshot.projectionSha256, |
| snapshotLabel: snapshotPath, |
| snapshotWrite: { path: snapshotPath, text: `${JSON.stringify(snapshot, null, 2)}\n` }, |
| }; |
| } |
| |
| function assertProjectionDoesNotShrink(previous, next) { |
| const removals = collectProjectionRemovals(previous, next); |
| if (removals.length === 0) return; |
| |
| throw new Error( |
| `models.dev refresh would remove committed projection paths: ${removals.join(', ')}; inspect the upstream change and rerun with --accept-upstream-removals to acknowledge it`, |
| ); |
| } |
| |
| // `--check` only proves the generated modules match the committed snapshot. |
| // Nothing compared that snapshot against models.dev, which is how it stayed |
| // weeks behind upstream without anything reporting it. This walks the two one |
| // model at a time, so a shape the projector rejects becomes its own finding |
| // instead of aborting the whole comparison the way a refresh does. |
| const DRIFT_LIST_LIMIT = 20; |
| |
| // Every section of the projection, flattened to one value per entity. The |
| // report reads sections through this table instead of naming them itself, |
| // which is how it went out comparing only two of the four. A section added to |
| // buildProjection is compared here without touching the comparison. |
| const PROJECTION_SECTIONS = { |
| metadata: entitiesByProviderAndModel, |
| // modelKey is `${providerType}:${id}`, so replacing the first colon yields |
| // the label every other section already uses. |
| pricing: (section) => new Map(section.map((entry) => [entry.modelKey.replace(':', '/'), entry])), |
| providerFacts: (section) => new Map(Object.entries(section)), |
| providerOverrides: entitiesByProviderAndModel, |
| }; |
| |
| async function collectDrift(snapshot, refreshInputPath) { |
| const rejectedProviders = []; |
| const rejectedModels = []; |
| const rejected = new Set(); |
| // The raw catalog, not the selected one: a provider that vanished upstream |
| // is the report's most important finding, and selection throws on it. |
| const upstream = buildProjection(JSON.parse((await readUpstream(refreshInputPath)).text), { |
| onReject: (kind, label, error) => { |
| rejected.add(label); |
| (kind === 'provider' ? rejectedProviders : rejectedModels).push(`${label}: ${error.message}`); |
| }, |
| }); |
| const previous = projectionEntities(snapshot.projection); |
| const next = projectionEntities(upstream); |
| const report = { rejectedProviders, rejectedModels, added: [], removed: [], changed: [] }; |
| for (const label of [...new Set([...previous.keys(), ...next.keys()])].sort()) { |
| const before = previous.get(label); |
| const after = next.get(label); |
| if (before === undefined) { |
| report.added.push(label); |
| continue; |
| } |
| if (after === undefined) { |
| // A shape the projector rejected already has its own finding above. It |
| // is not upstream saying the entity is gone. |
| if (!isRejected(label, rejected)) report.removed.push(label); |
| continue; |
| } |
| const fields = driftedFields(before, after); |
| if (fields.length > 0) report.changed.push(`${label}: ${fields.join(', ')}`); |
| } |
| const drifted = Object.values(report).some((entries) => entries.length > 0); |
| return { ...report, drifted }; |
| } |
| |
| function entitiesByProviderAndModel(section) { |
| const entities = new Map(); |
| for (const [providerType, models] of Object.entries(section)) { |
| for (const [id, value] of Object.entries(models)) entities.set(`${providerType}/${id}`, value); |
| } |
| return entities; |
| } |
| |
| function projectionEntities(projection) { |
| const entities = new Map(); |
| for (const [section, flatten] of Object.entries(PROJECTION_SECTIONS)) { |
| for (const [label, value] of flatten(projection[section])) { |
| const entity = entities.get(label); |
| if (entity) entity[section] = value; |
| else entities.set(label, { [section]: value }); |
| } |
| } |
| return entities; |
| } |
| |
| function isRejected(label, rejected) { |
| if (rejected.has(label)) return true; |
| const slash = label.indexOf('/'); |
| return slash !== -1 && rejected.has(label.slice(0, slash)); |
| } |
| |
| function driftedFields(previous, next) { |
| const fields = []; |
| for (const section of Object.keys(PROJECTION_SECTIONS)) { |
| const before = previous[section]; |
| const after = next[section]; |
| if (sameValue(before, after)) continue; |
| if (isPlainObject(before) && isPlainObject(after)) { |
| const keys = [...new Set([...Object.keys(before), ...Object.keys(after)])].sort(); |
| for (const key of keys) { |
| if (!sameValue(before[key], after[key])) fields.push(`${section}.${key}`); |
| } |
| continue; |
| } |
| fields.push(section); |
| } |
| return fields; |
| } |
| |
| function isPlainObject(value) { |
| return typeof value === 'object' && value !== null && !Array.isArray(value); |
| } |
| |
| // Both sides are projector output, so their keys are already in one order. |
| function sameValue(left, right) { |
| return JSON.stringify(left) === JSON.stringify(right); |
| } |
| |
| function formatDrift(report) { |
| const lines = []; |
| for (const [label, entries] of [ |
| ['providers the projector rejects', report.rejectedProviders], |
| ['models the projector rejects', report.rejectedModels], |
| ['entries upstream has and the snapshot does not', report.added], |
| ['entries the snapshot has and upstream does not', report.removed], |
| ['entries whose projection changed', report.changed], |
| ]) { |
| if (entries.length === 0) continue; |
| lines.push(`${label}: ${entries.length}`); |
| for (const entry of entries.slice(0, DRIFT_LIST_LIMIT)) lines.push(` ${entry}`); |
| if (entries.length > DRIFT_LIST_LIMIT) { |
| lines.push(` ... and ${entries.length - DRIFT_LIST_LIMIT} more`); |
| } |
| } |
| if (!report.drifted) return `${SOURCE_URL} matches the committed snapshot.`; |
| return [`${SOURCE_URL} has drifted from the committed snapshot.`, ...lines].join('\n'); |
| } |
| |
| async function replaceFilesTransactionally(writes) { |
| if (new Set(writes.map((write) => write.path)).size !== writes.length) { |
| throw new Error('model metadata outputs must use distinct paths'); |
| } |
| // Later workspace prebuilds may sync after an earlier workspace has already |
| // compiled. Preserve byte-identical targets so their mtimes cannot make that |
| // already-built workspace look stale. |
| const changedWrites = ( |
| await Promise.all( |
| writes.map(async (write) => { |
| try { |
| return (await readFile(write.path)).equals(Buffer.from(write.text)) ? undefined : write; |
| } catch (error) { |
| if (error?.code === 'ENOENT') return write; |
| throw error; |
| } |
| }), |
| ) |
| ).filter(Boolean); |
| if (changedWrites.length === 0) return; |
| const transactionId = `${process.pid}-${randomUUID()}`; |
| const entries = changedWrites.map((write) => ({ |
| ...write, |
| stagedPath: `${write.path}.tmp-${transactionId}`, |
| backupPath: `${write.path}.bak-${transactionId}`, |
| hadOriginal: false, |
| installed: false, |
| })); |
| |
| let committed = false; |
| try { |
| // Stage every byte before replacing any target. Missing/unwritable output |
| // directories therefore leave the existing snapshot and outputs intact. |
| const stageResults = await Promise.allSettled( |
| entries.map((entry) => writeFile(entry.stagedPath, entry.text, { flag: 'wx' })), |
| ); |
| const stageFailure = stageResults.find((result) => result.status === 'rejected'); |
| if (stageFailure) throw stageFailure.reason; |
| for (const entry of entries) { |
| try { |
| await rename(entry.path, entry.backupPath); |
| entry.hadOriginal = true; |
| } catch (error) { |
| if (error?.code !== 'ENOENT') throw error; |
| } |
| await rename(entry.stagedPath, entry.path); |
| entry.installed = true; |
| } |
| committed = true; |
| } catch (error) { |
| let rollbackError; |
| for (const entry of [...entries].reverse()) { |
| try { |
| if (entry.installed) await rm(entry.path, { force: true }); |
| if (entry.hadOriginal) await rename(entry.backupPath, entry.path); |
| } catch (candidate) { |
| rollbackError ??= candidate; |
| } |
| } |
| if (rollbackError) { |
| throw new AggregateError( |
| [error, rollbackError], |
| 'model metadata transaction rollback failed', |
| ); |
| } |
| throw error; |
| } finally { |
| const cleanupPaths = entries.flatMap((entry) => [ |
| entry.stagedPath, |
| ...(committed ? [entry.backupPath] : []), |
| ]); |
| await Promise.all(cleanupPaths.map((path) => rm(path, { force: true }).catch(() => {}))); |
| } |
| } |
| |
| async function loadSnapshot(snapshotPath) { |
| const snapshot = JSON.parse(await readFile(snapshotPath, 'utf8')); |
| if ( |
| snapshot?.formatVersion !== 1 || |
| snapshot.sourceUrl !== SOURCE_URL || |
| !snapshot.origin || |
| typeof snapshot.origin !== 'object' || |
| Array.isArray(snapshot.origin) || |
| typeof snapshot.projectionSha256 !== 'string' || |
| !snapshot.projection || |
| typeof snapshot.projection !== 'object' || |
| Array.isArray(snapshot.projection) |
| ) { |
| throw new Error(`models.dev snapshot ${snapshotPath} has an unsupported shape`); |
| } |
| const actualDigest = sha256(JSON.stringify(snapshot.projection)); |
| if (actualDigest !== snapshot.projectionSha256) { |
| throw new Error( |
| `models.dev snapshot ${snapshotPath} digest mismatch: expected ${snapshot.projectionSha256}, got ${actualDigest}`, |
| ); |
| } |
| return { |
| projection: snapshot.projection, |
| snapshotDigest: actualDigest, |
| snapshotLabel: snapshotPath, |
| }; |
| } |
| |
| async function loadSnapshotIfPresent(snapshotPath) { |
| try { |
| return await loadSnapshot(snapshotPath); |
| } catch (error) { |
| if (error?.code === 'ENOENT') return undefined; |
| throw error; |
| } |
| } |
| |
| function sha256(value) { |
| return createHash('sha256').update(value).digest('hex'); |
| } |
| |
| function completeGeneratedModule(source) { |
| return source.endsWith('\n') ? source : `${source}\n`; |
| } |
| |
| async function assertGeneratedOutputs(metadataPath, pricingPath, source) { |
| const metadataSource = await readFile(metadataPath, 'utf8'); |
| assert.ok( |
| metadataSource.includes( |
| `// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`, |
| ), |
| `${metadataPath} is stale; run npm run sync:model-metadata`, |
| ); |
| const metadataModule = await loadTypeScriptModule(metadataSource); |
| assert.deepEqual( |
| metadataModule.GENERATED_MODELS_DEV_METADATA, |
| source.projection.metadata, |
| `${metadataPath} is stale; run npm run sync:model-metadata`, |
| ); |
| assert.deepEqual( |
| metadataModule.GENERATED_MODELS_DEV_MODEL_PROVIDER_OVERRIDES, |
| Object.fromEntries( |
| Object.entries(source.projection.providerOverrides).map(([provider, overrides]) => [ |
| provider, |
| normalizeRuntimeOverrides(overrides), |
| ]), |
| ), |
| `${metadataPath} is stale; run npm run sync:model-metadata`, |
| ); |
| assert.deepEqual( |
| metadataModule.GENERATED_MODELS_DEV_PROVIDER_FACTS, |
| source.projection.providerFacts, |
| `${metadataPath} is stale; run npm run sync:model-metadata`, |
| ); |
| if (!pricingPath) return; |
| const pricingSource = await readFile(pricingPath, 'utf8'); |
| assert.ok( |
| pricingSource.includes( |
| `// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`, |
| ), |
| `${pricingPath} is stale; run npm run sync:model-metadata`, |
| ); |
| const pricingModule = await loadTypeScriptModule(pricingSource); |
| assert.deepEqual( |
| pricingModule.GENERATED_MODEL_PRICING, |
| source.projection.pricing, |
| `${pricingPath} is stale; run npm run sync:model-metadata`, |
| ); |
| } |
| |
| export async function loadTypeScriptModule(source) { |
| const javascript = stripTypeScriptTypes(source, { mode: 'strip' }); |
| return import(`data:text/javascript;base64,${Buffer.from(javascript).toString('base64')}`); |
| } |
| |
| function toModelProviderOverride(providerId, modelId, override) { |
| if ( |
| !override || |
| typeof override !== 'object' || |
| typeof override.npm !== 'string' || |
| (override.api !== undefined && typeof override.api !== 'string') |
| ) { |
| throw new Error( |
| `models.dev model ${providerId}/${modelId} has an unsupported provider override`, |
| ); |
| } |
| return { npm: override.npm, ...(override.api ? { api: override.api } : {}) }; |
| } |
| |
| function normalizeRuntimeOverrides(overrides) { |
| const adapters = { |
| '@ai-sdk/anthropic': { kind: 'anthropic', auth: 'api-key', normalizeBaseUrl: true }, |
| '@ai-sdk/google': { kind: 'google', normalizeBaseUrl: false }, |
| '@ai-sdk/openai': { kind: 'openai' }, |
| '@ai-sdk/openai-compatible': { kind: 'openai-compatible', name: 'provider' }, |
| }; |
| return Object.fromEntries( |
| Object.entries(overrides).map(([modelId, override]) => { |
| if (!Object.hasOwn(adapters, override.npm)) { |
| throw new Error(`models.dev model ${modelId} uses unsupported SDK ${override.npm}`); |
| } |
| return [ |
| modelId, |
| { adapter: adapters[override.npm], ...(override.api ? { baseUrl: override.api } : {}) }, |
| ]; |
| }), |
| ); |
| } |
| |
| export function toPricing(providerType, modelId, model) { |
| const cost = model?.cost; |
| if (cost === undefined) return undefined; |
| if (!cost || typeof cost !== 'object' || Array.isArray(cost)) { |
| throw new Error(`models.dev model ${providerType}/${modelId} has an unsupported cost shape`); |
| } |
| // PricingConfig and computeCost() currently represent one flat rate. Do |
| // not publish a base rate for a model whose actual price depends on input |
| // volume or an explicit tier table; that would systematically undercharge |
| // long-context requests until the runtime can select a tier by usage. |
| if ( |
| Object.prototype.hasOwnProperty.call(cost, 'context_over_200k') || |
| Object.prototype.hasOwnProperty.call(cost, 'tiers') |
| ) { |
| return undefined; |
| } |
| const inputUsdPer1M = priceNumber(providerType, modelId, cost.input, 'input'); |
| const outputUsdPer1M = priceNumber(providerType, modelId, cost.output, 'output'); |
| const cacheReadUsdPer1M = optionalPriceNumber( |
| providerType, |
| modelId, |
| cost.cache_read, |
| 'cache_read', |
| ); |
| const cacheWriteUsdPer1M = optionalPriceNumber( |
| providerType, |
| modelId, |
| cost.cache_write, |
| 'cache_write', |
| ); |
| return { |
| modelKey: `${providerType}:${modelId}`, |
| inputUsdPer1M, |
| outputUsdPer1M, |
| ...(cacheReadUsdPer1M !== undefined ? { cacheReadUsdPer1M } : {}), |
| ...(cacheWriteUsdPer1M !== undefined ? { cacheWriteUsdPer1M } : {}), |
| }; |
| } |
| |
| function priceNumber(providerType, modelId, value, field) { |
| if (typeof value !== 'number' || !Number.isFinite(value) || value < 0) { |
| throw new Error(`models.dev model ${providerType}/${modelId} has an unsupported cost.${field}`); |
| } |
| return value; |
| } |
| |
| function optionalPriceNumber(providerType, modelId, value, field) { |
| return value === undefined ? undefined : priceNumber(providerType, modelId, value, field); |
| } |
| |
| // Every generated file names the exact repository-contained input projection. |
| // The copyright line is required by models.dev's MIT license and is repeated |
| // in LICENSE under THIRD-PARTY COMPONENTS. The digest binds generated output |
| // to the snapshot bytes that ship in the same source archive. |
| function snapshotHeader(handEditLine, source) { |
| return [ |
| `// Generated by scripts/sync-model-metadata.mjs from ${SOURCE_URL}.`, |
| '// models.dev is the upstream refresh source; this committed snapshot is the build input.', |
| `// Snapshot: ${source.snapshotLabel} (SHA-256 ${source.snapshotDigest}).`, |
| '// Upstream: anomalyco/models.dev (https://github.com/anomalyco/models.dev), MIT,', |
| '// Copyright (c) 2025 models.dev. See LICENSE, THIRD-PARTY COMPONENTS.', |
| handEditLine, |
| ]; |
| } |
| |
| function buildPricingModule(pricing, source) { |
| const lines = [ |
| ...snapshotHeader( |
| '// Do not edit by hand; special access-path pricing belongs in builtin-pricing.ts.', |
| source, |
| ), |
| "import type { PricingConfig } from '@maka/core/usage-stats/types';", |
| '', |
| 'export const GENERATED_MODEL_PRICING: readonly PricingConfig[] = [', |
| ]; |
| for (const entry of pricing) lines.push(` ${JSON.stringify(entry)},`); |
| lines.push('];', ''); |
| return lines.join('\n'); |
| } |
| |
| if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| // Drift is a finding, not a crash. Exit 2 so a caller can tell "upstream |
| // moved" from "this command failed"; both reported 1 before, which made the |
| // difference unreadable to the one job that has to act on it. |
| if ((await main())?.drifted) process.exitCode = 2; |
| } |
| |
| function option(name, argv) { |
| const index = argv.indexOf(name); |
| if (index === -1) return undefined; |
| const value = argv[index + 1]; |
| if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`); |
| return value; |
| } |