#!/usr/bin/env node import { writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, ".."); const outputPath = path.join(projectRoot, "packages", "core", "models.json"); const sources = { litellm: { id: "litellm", name: "LiteLLM model prices and context window", url: "https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json" }, modelsDev: { id: "models.dev", name: "models.dev API", url: "https://models.dev/api.json" }, openrouter: { id: "openrouter", name: "OpenRouter models API", url: "https://openrouter.ai/api/v1/models" } }; const sourceOrder = ["models.dev", "litellm", "openrouter"]; const support1MContextThreshold = 1_000_000; const schemaVersion = 2; const firstPartyProviderAliases = new Map(Object.entries({ ai21: "ai21", alibaba: "alibaba", anthropic: "anthropic", amazon: "amazon", bedrock: "amazon", cohere: "cohere", deepseek: "deepseek", "deepseek-ai": "deepseek", gemini: "google", google: "google", "google-vertex": "google", llama: "meta-llama", meta: "meta-llama", "meta-llama": "meta-llama", "meta-llama3": "meta-llama", mistral: "mistral", mistralai: "mistral", minimax: "minimax", minimaxai: "minimax", moonshot: "moonshotai", moonshotai: "moonshotai", openai: "openai", perplexity: "perplexity", qwen: "alibaba", xai: "x-ai", "x-ai": "x-ai", zai: "z-ai", "z-ai": "z-ai", "zai-org": "z-ai", zhipuai: "z-ai" })); const providerHints = [ [/^(chatgpt|codex|dall-e|gpt-|gpt_|o[1345](?:-|$)|omni-|text-embedding-|tts-|whisper-)/i, "openai"], [/^claude-/i, "anthropic"], [/^(gemini-|imagen-|veo-)/i, "google"], [/^grok-/i, "x-ai"], [/^deepseek[-_]/i, "deepseek"], [/^kimi[-_]/i, "moonshotai"], [/^(glm-|charglm-|codegeex-|cogview-)/i, "z-ai"], [/^(qwen|qwq|wanx|wan[-_])/i, "alibaba"], [/^(llama-|codellama|meta-llama)/i, "meta-llama"], [/^(mistral|mixtral|codestral|devstral|magistral|ministral|pixtral|voxtral)/i, "mistral"], [/^(minimax|abab)/i, "minimax"], [/^command[-_]/i, "cohere"], [/^sonar(?:-|$)/i, "perplexity"], [/^(nova-|titan-|amazon\\.)/i, "amazon"] ]; async function main() { const [modelsDevPayload, liteLlmPayload, openRouterPayload] = await Promise.all([ fetchJson(sources.modelsDev.url), fetchJson(sources.litellm.url), fetchJson(sources.openrouter.url) ]); const entries = new Map(); ingestModelsDev(entries, modelsDevPayload); ingestLiteLlm(entries, liteLlmPayload); ingestOpenRouter(entries, openRouterPayload); const providerModelRecords = Array.from(entries.values()) .map(finalizeEntry) .sort((a, b) => a.id.localeCompare(b.id)); const models = dedupeModels(providerModelRecords); const payload = { schemaVersion, generatedAt: new Date().toISOString(), generatedBy: "scripts/generate-models-json.mjs", sources: Object.values(sources).map(({ id, name, url }) => ({ id, name, url })), summary: buildSummary(models, providerModelRecords.length), models }; await writeFile(outputPath, `${JSON.stringify(payload, null, 2)}\n`, "utf8"); console.log(`Wrote ${models.length} model records to ${path.relative(projectRoot, outputPath)}`); console.log(`Merged ${providerModelRecords.length - models.length} duplicate provider/model records`); console.log(`Providers: ${payload.summary.providerCount}`); console.log(`Models with >=1M context: ${payload.summary.modelsWith1MContext}`); console.log(`Models with image input: ${payload.summary.modelsWithImageInput}`); console.log(`Models with image output/generation: ${payload.summary.modelsWithImageOutput}`); } async function fetchJson(url) { const response = await fetch(url, { headers: { accept: "application/json" } }); if (!response.ok) { throw new Error(`Failed to fetch ${url}: HTTP ${response.status}`); } return response.json(); } function ingestModelsDev(entries, payload) { if (!isRecord(payload)) return; for (const [providerId, provider] of Object.entries(payload)) { if (!isRecord(provider) || !isRecord(provider.models)) continue; for (const [modelKey, model] of Object.entries(provider.models)) { if (!isRecord(model)) continue; const modelId = readString(model.id) || modelKey; const entry = ensureEntry(entries, providerId, modelId); const modalities = normalizeModalities(model.modalities); const limits = compactObject({ contextTokens: readNumber(model.limit?.context), inputTokens: readNumber(model.limit?.input), outputTokens: readNumber(model.limit?.output) }); const capabilities = compactObject({ attachments: readBoolean(model.attachment), audioInput: modalities.input.includes("audio"), audioOutput: modalities.output.includes("audio"), imageInput: modalities.input.includes("image"), imageOutput: modalities.output.includes("image"), interleaved: readBoolean(model.interleaved), openWeights: readBoolean(model.open_weights), pdfInput: modalities.input.includes("pdf"), reasoning: readBoolean(model.reasoning), structuredOutput: readBoolean(model.structured_output), temperature: readBoolean(model.temperature), toolCalling: readBoolean(model.tool_call), videoInput: modalities.input.includes("video") }); entry.sourceRecords.push(compactObject({ source: "models.dev", sourceUrl: sources.modelsDev.url, provider: providerId, providerName: readString(provider.name), providerApi: readString(provider.api), providerDoc: readString(provider.doc), model: modelId, modelKey, displayName: readString(model.name), family: readString(model.family), status: readString(model.status), metadata: compactObject({ experimental: readBoolean(model.experimental), knowledgeCutoff: readString(model.knowledge), lastUpdated: readString(model.last_updated), openWeights: readBoolean(model.open_weights), releaseDate: readString(model.release_date), reasoningOptions: model.reasoning_options }), limits, modalities, capabilities, pricing: pricingFromModelsDev(model.cost) })); } } } function ingestLiteLlm(entries, payload) { if (!isRecord(payload)) return; for (const [modelId, model] of Object.entries(payload)) { if (modelId === "sample_spec" || !isRecord(model)) continue; const provider = readString(model.litellm_provider) || "unknown"; const entry = ensureEntry(entries, provider, modelId); const mode = readString(model.mode); const modalities = inferLiteLlmModalities(model, mode); const limits = compactObject({ contextTokens: readNumber(model.max_input_tokens) ?? readNumber(model.max_tokens), inputTokens: readNumber(model.max_input_tokens), outputTokens: readNumber(model.max_output_tokens) ?? readNumber(model.max_tokens), maxTokens: readNumber(model.max_tokens), maxAudioLengthHours: readNumber(model.max_audio_length_hours), maxAudioPerPrompt: readNumber(model.max_audio_per_prompt), maxDocumentChunksPerQuery: readNumber(model.max_document_chunks_per_query), maxImagesPerPrompt: readNumber(model.max_images_per_prompt), maxPdfSizeMB: readNumber(model.max_pdf_size_mb), maxQueryTokens: readNumber(model.max_query_tokens), maxTokensPerDocumentChunk: readNumber(model.max_tokens_per_document_chunk), maxVideoLength: readNumber(model.max_video_length), maxVideosPerPrompt: readNumber(model.max_videos_per_prompt), outputVectorSize: readNumber(model.output_vector_size) }); entry.sourceRecords.push(compactObject({ source: "litellm", sourceUrl: sources.litellm.url, provider, model: modelId, mode, metadata: compactObject({ comment: readString(model.comment), deprecationDate: readString(model.deprecation_date), metadata: isRecord(model.metadata) ? model.metadata : undefined, providerSpecificEntry: model.provider_specific_entry, source: readString(model.source), supportedEndpoints: readStringArray(model.supported_endpoints), supportedRegions: readStringArray(model.supported_regions) }), limits, modalities, capabilities: capabilitiesFromLiteLlm(model, mode, modalities), pricing: pricingFromLiteLlm(model) })); } } function ingestOpenRouter(entries, payload) { if (!isRecord(payload) || !Array.isArray(payload.data)) return; for (const model of payload.data) { if (!isRecord(model)) continue; const modelId = readString(model.id) || readString(model.canonical_slug); if (!modelId) continue; const provider = "openrouter"; const entry = ensureEntry(entries, provider, modelId); const modalities = normalizeModalities({ input: model.architecture?.input_modalities, output: model.architecture?.output_modalities }); const supportedParameters = readStringArray(model.supported_parameters); const limits = compactObject({ contextTokens: readNumber(model.context_length) ?? readNumber(model.top_provider?.context_length), outputTokens: readNumber(model.top_provider?.max_completion_tokens) }); entry.sourceRecords.push(compactObject({ source: "openrouter", sourceUrl: sources.openrouter.url, provider, model: modelId, displayName: readString(model.name), metadata: compactObject({ canonicalSlug: readString(model.canonical_slug), createdAt: epochSecondsToIso(model.created), expirationDate: readString(model.expiration_date), huggingFaceId: readString(model.hugging_face_id), instructType: readString(model.architecture?.instruct_type), knowledgeCutoff: readString(model.knowledge_cutoff), links: isRecord(model.links) ? model.links : undefined, perRequestLimits: model.per_request_limits, reasoning: isRecord(model.reasoning) ? model.reasoning : undefined, supportedParameters, supportedVoices: model.supported_voices, tokenizer: readString(model.architecture?.tokenizer), topProvider: isRecord(model.top_provider) ? model.top_provider : undefined }), limits, modalities, capabilities: compactObject({ audioInput: modalities.input.includes("audio"), audioOutput: modalities.output.includes("audio"), imageInput: modalities.input.includes("image"), imageOutput: modalities.output.includes("image"), parallelFunctionCalling: supportedParameters.includes("parallel_tool_calls"), reasoning: supportedParameters.includes("reasoning") || supportedParameters.includes("include_reasoning") || isRecord(model.reasoning), responseSchema: supportedParameters.includes("response_format"), structuredOutput: supportedParameters.includes("structured_outputs"), temperature: supportedParameters.includes("temperature"), toolCalling: supportedParameters.includes("tools"), toolChoice: supportedParameters.includes("tool_choice"), webSearch: supportedParameters.includes("web_search_options"), videoInput: modalities.input.includes("video") }), pricing: pricingFromOpenRouter(model.pricing) })); } } function ensureEntry(entries, provider, model) { const entryId = composeEntryId(provider, model); const key = normalizeEntryKey(entryId); const existing = entries.get(key); if (existing) return existing; const entry = { id: entryId, provider, model, sourceRecords: [] }; entries.set(key, entry); return entry; } function finalizeEntry(entry) { const records = entry.sourceRecords.sort(compareSourceRecords); const displayName = firstDefined(records.map((record) => record.displayName)); const family = firstDefined(records.map((record) => record.family)); const mode = firstDefined(records.map((record) => record.mode)); const limits = mergeLimits(records.map((record) => record.limits)); const modalities = mergeModalities(records.map((record) => record.modalities)); const capabilities = mergeCapabilities(records.map((record) => record.capabilities), modalities, limits, mode); const pricingOffers = records .filter((record) => isRecord(record.pricing) && Object.keys(record.pricing).length > 0) .map((record) => compactObject({ source: record.source, provider: record.provider, model: record.model, sourceUrl: record.sourceUrl, ...record.pricing })); return compactObject({ id: entry.id, provider: entry.provider, model: entry.model, displayName, family, mode, sources: uniqueStrings(records.map((record) => record.source)), limits, modalities, capabilities, pricing: pricingOffers.length > 0 ? { currency: "USD", normalizedUnit: "per1MTokens values are USD per 1,000,000 tokens; non-token values keep the unit named by their object key.", offers: pricingOffers } : undefined, metadata: mergeMetadata(records), sourceRecords: records.map((record) => omit(record, ["pricing", "limits", "modalities", "capabilities"])) }); } function dedupeModels(providerModelRecords) { const groups = new Map(); for (const record of providerModelRecords) { const identity = canonicalIdentityForRecord(record); const existing = groups.get(identity.key); if (existing) { existing.records.push(record); } else { groups.set(identity.key, { identity, records: [record] }); } } return Array.from(groups.values()) .map(({ identity, records }) => mergeDedupedModel(identity, records)) .sort((a, b) => a.id.localeCompare(b.id)); } function mergeDedupedModel(identity, records) { const sortedRecords = records.slice().sort((a, b) => providerModelRecordScore(a, identity) - providerModelRecordScore(b, identity)); const representative = sortedRecords[0]; const sourceRecords = dedupeSourceRecords(sortedRecords.flatMap((record) => record.sourceRecords ?? [])) .sort(compareSourceRecords); const pricingOffers = dedupePricingOffers(sortedRecords.flatMap((record) => record.pricing?.offers ?? [])); const limits = mergeLimits(sortedRecords.map((record) => record.limits)); const modalities = mergeModalities(sortedRecords.map((record) => record.modalities)); const mode = firstDefined(sortedRecords.map((record) => record.mode)); const capabilities = mergeCapabilities(sortedRecords.map((record) => record.capabilities), modalities, limits, mode); const metadata = mergeMetadata(sourceRecords); return compactObject({ id: identity.id, provider: identity.provider, model: identity.model, displayName: firstDefined(sortedRecords.map((record) => record.displayName)) || representative.displayName, family: firstDefined(sortedRecords.map((record) => record.family)), mode, sources: uniqueStrings(sortedRecords.flatMap((record) => record.sources ?? [])), providers: uniqueStrings([ ...sortedRecords.map((record) => record.provider), ...sourceRecords.map((record) => record.provider), ...pricingOffers.map((offer) => offer.provider) ]), aliases: uniqueStrings([ ...sortedRecords.map((record) => record.id), ...sortedRecords.map((record) => composeEntryId(record.provider, record.model)), ...sourceRecords.map((record) => composeEntryId(record.provider, record.model)) ]), mergedProviderModelRecords: sortedRecords.length, limits, modalities, capabilities, pricing: pricingOffers.length > 0 ? { currency: "USD", normalizedUnit: "per1MTokens values are USD per 1,000,000 tokens; non-token values keep the unit named by their object key.", offers: pricingOffers } : undefined, metadata: compactObject({ ...metadata, providerModelRecordCount: sortedRecords.length }), sourceRecords }); } function canonicalIdentityForRecord(record) { const segments = modelPathSegments(record.model || record.id); const model = canonicalModelSlug(segments.at(-1) || record.model || record.id); const providerFromPath = firstDefined(segments.map(canonicalKnownProvider)); const providerFromName = inferProviderFromModelName(model, record.displayName, record.family); const providerFromRecord = canonicalProviderToken(record.provider); const provider = providerFromPath || providerFromName || providerFromRecord || "unknown"; const id = `${provider}/${model}`; return { id, key: normalizeEntryKey(id), model, provider }; } function providerModelRecordScore(record, identity) { let score = 0; if (record.provider !== identity.provider) score += 20; if (canonicalModelSlug(modelPathSegments(record.model).at(-1) || record.model) !== identity.model) score += 10; if (!record.pricing?.offers?.length) score += 5; if (!record.sources?.includes("models.dev")) score += 2; if (!record.sources?.includes("litellm")) score += 1; return score; } function dedupeSourceRecords(records) { const seen = new Set(); const output = []; for (const record of records) { const key = JSON.stringify([ record.source, record.provider, record.model, record.modelKey, record.displayName, record.family, record.mode, record.status ]); if (seen.has(key)) continue; seen.add(key); output.push(record); } return output; } function dedupePricingOffers(offers) { const seen = new Set(); const output = []; for (const offer of offers) { const key = JSON.stringify(offer); if (seen.has(key)) continue; seen.add(key); output.push(offer); } return output.sort((a, b) => { const sourceDiff = sourceOrder.indexOf(a.source) - sourceOrder.indexOf(b.source); if (sourceDiff !== 0) return sourceDiff; return `${a.provider}/${a.model}`.localeCompare(`${b.provider}/${b.model}`); }); } function modelPathSegments(value) { return String(value || "") .split("/") .map((segment) => segment.trim()) .filter(Boolean); } function canonicalKnownProvider(value) { const normalized = normalizeProviderToken(value); return firstPartyProviderAliases.get(normalized); } function canonicalProviderToken(value) { const normalized = normalizeProviderToken(value); return firstPartyProviderAliases.get(normalized) || normalized || undefined; } function normalizeProviderToken(value) { return String(value || "") .trim() .replace(/^hf:/i, "") .replace(/^@/, "") .replace(/_/g, "-") .replace(/\s+/g, "-") .toLowerCase(); } function canonicalModelSlug(value) { return String(value || "unknown") .trim() .replace(/^hf:/i, "") .replace(/_/g, "-") .replace(/\s+/g, "-") .replace(/-+/g, "-") .toLowerCase(); } function inferProviderFromModelName(...values) { const haystack = values .filter((value) => typeof value === "string" && value.trim()) .join(" ") .trim(); for (const [pattern, provider] of providerHints) { if (pattern.test(haystack)) return provider; } return undefined; } function compareSourceRecords(a, b) { const sourceDiff = sourceOrder.indexOf(a.source) - sourceOrder.indexOf(b.source); if (sourceDiff !== 0) return sourceDiff; return `${a.provider}/${a.model}`.localeCompare(`${b.provider}/${b.model}`); } function buildSummary(models, rawProviderModelCount) { const providerCount = new Set(models.map((model) => model.provider)).size; const availabilityProviderCount = new Set(models.flatMap((model) => model.providers ?? [model.provider])).size; return { modelCount: models.length, rawProviderModelCount, duplicateProviderModelRecordsMerged: rawProviderModelCount - models.length, providerCount, availabilityProviderCount, sourceCounts: Object.fromEntries( sourceOrder.map((source) => [ source, models.reduce((count, model) => count + (model.sources.includes(source) ? 1 : 0), 0) ]) ), pricingOfferCount: models.reduce((count, model) => count + (model.pricing?.offers?.length ?? 0), 0), modelsWithPricing: models.filter((model) => model.pricing?.offers?.length > 0).length, modelsWithoutPricing: models.filter((model) => !model.pricing?.offers?.length).length, modelsWith1MContext: models.filter((model) => model.limits?.supports1MContext).length, modelsWithImageInput: models.filter((model) => model.capabilities?.imageInput).length, modelsWithImageOutput: models.filter((model) => model.capabilities?.imageOutput || model.capabilities?.imageGeneration).length, modelsWithAudioInput: models.filter((model) => model.capabilities?.audioInput).length, modelsWithToolCalling: models.filter((model) => model.capabilities?.toolCalling || model.capabilities?.functionCalling).length, modelsWithReasoning: models.filter((model) => model.capabilities?.reasoning).length }; } function pricingFromModelsDev(cost) { if (!isRecord(cost)) return undefined; const per1MTokens = compactObject({ cacheRead: readNumber(cost.cache_read), cacheWrite: readNumber(cost.cache_write), input: readNumber(cost.input), inputAudio: readNumber(cost.input_audio), output: readNumber(cost.output), outputAudio: readNumber(cost.output_audio), reasoningOutput: readNumber(cost.reasoning) }); const known = new Set(["cache_read", "cache_write", "input", "input_audio", "output", "output_audio", "reasoning", "tiers", "context_over_200k"]); const extra = numericObjectExcept(cost, known); return compactObject({ sourceUnit: "usd_per_1m_tokens", per1MTokens, tiered: compactObject({ contextOver200K: normalizeNestedNumbers(cost.context_over_200k), tiers: normalizeNestedNumbers(cost.tiers) }), extra }); } function pricingFromLiteLlm(model) { const consumed = new Set(); const per1MTokens = compactObject({ cacheRead: per1MFromPerToken(firstNumber(model, consumed, [ "cache_read_input_token_cost", "input_cache_read_cost_per_token", "cache_read_cost_per_token" ])), cacheWrite: per1MFromPerToken(firstNumber(model, consumed, [ "cache_creation_input_token_cost", "input_cache_write_cost_per_token", "cache_write_cost_per_token" ])), input: per1MFromPerToken(firstNumber(model, consumed, [ "input_cost_per_token", "prompt_cost_per_token" ])), inputAudio: per1MFromPerToken(firstNumber(model, consumed, ["input_cost_per_audio_token"])), inputImage: per1MFromPerToken(firstNumber(model, consumed, ["input_cost_per_image_token"])), output: per1MFromPerToken(firstNumber(model, consumed, [ "output_cost_per_token", "completion_cost_per_token" ])), outputAudio: per1MFromPerToken(firstNumber(model, consumed, ["output_cost_per_audio_token"])), outputImage: per1MFromPerToken(firstNumber(model, consumed, ["output_cost_per_image_token"])), reasoningOutput: per1MFromPerToken(firstNumber(model, consumed, ["output_cost_per_reasoning_token"])) }); const perImage = numericGroup(model, consumed, { input: "input_cost_per_image", output: "output_cost_per_image", outputAbove512x512: "output_cost_per_image_above_512_and_512_pixels", outputAbove512x512Premium: "output_cost_per_image_above_512_and_512_pixels_and_premium_image", outputAbove1024x1024: "output_cost_per_image_above_1024_and_1024_pixels", outputAbove1024x1024Premium: "output_cost_per_image_above_1024_and_1024_pixels_and_premium_image", outputPremium: "output_cost_per_image_premium_image" }); const perPixel = numericGroup(model, consumed, { input: "input_cost_per_pixel", output: "output_cost_per_pixel" }); const perAudioSecond = numericGroup(model, consumed, { input: "input_cost_per_audio_per_second", inputAbove128KTokens: "input_cost_per_audio_per_second_above_128k_tokens", output: "output_cost_per_second" }); const perVideoSecond = numericGroup(model, consumed, { input: "input_cost_per_video_per_second", inputAbove128KTokens: "input_cost_per_video_per_second_above_128k_tokens", inputAbove8SInterval: "input_cost_per_video_per_second_above_8s_interval", inputAbove15SInterval: "input_cost_per_video_per_second_above_15s_interval", output: "output_cost_per_video_per_second", output1080p: "output_cost_per_second_1080p" }); const perCharacter = numericGroup(model, consumed, { input: "input_cost_per_character", inputAbove128KTokens: "input_cost_per_character_above_128k_tokens", output: "output_cost_per_character", outputAbove128KTokens: "output_cost_per_character_above_128k_tokens" }); const perRequest = numericGroup(model, consumed, { input: "input_cost_per_request" }); const perQuery = numericGroup(model, consumed, { input: "input_cost_per_query" }); const perPage = numericGroup(model, consumed, { annotation: "annotation_cost_per_page", ocr: "ocr_cost_per_page" }); const perCredit = numericGroup(model, consumed, { ocr: "ocr_cost_per_credit" }); const perSession = numericGroup(model, consumed, { codeInterpreter: "code_interpreter_cost_per_session" }); const perGBPerDay = numericGroup(model, consumed, { fileSearch: "file_search_cost_per_gb_per_day", vectorStore: "vector_store_cost_per_gb_per_day" }); const per1KCalls = numericGroup(model, consumed, { fileSearch: "file_search_cost_per_1k_calls" }); if (isRecord(model.search_context_cost_per_query)) consumed.add("search_context_cost_per_query"); return compactObject({ sourceUnit: "usd_per_token_for_token_fields", per1MTokens, perImage, perPixel, perAudioSecond, perVideoSecond, perCharacter, perRequest, perQuery, perPage, perCredit, perSession, perGBPerDay, per1KCalls, searchContextPerQuery: normalizeNestedNumbers(model.search_context_cost_per_query), extra: numericPricingObjectExcept(model, consumed) }); } function pricingFromOpenRouter(pricing) { if (!isRecord(pricing)) return undefined; const consumed = new Set(); const per1MTokens = compactObject({ cacheRead: per1MFromPerToken(firstNumber(pricing, consumed, ["input_cache_read"])), cacheWrite: per1MFromPerToken(firstNumber(pricing, consumed, ["input_cache_write"])), input: per1MFromPerToken(firstNumber(pricing, consumed, ["prompt"])), internalReasoning: per1MFromPerToken(firstNumber(pricing, consumed, ["internal_reasoning"])), output: per1MFromPerToken(firstNumber(pricing, consumed, ["completion"])) }); const other = numericGroup(pricing, consumed, { audio: "audio", image: "image", webSearch: "web_search" }); return compactObject({ sourceUnit: "usd_per_token_for_token_fields", per1MTokens, other, extra: numericObjectExcept(pricing, consumed) }); } function capabilitiesFromLiteLlm(model, mode, modalities) { return compactObject({ adaptiveThinking: readBoolean(model.supports_adaptive_thinking), assistantPrefill: readBoolean(model.supports_assistant_prefill), audioInput: readBoolean(model.supports_audio_input) || modalities.input.includes("audio"), audioOutput: readBoolean(model.supports_audio_output) || modalities.output.includes("audio"), codeExecution: readBoolean(model.supports_code_execution), computerUse: readBoolean(model.supports_computer_use), embedding: mode === "embedding", embeddingImageInput: readBoolean(model.supports_embedding_image_input), fileSearch: readBoolean(model.supports_file_search), functionCalling: readBoolean(model.supports_function_calling), imageEditing: readBoolean(model.supports_nova_canvas_image_edit), imageGeneration: mode === "image_generation", imageInput: readBoolean(model.supports_vision) || readBoolean(model.supports_image_input) || readBoolean(model.supports_embedding_image_input) || modalities.input.includes("image"), imageOutput: mode === "image_generation" || modalities.output.includes("image"), lowReasoningEffort: readBoolean(model.supports_low_reasoning_effort), maxReasoningEffort: readBoolean(model.supports_max_reasoning_effort), minimalReasoningEffort: readBoolean(model.supports_minimal_reasoning_effort), moderation: mode === "moderation", multimodal: readBoolean(model.supports_multimodal), nativeStreaming: readBoolean(model.supports_native_streaming), nativeStructuredOutput: readBoolean(model.supports_native_structured_output), noneReasoningEffort: readBoolean(model.supports_none_reasoning_effort), parallelFunctionCalling: readBoolean(model.supports_parallel_function_calling), pdfInput: readBoolean(model.supports_pdf_input) || modalities.input.includes("pdf"), promptCaching: readBoolean(model.supports_prompt_caching), reasoning: readBoolean(model.supports_reasoning), rerank: mode === "rerank", responseSchema: readBoolean(model.supports_response_schema), samplingParams: readBoolean(model.supports_sampling_params), serviceTier: readBoolean(model.supports_service_tier), speech: mode === "audio_speech", systemMessages: readBoolean(model.supports_system_messages), toolChoice: readBoolean(model.supports_tool_choice), transcription: mode === "audio_transcription", urlContext: readBoolean(model.supports_url_context), videoInput: readBoolean(model.supports_video_input) || modalities.input.includes("video"), vision: readBoolean(model.supports_vision), webSearch: readBoolean(model.supports_web_search), xhighReasoningEffort: readBoolean(model.supports_xhigh_reasoning_effort) }); } function inferLiteLlmModalities(model, mode) { const input = new Set(); const output = new Set(); if (mode === "audio_transcription") { input.add("audio"); output.add("text"); } else if (mode === "audio_speech") { input.add("text"); output.add("audio"); } else if (mode === "image_generation") { input.add("text"); output.add("image"); } else if (mode === "embedding") { input.add("text"); output.add("embedding"); } else if (mode === "rerank") { input.add("text"); output.add("score"); } else { input.add("text"); output.add("text"); } if (readBoolean(model.supports_vision) || readBoolean(model.supports_image_input) || readBoolean(model.supports_embedding_image_input) || readNumber(model.input_cost_per_image) !== undefined || readNumber(model.input_cost_per_image_token) !== undefined) { input.add("image"); } if (readBoolean(model.supports_audio_input) || readNumber(model.input_cost_per_audio_token) !== undefined) { input.add("audio"); } if (readBoolean(model.supports_audio_output) || readNumber(model.output_cost_per_audio_token) !== undefined) { output.add("audio"); } if (readBoolean(model.supports_video_input) || readNumber(model.input_cost_per_video_per_second) !== undefined) { input.add("video"); } if (readBoolean(model.supports_pdf_input)) { input.add("pdf"); } return { input: Array.from(input).sort(), output: Array.from(output).sort() }; } function mergeLimits(limitsList) { const output = {}; const numericKeys = [ "contextTokens", "inputTokens", "maxAudioLengthHours", "maxAudioPerPrompt", "maxDocumentChunksPerQuery", "maxImagesPerPrompt", "maxPdfSizeMB", "maxQueryTokens", "maxTokens", "maxTokensPerDocumentChunk", "maxVideoLength", "maxVideosPerPrompt", "outputTokens", "outputVectorSize" ]; for (const key of numericKeys) { const values = limitsList.map((limits) => readNumber(limits?.[key])).filter((value) => value !== undefined); if (values.length > 0) output[key] = Math.max(...values); } const contextCandidates = [output.contextTokens, output.inputTokens, output.maxTokens] .filter((value) => Number.isFinite(value)); output.supports1MContext = contextCandidates.some((value) => value >= support1MContextThreshold); return compactObject(output); } function mergeModalities(modalityList) { const input = new Set(); const output = new Set(); for (const modalities of modalityList) { for (const value of modalities?.input ?? []) input.add(value); for (const value of modalities?.output ?? []) output.add(value); } return { input: Array.from(input).sort(), output: Array.from(output).sort() }; } function mergeCapabilities(capabilitiesList, modalities, limits, mode) { const merged = {}; for (const capabilities of capabilitiesList) { if (!isRecord(capabilities)) continue; for (const [key, value] of Object.entries(capabilities)) { if (value === true) merged[key] = true; else if (value === false && merged[key] !== true) merged[key] = false; } } merged.audioInput = merged.audioInput || modalities.input.includes("audio"); merged.audioOutput = merged.audioOutput || modalities.output.includes("audio"); merged.imageInput = merged.imageInput || modalities.input.includes("image"); merged.imageOutput = merged.imageOutput || modalities.output.includes("image"); merged.pdfInput = merged.pdfInput || modalities.input.includes("pdf"); merged.videoInput = merged.videoInput || modalities.input.includes("video"); merged.supports1MContext = Boolean(limits.supports1MContext); if (mode === "image_generation") merged.imageGeneration = true; return compactObject(merged); } function mergeMetadata(records) { const output = compactObject({ displayNames: uniqueStrings(records.map((record) => record.displayName)), families: uniqueStrings(records.map((record) => record.family)), modes: uniqueStrings(records.map((record) => record.mode)), statuses: uniqueStrings(records.map((record) => record.status)), knowledgeCutoff: firstDefined(records.map((record) => record.metadata?.knowledgeCutoff)), releaseDate: firstDefined(records.map((record) => record.metadata?.releaseDate)), lastUpdated: firstDefined(records.map((record) => record.metadata?.lastUpdated)), deprecationDate: firstDefined(records.map((record) => record.metadata?.deprecationDate)), supportedParameters: uniqueStrings(records.flatMap((record) => record.metadata?.supportedParameters ?? [])), supportedEndpoints: uniqueStrings(records.flatMap((record) => record.metadata?.supportedEndpoints ?? [])), supportedRegions: uniqueStrings(records.flatMap((record) => record.metadata?.supportedRegions ?? [])) }); return output; } function normalizeModalities(value) { if (!isRecord(value)) return { input: [], output: [] }; return { input: readStringArray(value.input).sort(), output: readStringArray(value.output).sort() }; } function composeEntryId(provider, model) { const normalizedProvider = String(provider || "unknown").trim() || "unknown"; const normalizedModel = String(model || "unknown").trim() || "unknown"; const lowerProviderPrefix = `${normalizedProvider.toLowerCase()}/`; if (normalizedModel.toLowerCase().startsWith(lowerProviderPrefix)) { return normalizedModel; } return `${normalizedProvider}/${normalizedModel}`; } function normalizeEntryKey(value) { return String(value).trim().replace(/^\/+|\/+$/g, "").replace(/\/+/g, "/").toLowerCase(); } function firstNumber(record, consumed, keys) { for (const key of keys) { const value = readNumber(record?.[key]); if (value !== undefined) { consumed.add(key); return value; } } return undefined; } function numericGroup(record, consumed, mapping) { const output = {}; for (const [targetKey, sourceKey] of Object.entries(mapping)) { const value = readNumber(record?.[sourceKey]); if (value !== undefined) { output[targetKey] = value; consumed.add(sourceKey); } } return compactObject(output); } function numericObjectExcept(record, excludedKeys) { if (!isRecord(record)) return undefined; const output = {}; for (const [key, value] of Object.entries(record)) { if (excludedKeys.has(key)) continue; const normalized = normalizeNestedNumbers(value); if (normalized !== undefined) output[key] = normalized; } return compactObject(output); } function numericPricingObjectExcept(record, excludedKeys) { if (!isRecord(record)) return undefined; const output = {}; for (const [key, value] of Object.entries(record)) { if (excludedKeys.has(key) || !looksLikePricingKey(key)) continue; const normalized = normalizeNestedNumbers(value); if (normalized !== undefined) output[key] = normalized; } return compactObject(output); } function looksLikePricingKey(key) { return key.includes("cost") || key.includes("price") || key.includes("_per_") || key.includes("dbu") || key.includes("uplift_multiplier"); } function normalizeNestedNumbers(value) { const number = readNumber(value); if (number !== undefined) return number; if (Array.isArray(value)) { const items = value .map((item) => normalizeNestedNumbers(item)) .filter((item) => item !== undefined); return items.length > 0 ? items : undefined; } if (isRecord(value)) { const output = {}; for (const [key, nested] of Object.entries(value)) { const normalized = normalizeNestedNumbers(nested); if (normalized !== undefined) output[key] = normalized; } return Object.keys(output).length > 0 ? output : undefined; } return undefined; } function per1MFromPerToken(value) { return value === undefined ? undefined : roundNumber(value * 1_000_000); } function readNumber(value) { const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : Number.NaN; return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined; } function readString(value) { return typeof value === "string" && value.trim() ? value.trim() : undefined; } function readBoolean(value) { return typeof value === "boolean" ? value : undefined; } function readStringArray(value) { return Array.isArray(value) ? value.filter((item) => typeof item === "string" && item.trim()).map((item) => item.trim()) : []; } function firstDefined(values) { return values.find((value) => value !== undefined && value !== null && value !== ""); } function uniqueStrings(values) { return Array.from(new Set(values.filter((value) => typeof value === "string" && value.trim()).map((value) => value.trim()))).sort(); } function compactObject(object) { if (!isRecord(object)) return undefined; const output = {}; for (const [key, value] of Object.entries(object)) { if (value === undefined || value === null) continue; if (Array.isArray(value) && value.length === 0) continue; if (isRecord(value) && Object.keys(value).length === 0) continue; output[key] = value; } return Object.keys(output).length > 0 ? output : undefined; } function omit(object, keys) { const keySet = new Set(keys); const output = {}; for (const [key, value] of Object.entries(object)) { if (!keySet.has(key)) output[key] = value; } return output; } function roundNumber(value) { return Number(value.toPrecision(12)); } function epochSecondsToIso(value) { const seconds = readNumber(value); if (seconds === undefined) return undefined; return new Date(seconds * 1000).toISOString(); } function isRecord(value) { return typeof value === "object" && value !== null && !Array.isArray(value); } main().catch((error) => { console.error(error); process.exitCode = 1; });