mirror of
https://github.com/Nezumi-2711/9router-config-generate.git
synced 2026-09-22 05:31:47 +00:00
fix: update logic for generate config
This commit is contained in:
+3
-1
@@ -11,7 +11,9 @@
|
||||
"preview": "vite preview",
|
||||
"verify": "bun run build && bun run test && bun run lint",
|
||||
"deploy": "bun run verify && wrangler deploy",
|
||||
"preview:cf": "vite build && wrangler dev"
|
||||
"preview:cf": "vite build && wrangler dev",
|
||||
"sync:models": "bun run scripts/sync-models.ts",
|
||||
"sync:models:check": "bun run scripts/sync-models.ts --check"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.8",
|
||||
|
||||
@@ -0,0 +1,203 @@
|
||||
#!/usr/bin/env bun
|
||||
/**
|
||||
* Sync script for models.dev catalog snapshot.
|
||||
* Fetches models.json and api.json from https://models.dev, prunes, joins reasoning efforts,
|
||||
* and generates src/data/modelCatalog.generated.ts.
|
||||
*/
|
||||
import { resolve } from 'node:path'
|
||||
|
||||
const MODELS_DEV_URL = 'https://models.dev/models.json'
|
||||
const API_DEV_URL = 'https://models.dev/api.json'
|
||||
|
||||
const PROVIDER_ALIASES: Record<string, string[]> = {
|
||||
google: ['google', 'google-vertex'],
|
||||
anthropic: ['anthropic', 'google-vertex-anthropic', 'amazon-bedrock'],
|
||||
zhipuai: ['zhipuai', 'zai'],
|
||||
openai: ['openai', 'azure'],
|
||||
}
|
||||
|
||||
// Allowlist for reasoning effort options, filtering out 'none', 'default', null, etc.
|
||||
// Per plan recommendation: PR 1 ships with ['minimal', 'low', 'medium', 'high', 'xhigh', 'max']
|
||||
const EFFORT_ALLOWLIST = ['minimal', 'low', 'medium', 'high', 'xhigh', 'max'] as const
|
||||
|
||||
interface RawModel {
|
||||
name?: string
|
||||
limit?: {
|
||||
context?: number
|
||||
output?: number
|
||||
input?: number
|
||||
}
|
||||
}
|
||||
|
||||
interface RawReasoningOption {
|
||||
type: string
|
||||
values?: (string | null)[]
|
||||
}
|
||||
|
||||
interface RawProviderModel {
|
||||
reasoning_options?: RawReasoningOption[]
|
||||
}
|
||||
|
||||
interface RawProvider {
|
||||
models?: Record<string, RawProviderModel>
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const isCheckMode = process.argv.includes('--check')
|
||||
|
||||
console.log('Fetching models.dev catalog...')
|
||||
const [modelsRes, apiRes] = await Promise.all([
|
||||
fetch(MODELS_DEV_URL),
|
||||
fetch(API_DEV_URL),
|
||||
])
|
||||
|
||||
if (!modelsRes.ok) {
|
||||
throw new Error(`Failed to fetch ${MODELS_DEV_URL}: HTTP ${modelsRes.status}`)
|
||||
}
|
||||
if (!apiRes.ok) {
|
||||
throw new Error(`Failed to fetch ${API_DEV_URL}: HTTP ${apiRes.status}`)
|
||||
}
|
||||
|
||||
const rawModels = (await modelsRes.json()) as Record<string, RawModel>
|
||||
const rawApi = (await apiRes.json()) as Record<string, RawProvider>
|
||||
|
||||
const rawKeys = Object.keys(rawModels)
|
||||
if (rawKeys.length < 100) {
|
||||
throw new Error(`Sanity check failed: models.dev returned only ${rawKeys.length} models (< 100). Aborting.`)
|
||||
}
|
||||
|
||||
// Build pruned catalog
|
||||
// Keys sorted deterministically
|
||||
const sortedKeys = rawKeys.sort()
|
||||
|
||||
let usableCount = 0
|
||||
let effortsCount = 0
|
||||
let warningCount = 0
|
||||
|
||||
interface GeneratedEntry {
|
||||
n: string
|
||||
c: number
|
||||
o?: number
|
||||
e?: string[]
|
||||
}
|
||||
|
||||
const prunedCatalog: Record<string, GeneratedEntry> = {}
|
||||
|
||||
for (const fullKey of sortedKeys) {
|
||||
const raw = rawModels[fullKey]
|
||||
const context = raw?.limit?.context
|
||||
|
||||
// 1. Prune entry with context <= 0 or missing (image/realtime)
|
||||
if (!context || context <= 0) {
|
||||
continue
|
||||
}
|
||||
|
||||
usableCount++
|
||||
|
||||
const entry: GeneratedEntry = {
|
||||
n: raw.name || fullKey,
|
||||
c: context,
|
||||
}
|
||||
|
||||
// 2. Emit 'o' only when limit.output > 0
|
||||
if (raw.limit?.output && raw.limit.output > 0) {
|
||||
entry.o = raw.limit.output
|
||||
}
|
||||
|
||||
// Mitigation check from plan: check if raw.limit.input < context - (entry.o ?? 0)
|
||||
if (raw.limit?.input && entry.o && raw.limit.input < (context - entry.o)) {
|
||||
console.warn(`[warning] ${fullKey}: limit.input (${raw.limit.input}) < context - out (${context - entry.o})`)
|
||||
warningCount++
|
||||
}
|
||||
|
||||
// 3. Extract and sanitize reasoning efforts from api.json
|
||||
const [lab, id] = fullKey.split('/', 2)
|
||||
const providersToCheck = PROVIDER_ALIASES[lab] ?? [lab]
|
||||
let foundEfforts: string[] | null = null
|
||||
|
||||
for (const p of providersToCheck) {
|
||||
const providerData = rawApi[p]
|
||||
if (!providerData?.models) continue
|
||||
const candidate = providerData.models[fullKey] || providerData.models[id]
|
||||
if (candidate?.reasoning_options) {
|
||||
const effortOpt = candidate.reasoning_options.find((opt) => opt.type === 'effort')
|
||||
if (effortOpt?.values && Array.isArray(effortOpt.values)) {
|
||||
// Filter against allowlist preserving order
|
||||
const sanitized = effortOpt.values
|
||||
.filter((v): v is string => typeof v === 'string' && (EFFORT_ALLOWLIST as readonly string[]).includes(v))
|
||||
if (sanitized.length > 0) {
|
||||
foundEfforts = sanitized
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundEfforts && foundEfforts.length > 0) {
|
||||
entry.e = foundEfforts
|
||||
effortsCount++
|
||||
}
|
||||
|
||||
prunedCatalog[fullKey] = entry
|
||||
}
|
||||
|
||||
// Format generated code deterministically
|
||||
const today = new Date().toISOString().slice(0, 10)
|
||||
const lines: string[] = []
|
||||
lines.push('// @generated by scripts/sync-models.ts — DO NOT EDIT')
|
||||
lines.push('// Source: https://models.dev/models.json + api.json (MIT)')
|
||||
lines.push("import type { CatalogEntry } from './modelCatalog.ts'")
|
||||
lines.push('')
|
||||
lines.push(`export const CATALOG_GENERATED_AT = '${today}'`)
|
||||
lines.push(`export const CATALOG_SOURCE_COUNT = ${usableCount}`)
|
||||
lines.push('')
|
||||
lines.push('export const MODEL_CATALOG: Record<string, CatalogEntry> = {')
|
||||
|
||||
for (const [key, entry] of Object.entries(prunedCatalog)) {
|
||||
// deterministic field order: n, c, o, e
|
||||
const parts: string[] = []
|
||||
parts.push(`"n":${JSON.stringify(entry.n)}`)
|
||||
parts.push(`"c":${entry.c}`)
|
||||
if (entry.o !== undefined) {
|
||||
parts.push(`"o":${entry.o}`)
|
||||
}
|
||||
if (entry.e && entry.e.length > 0) {
|
||||
parts.push(`"e":${JSON.stringify(entry.e)}`)
|
||||
}
|
||||
lines.push(` ${JSON.stringify(key)}: {${parts.join(',')}},`)
|
||||
}
|
||||
|
||||
lines.push('}')
|
||||
lines.push('')
|
||||
|
||||
const generatedContent = lines.join('\n')
|
||||
const targetPath = resolve(import.meta.dir, '../src/data/modelCatalog.generated.ts')
|
||||
|
||||
if (isCheckMode) {
|
||||
const existingContent = await Bun.file(targetPath).text().catch(() => '')
|
||||
// Compare ignoring lines with CATALOG_GENERATED_AT or Generated
|
||||
const normalize = (s: string) =>
|
||||
s
|
||||
.split('\n')
|
||||
.filter((l) => !l.includes('CATALOG_GENERATED_AT') && !l.includes('@generated'))
|
||||
.join('\n')
|
||||
|
||||
if (normalize(existingContent) !== normalize(generatedContent)) {
|
||||
console.error('Check failed: src/data/modelCatalog.generated.ts is out of sync with models.dev.')
|
||||
process.exit(1)
|
||||
}
|
||||
console.log('Check passed: src/data/modelCatalog.generated.ts is up to date.')
|
||||
return
|
||||
}
|
||||
|
||||
await Bun.write(targetPath, generatedContent)
|
||||
const byteSize = new TextEncoder().encode(generatedContent).length
|
||||
console.log(
|
||||
`Synced ${usableCount} models, ${effortsCount} with efforts, ${warningCount} warnings, ${byteSize} bytes written to ${targetPath}`
|
||||
)
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
console.error(err)
|
||||
process.exit(1)
|
||||
})
|
||||
@@ -4,6 +4,8 @@ import { Card } from './ui/Card'
|
||||
import { Badge } from './ui/Badge'
|
||||
import { Input } from './ui/Input'
|
||||
import { Search, CheckSquare, Square, Eye, Wrench, Layers, RefreshCw } from 'lucide-react'
|
||||
import { resolveModelLimits } from '../data/modelCatalog.ts'
|
||||
import { CATALOG_GENERATED_AT } from '../data/modelCatalog.generated.ts'
|
||||
|
||||
interface ModelListProps {
|
||||
models: Model[]
|
||||
@@ -93,6 +95,19 @@ export const ModelList: React.FC<ModelListProps> = ({
|
||||
) : (
|
||||
filteredModels.map((model) => {
|
||||
const isSelected = selectedIds.includes(model.id)
|
||||
const limits = resolveModelLimits(model)
|
||||
const isCatalog = limits.source === 'models.dev'
|
||||
const tooltipContext = isCatalog
|
||||
? `Context window: ${limits.contextWindow.toLocaleString()} tokens (models.dev: ${limits.catalogName ?? limits.catalogId}, snapshot ${CATALOG_GENERATED_AT})`
|
||||
: limits.source === 'gateway'
|
||||
? `Context window: ${limits.contextWindow.toLocaleString()} tokens (gateway reported)`
|
||||
: `Context window: ${limits.contextWindow.toLocaleString()} tokens (default fallback)`
|
||||
const tooltipOutput = isCatalog
|
||||
? `Max output: ${limits.maxOutputTokens.toLocaleString()} tokens (models.dev: ${limits.catalogName ?? limits.catalogId})`
|
||||
: limits.source === 'gateway'
|
||||
? `Max output: ${limits.maxOutputTokens.toLocaleString()} tokens (gateway reported)`
|
||||
: `Max output: ${limits.maxOutputTokens.toLocaleString()} tokens (default fallback)`
|
||||
|
||||
return (
|
||||
<div
|
||||
key={model.id}
|
||||
@@ -139,12 +154,13 @@ export const ModelList: React.FC<ModelListProps> = ({
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col items-end gap-1 shrink-0 text-[10px] text-zinc-400">
|
||||
{model.contextWindow && (
|
||||
<span className="flex items-center gap-0.5 font-mono" title="Context window">
|
||||
<Layers className="w-3 h-3 text-zinc-400" />
|
||||
{(model.contextWindow / 1000).toFixed(0)}k
|
||||
</span>
|
||||
)}
|
||||
<span className="flex items-center gap-0.5 font-mono" title={tooltipContext}>
|
||||
<Layers className={`w-3 h-3 ${isCatalog ? 'text-amber-500' : 'text-zinc-400'}`} />
|
||||
{(limits.contextWindow / 1000).toFixed(0)}k
|
||||
</span>
|
||||
<span className="font-mono text-[9px] text-zinc-400/80" title={tooltipOutput}>
|
||||
↑{(limits.maxOutputTokens / 1000).toFixed(0)}k
|
||||
</span>
|
||||
<div className="flex items-center gap-1">
|
||||
{model.vision && (
|
||||
<span title="Supports Vision" className="flex items-center">
|
||||
|
||||
@@ -25,8 +25,8 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={`relative group rounded-lg overflow-hidden border border-zinc-200 dark:border-[#262732] bg-[#f8f8fa] dark:bg-[#0c0d12] ${className}`}>
|
||||
<div className="flex items-center justify-between px-3.5 py-2 border-b border-zinc-200 dark:border-[#22232c] bg-zinc-100/70 dark:bg-[#12131a]/80 text-xs text-zinc-500 font-mono">
|
||||
<div className={`relative group rounded-lg overflow-hidden border border-zinc-200 dark:border-[#262732] bg-[#f8f8fa] dark:bg-[#0c0d12] flex flex-col ${className}`}>
|
||||
<div className="flex items-center justify-between px-3.5 py-2 shrink-0 border-b border-zinc-200 dark:border-[#22232c] bg-zinc-100/70 dark:bg-[#12131a]/80 text-xs text-zinc-500 font-mono">
|
||||
<span>{language ? language.toUpperCase() : 'CODE'}</span>
|
||||
<button
|
||||
onClick={handleCopy}
|
||||
@@ -46,7 +46,7 @@ export const CodeBlock: React.FC<CodeBlockProps> = ({
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
<div className="overflow-x-auto p-3.5 font-mono text-[13px] leading-relaxed text-zinc-800 dark:text-zinc-200 selection:bg-amber-500/20">
|
||||
<div className="flex-1 min-h-0 overflow-auto p-3.5 font-mono text-[13px] leading-relaxed text-zinc-800 dark:text-zinc-200 selection:bg-amber-500/20">
|
||||
<pre className="m-0 tab-4 whitespace-pre">
|
||||
<code>{code}</code>
|
||||
</pre>
|
||||
|
||||
@@ -6,7 +6,6 @@ export const mockModels: Model[] = [
|
||||
name: 'Claude Opus 4.7 (Anthropic via 9router)',
|
||||
family: 'claude-opus-4-7',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Most powerful reasoning model for complex architecture and codebases',
|
||||
@@ -16,7 +15,6 @@ export const mockModels: Model[] = [
|
||||
name: 'Claude Sonnet 4.5',
|
||||
family: 'claude-sonnet-4-5',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'High-speed intelligent coding powerhouse with strong agentic skills',
|
||||
@@ -26,7 +24,6 @@ export const mockModels: Model[] = [
|
||||
name: 'Claude Sonnet 4.5 (KR Route)',
|
||||
family: 'claude-sonnet-4-5',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Alternative regional routing for low latency',
|
||||
@@ -36,7 +33,6 @@ export const mockModels: Model[] = [
|
||||
name: 'GPT-4o',
|
||||
family: 'gpt-4o',
|
||||
provider: 'OpenAI',
|
||||
contextWindow: 128000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Flagship multimodal omni model with rapid reasoning and code output',
|
||||
@@ -46,7 +42,6 @@ export const mockModels: Model[] = [
|
||||
name: 'o3-mini (High)',
|
||||
family: 'o3-mini',
|
||||
provider: 'OpenAI',
|
||||
contextWindow: 200000,
|
||||
vision: false,
|
||||
toolCalling: true,
|
||||
reasoning: true,
|
||||
@@ -57,7 +52,6 @@ export const mockModels: Model[] = [
|
||||
name: 'Gemini 3 Flash Preview',
|
||||
family: 'gemini-3-flash',
|
||||
provider: 'Google Vertex',
|
||||
contextWindow: 1000000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
description: 'Ultra high-speed large 1M token context model for massive file ingestion',
|
||||
@@ -67,7 +61,6 @@ export const mockModels: Model[] = [
|
||||
name: 'DeepSeek R1',
|
||||
family: 'deepseek-r1',
|
||||
provider: 'DeepSeek',
|
||||
contextWindow: 64000,
|
||||
vision: false,
|
||||
toolCalling: true,
|
||||
reasoning: true,
|
||||
|
||||
@@ -0,0 +1,371 @@
|
||||
// @generated by scripts/sync-models.ts — DO NOT EDIT
|
||||
// Source: https://models.dev/models.json + api.json (MIT)
|
||||
import type { CatalogEntry } from './modelCatalog.ts'
|
||||
|
||||
export const CATALOG_GENERATED_AT = '2026-09-04'
|
||||
export const CATALOG_SOURCE_COUNT = 362
|
||||
|
||||
export const MODEL_CATALOG: Record<string, CatalogEntry> = {
|
||||
"aisingapore/gemma-sea-lion-v4-27b-it": {"n":"Gemma-SEA-LION-v4-27B-IT","c":128000,"o":128000},
|
||||
"alibaba/qwen-flash": {"n":"Qwen Flash","c":1000000,"o":32768},
|
||||
"alibaba/qwen-max": {"n":"Qwen Max","c":32768,"o":8192},
|
||||
"alibaba/qwen-omni-turbo": {"n":"Qwen-Omni Turbo","c":32768,"o":2048},
|
||||
"alibaba/qwen-plus": {"n":"Qwen Plus","c":1000000,"o":32768},
|
||||
"alibaba/qwen-turbo": {"n":"Qwen Turbo","c":1000000,"o":16384},
|
||||
"alibaba/qwen-vl-max": {"n":"Qwen-VL Max","c":131072,"o":8192},
|
||||
"alibaba/qwen-vl-plus": {"n":"Qwen-VL Plus","c":131072,"o":8192},
|
||||
"alibaba/qwen2-5-vl-72b-instruct": {"n":"Qwen2.5-VL 72B Instruct","c":131072,"o":8192},
|
||||
"alibaba/qwen2.5-coder-0.5b": {"n":"Qwen2.5-Coder-0.5B","c":32768,"o":8192},
|
||||
"alibaba/qwen2.5-coder-32b-instruct": {"n":"Qwen2.5-Coder-32B-Instruct","c":131072,"o":8192},
|
||||
"alibaba/qwen3-235b-a22b": {"n":"Qwen3 235B-A22B","c":131072,"o":16384},
|
||||
"alibaba/qwen3-235b-a22b-instruct-2507": {"n":"Qwen3 235B-A22B Instruct 2507","c":262144,"o":16384},
|
||||
"alibaba/qwen3-30b-a3b": {"n":"Qwen3 30B A3B","c":131072,"o":16384},
|
||||
"alibaba/qwen3-32b": {"n":"Qwen3 32B","c":131072,"o":16384},
|
||||
"alibaba/qwen3-coder-30b-a3b-instruct": {"n":"Qwen3-Coder 30B-A3B Instruct","c":262144,"o":65536},
|
||||
"alibaba/qwen3-coder-480b-a35b-instruct": {"n":"Qwen3-Coder 480B-A35B Instruct","c":262144,"o":65536},
|
||||
"alibaba/qwen3-coder-flash": {"n":"Qwen3 Coder Flash","c":1000000,"o":65536},
|
||||
"alibaba/qwen3-coder-next": {"n":"Qwen3 Coder Next","c":262144,"o":65536},
|
||||
"alibaba/qwen3-coder-plus": {"n":"Qwen3 Coder Plus","c":1048576,"o":65536},
|
||||
"alibaba/qwen3-max": {"n":"Qwen3 Max","c":262144,"o":65536},
|
||||
"alibaba/qwen3-next-80b-a3b-instruct": {"n":"Qwen3-Next 80B-A3B Instruct","c":131072,"o":32768},
|
||||
"alibaba/qwen3-next-80b-a3b-thinking": {"n":"Qwen3-Next 80B-A3B (Thinking)","c":131072,"o":32768},
|
||||
"alibaba/qwen3-vl-235b-a22b-instruct": {"n":"Qwen3 VL 235B A22B Instruct","c":131072,"o":32768},
|
||||
"alibaba/qwen3-vl-235b-a22b-thinking": {"n":"Qwen3 VL 235B A22B Thinking","c":131072,"o":32768},
|
||||
"alibaba/qwen3-vl-plus": {"n":"Qwen3-VL Plus","c":262144,"o":32768},
|
||||
"alibaba/qwen3.5-122b-a10b": {"n":"Qwen3.5 122B-A10B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.5-27b": {"n":"Qwen3.5 27B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.5-35b-a3b": {"n":"Qwen3.5 35B-A3B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.5-397b-a17b": {"n":"Qwen3.5 397B-A17B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.5-9b": {"n":"Qwen3.5 9B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.5-flash": {"n":"Qwen3.5 Flash","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.5-plus": {"n":"Qwen3.5 Plus","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.6-27b": {"n":"Qwen3.6 27B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.6-35b-a3b": {"n":"Qwen3.6 35B-A3B","c":262144,"o":65536},
|
||||
"alibaba/qwen3.6-flash": {"n":"Qwen3.6 Flash","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.6-max-preview": {"n":"Qwen3.6 Max Preview","c":262144,"o":65536},
|
||||
"alibaba/qwen3.6-plus": {"n":"Qwen3.6 Plus","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.7-flash": {"n":"Qwen3.7 Flash","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.7-max": {"n":"Qwen3.7 Max","c":1000000,"o":65536},
|
||||
"alibaba/qwen3.7-plus": {"n":"Qwen3.7 Plus","c":1000000,"o":64000},
|
||||
"alibaba/qwen3.8-2.4t-a95b": {"n":"Qwen3.8 2.4T A95B","c":262144,"o":131072},
|
||||
"alibaba/qwen3.8-27b": {"n":"Qwen3.8 27B","c":262144,"o":32768},
|
||||
"alibaba/qwen3.8-flash": {"n":"Qwen3.8 Flash","c":1000000,"o":131072,"e":["low","medium","xhigh"]},
|
||||
"alibaba/qwen3.8-flash-next": {"n":"Qwen3.8 Flash Next","c":262144,"o":131072},
|
||||
"alibaba/qwen3.8-max": {"n":"Qwen3.8 Max","c":1000000,"o":131072,"e":["low","medium","xhigh"]},
|
||||
"alibaba/qwen3.8-max-0902": {"n":"Qwen3.8 Max 0902","c":1000000,"o":131072},
|
||||
"alibaba/qwen3.8-max-preview": {"n":"Qwen3.8 Max Preview","c":1000000,"o":131072},
|
||||
"alibaba/qwq-32b": {"n":"QwQ 32B","c":131072,"o":8192},
|
||||
"alibaba/qwq-plus": {"n":"QwQ Plus","c":131072,"o":8192},
|
||||
"anthropic/claude-3-5-haiku-20241022": {"n":"Claude Haiku 3.5","c":200000,"o":8192},
|
||||
"anthropic/claude-3-5-sonnet-20241022": {"n":"Claude Sonnet 3.5 v2","c":200000,"o":8192},
|
||||
"anthropic/claude-3-7-sonnet-20250219": {"n":"Claude Sonnet 3.7","c":200000,"o":64000},
|
||||
"anthropic/claude-3-haiku-20240307": {"n":"Claude Haiku 3","c":200000,"o":4096},
|
||||
"anthropic/claude-fable-5": {"n":"Claude Fable 5","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"anthropic/claude-fable-5-1": {"n":"Claude Fable 5.1","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"anthropic/claude-haiku-4-5": {"n":"Claude Haiku 4.5 (latest)","c":200000,"o":64000},
|
||||
"anthropic/claude-haiku-4-5-20251001": {"n":"Claude Haiku 4.5","c":200000,"o":64000},
|
||||
"anthropic/claude-mythos-5": {"n":"Claude Mythos 5","c":1000000,"o":128000},
|
||||
"anthropic/claude-opus-4-0": {"n":"Claude Opus 4 (latest)","c":200000,"o":32000},
|
||||
"anthropic/claude-opus-4-1": {"n":"Claude Opus 4.1 (latest)","c":200000,"o":32000},
|
||||
"anthropic/claude-opus-4-1-20250805": {"n":"Claude Opus 4.1","c":200000,"o":32000},
|
||||
"anthropic/claude-opus-4-20250514": {"n":"Claude Opus 4","c":200000,"o":32000},
|
||||
"anthropic/claude-opus-4-5": {"n":"Claude Opus 4.5 (latest)","c":200000,"o":64000,"e":["low","medium","high"]},
|
||||
"anthropic/claude-opus-4-5-20251101": {"n":"Claude Opus 4.5","c":200000,"o":64000,"e":["low","medium","high"]},
|
||||
"anthropic/claude-opus-4-6": {"n":"Claude Opus 4.6","c":1000000,"o":128000,"e":["low","medium","high","max"]},
|
||||
"anthropic/claude-opus-4-7": {"n":"Claude Opus 4.7","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"anthropic/claude-opus-4-8": {"n":"Claude Opus 4.8","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"anthropic/claude-opus-5": {"n":"Claude Opus 5","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"anthropic/claude-sonnet-4-0": {"n":"Claude Sonnet 4 (latest)","c":200000,"o":64000},
|
||||
"anthropic/claude-sonnet-4-20250514": {"n":"Claude Sonnet 4","c":200000,"o":64000},
|
||||
"anthropic/claude-sonnet-4-5": {"n":"Claude Sonnet 4.5 (latest)","c":200000,"o":64000},
|
||||
"anthropic/claude-sonnet-4-5-20250929": {"n":"Claude Sonnet 4.5","c":200000,"o":64000},
|
||||
"anthropic/claude-sonnet-4-6": {"n":"Claude Sonnet 4.6","c":1000000,"o":64000,"e":["low","medium","high","max"]},
|
||||
"anthropic/claude-sonnet-5": {"n":"Claude Sonnet 5","c":1000000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"arcee-ai/trinity-large-preview": {"n":"Trinity Large Preview","c":524288,"o":262144},
|
||||
"arcee-ai/trinity-large-thinking": {"n":"Trinity Large Thinking","c":524288,"o":262144},
|
||||
"arcee-ai/trinity-mini": {"n":"Trinity Mini","c":131072,"o":131072},
|
||||
"arcee-ai/trinity-nano-preview": {"n":"Trinity Nano Preview","c":131072,"o":131072},
|
||||
"bytedance-seed/seed-1-6": {"n":"Seed 1.6","c":256000,"o":64000},
|
||||
"bytedance-seed/seed-1-6-flash": {"n":"Seed 1.6 Flash","c":256000,"o":32000},
|
||||
"bytedance-seed/seed-1-6-vision": {"n":"Seed 1.6 Vision","c":256000,"o":32000},
|
||||
"bytedance-seed/seed-1-8": {"n":"Seed 1.8","c":256000,"o":64000},
|
||||
"bytedance-seed/seed-2.0-code": {"n":"Seed 2.0 Code","c":262144,"o":131072},
|
||||
"bytedance-seed/seed-2.0-lite": {"n":"Seed 2.0 Lite","c":256000,"o":32000},
|
||||
"bytedance-seed/seed-2.0-mini": {"n":"Seed 2.0 Mini","c":256000,"o":32000},
|
||||
"bytedance-seed/seed-2.0-pro": {"n":"Seed 2.0 Pro","c":256000,"o":128000},
|
||||
"bytedance-seed/seed-2.1-pro": {"n":"Seed 2.1 Pro","c":256000,"o":256000},
|
||||
"bytedance-seed/seed-2.1-turbo": {"n":"Seed 2.1 Turbo","c":256000,"o":256000},
|
||||
"bytedance-seed/seed-character": {"n":"Seed Character","c":256000,"o":256000},
|
||||
"bytedance-seed/seed-evolving": {"n":"Seed Evolving","c":256000,"o":256000},
|
||||
"cohere/c4ai-aya-expanse-32b": {"n":"Aya Expanse 32B","c":128000,"o":4000},
|
||||
"cohere/c4ai-aya-expanse-8b": {"n":"Aya Expanse 8B","c":8000,"o":4000},
|
||||
"cohere/c4ai-aya-vision-32b": {"n":"Aya Vision 32B","c":16000,"o":4000},
|
||||
"cohere/c4ai-aya-vision-8b": {"n":"Aya Vision 8B","c":16000,"o":4000},
|
||||
"cohere/command-a-03-2025": {"n":"Command A","c":256000,"o":8000},
|
||||
"cohere/command-a-plus-05-2026": {"n":"Command A Plus","c":128000,"o":64000},
|
||||
"cohere/command-a-reasoning-08-2025": {"n":"Command A Reasoning","c":256000,"o":32000},
|
||||
"cohere/command-a-translate-08-2025": {"n":"Command A Translate","c":8000,"o":8000},
|
||||
"cohere/command-a-vision-07-2025": {"n":"Command A Vision","c":128000,"o":8000},
|
||||
"cohere/command-r-08-2024": {"n":"Command R","c":128000,"o":4000},
|
||||
"cohere/command-r-plus-08-2024": {"n":"Command R+","c":128000,"o":4000},
|
||||
"cohere/command-r7b-12-2024": {"n":"Command R7B","c":128000,"o":4000},
|
||||
"cohere/command-r7b-arabic-02-2025": {"n":"Command R7B Arabic","c":128000,"o":4000},
|
||||
"cohere/north-mini-code-1-0": {"n":"North Mini Code","c":256000,"o":64000,"e":["high"]},
|
||||
"deepreinforce/ornith-1.0-31b": {"n":"Ornith 1.0 31B","c":262144},
|
||||
"deepreinforce/ornith-1.0-35b": {"n":"Ornith 1.0 35B","c":262144},
|
||||
"deepreinforce/ornith-1.0-397b": {"n":"Ornith 1.0 397B","c":262144},
|
||||
"deepreinforce/ornith-1.0-9b": {"n":"Ornith 1.0 9B","c":262144},
|
||||
"deepreinforce/ornith-1.5-35b-a3b": {"n":"Ornith 1.5 35B A3B","c":262144},
|
||||
"deepseek/deepseek-chat": {"n":"DeepSeek Chat","c":1000000,"o":384000},
|
||||
"deepseek/deepseek-ocr-2": {"n":"DeepSeek OCR 2","c":8192,"o":8192},
|
||||
"deepseek/deepseek-r1": {"n":"DeepSeek-R1","c":128000,"o":32768},
|
||||
"deepseek/deepseek-r1-distill-qwen-32b": {"n":"DeepSeek-R1-Distill-Qwen-32B","c":131072,"o":32768},
|
||||
"deepseek/deepseek-reasoner": {"n":"DeepSeek Reasoner","c":1000000,"o":384000},
|
||||
"deepseek/deepseek-v3": {"n":"DeepSeek-V3","c":131072,"o":8192},
|
||||
"deepseek/deepseek-v3-0324": {"n":"DeepSeek V3 0324","c":163840,"o":163840},
|
||||
"deepseek/deepseek-v3.1": {"n":"DeepSeek-V3.1","c":131072,"o":8192},
|
||||
"deepseek/deepseek-v3.2": {"n":"DeepSeek V3.2","c":128000,"o":64000},
|
||||
"deepseek/deepseek-v4-flash": {"n":"DeepSeek V4 Flash","c":1000000,"o":384000,"e":["low","high","max"]},
|
||||
"deepseek/deepseek-v4-flash-0731": {"n":"DeepSeek V4 Flash 0731","c":1000000,"o":384000},
|
||||
"deepseek/deepseek-v4-flash-vision-exp": {"n":"DeepSeek V4 Flash Vision Exp","c":1000000,"o":384000,"e":["low","high","max"]},
|
||||
"deepseek/deepseek-v4-pro": {"n":"DeepSeek V4 Pro","c":1000000,"o":384000,"e":["high","max"]},
|
||||
"deepseek/deepseek-v4-pro-0423": {"n":"DeepSeek V4 Pro 0423","c":1000000,"o":384000},
|
||||
"deepseek/deepseek-v4-pro-0813": {"n":"DeepSeek V4 Pro 0813","c":1000000,"o":384000},
|
||||
"google/deep-research-max-preview-04-2026": {"n":"Deep Research Max Preview","c":1048576,"o":65536},
|
||||
"google/deep-research-preview-04-2026": {"n":"Gemini Deep Research Preview","c":1048576,"o":65536},
|
||||
"google/gemini-2.0-flash": {"n":"Gemini 2.0 Flash","c":1048576,"o":8192},
|
||||
"google/gemini-2.0-flash-lite": {"n":"Gemini 2.0 Flash-Lite","c":1048576,"o":8192},
|
||||
"google/gemini-2.5-computer-use-preview-10-2025": {"n":"Gemini 2.5 Computer Use Preview","c":128000,"o":64000},
|
||||
"google/gemini-2.5-flash": {"n":"Gemini 2.5 Flash","c":1048576,"o":65536},
|
||||
"google/gemini-2.5-flash-image": {"n":"Nano Banana","c":32768,"o":32768},
|
||||
"google/gemini-2.5-flash-lite": {"n":"Gemini 2.5 Flash-Lite","c":1048576,"o":65536},
|
||||
"google/gemini-2.5-flash-tts": {"n":"Gemini 2.5 Flash TTS","c":32768,"o":16384},
|
||||
"google/gemini-2.5-pro": {"n":"Gemini 2.5 Pro","c":1048576,"o":65536},
|
||||
"google/gemini-2.5-pro-tts": {"n":"Gemini 2.5 Pro TTS","c":32768,"o":16384},
|
||||
"google/gemini-3-flash-preview": {"n":"Gemini 3 Flash Preview","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3-pro-image": {"n":"Nano Banana Pro","c":65536,"o":32768,"e":["low","high"]},
|
||||
"google/gemini-3-pro-image-preview": {"n":"Nano Banana Pro","c":65536,"o":32768},
|
||||
"google/gemini-3-pro-preview": {"n":"Gemini 3 Pro Preview","c":1048576,"o":65536},
|
||||
"google/gemini-3.1-flash-image": {"n":"Nano Banana 2","c":131072,"o":32768,"e":["minimal","high"]},
|
||||
"google/gemini-3.1-flash-image-preview": {"n":"Nano Banana 2","c":65536,"o":65536,"e":["minimal","high"]},
|
||||
"google/gemini-3.1-flash-lite": {"n":"Gemini 3.1 Flash Lite","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.1-flash-lite-image": {"n":"Nano Banana 2 Lite","c":65536,"o":4096,"e":["minimal","high"]},
|
||||
"google/gemini-3.1-flash-lite-preview": {"n":"Gemini 3.1 Flash Lite Preview","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.1-flash-live-preview": {"n":"Gemini 3.1 Flash Live Preview","c":131072,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.1-flash-tts-preview": {"n":"Gemini 3.1 Flash TTS Preview","c":8192,"o":16384},
|
||||
"google/gemini-3.1-pro-preview": {"n":"Gemini 3.1 Pro Preview","c":1048576,"o":65536,"e":["low","medium","high"]},
|
||||
"google/gemini-3.1-pro-preview-customtools": {"n":"Gemini 3.1 Pro Preview Custom Tools","c":1048576,"o":65536,"e":["low","medium","high"]},
|
||||
"google/gemini-3.5-flash": {"n":"Gemini 3.5 Flash","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.5-flash-lite": {"n":"Gemini 3.5 Flash Lite","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.5-live-translate-preview": {"n":"Gemini 3.5 Live Translate Preview","c":131072,"o":65536},
|
||||
"google/gemini-3.6-flash": {"n":"Gemini 3.6 Flash","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-3.7-flash": {"n":"Gemini 3.7 Flash","c":1048576,"o":65536,"e":["low","medium","high"]},
|
||||
"google/gemini-3.8-flash": {"n":"Gemini 3.8 Flash","c":1048576,"o":65536,"e":["low","medium","high"]},
|
||||
"google/gemini-embedding-001": {"n":"Gemini Embedding 001","c":2048,"o":1},
|
||||
"google/gemini-embedding-2": {"n":"Gemini Embedding 2","c":8192,"o":3072},
|
||||
"google/gemini-flash-latest": {"n":"Gemini Flash Latest","c":1048576,"o":65536,"e":["low","medium","high"]},
|
||||
"google/gemini-flash-lite-latest": {"n":"Gemini Flash-Lite Latest","c":1048576,"o":65536,"e":["minimal","low","medium","high"]},
|
||||
"google/gemini-omni-flash-preview": {"n":"Gemini Omni Flash Preview","c":1048576,"o":57920},
|
||||
"google/gemini-robotics-er-1.6-preview": {"n":"Gemini Robotics-ER 1.6 Preview","c":131072,"o":65536},
|
||||
"google/gemma-4-26b-a4b-it": {"n":"Gemma 4 26B A4B IT","c":262144,"o":32768},
|
||||
"google/gemma-4-31b-it": {"n":"Gemma 4 31B IT","c":262144,"o":32768},
|
||||
"google/gemma-4-E2B-it": {"n":"Gemma 4 E2B IT","c":131072,"o":8192},
|
||||
"google/gemma-4-E4B-it": {"n":"Gemma 4 E4B IT","c":131072,"o":8192},
|
||||
"google/lyria-3-clip-preview": {"n":"Lyria 3 Clip Preview","c":131072,"o":65536},
|
||||
"google/lyria-3-pro-preview": {"n":"Lyria 3 Pro Preview","c":131072,"o":8192},
|
||||
"google/veo-3.1-fast-generate-preview": {"n":"Veo 3.1 Fast Preview","c":1024},
|
||||
"google/veo-3.1-generate-preview": {"n":"Veo 3.1 Preview","c":1024,"o":1},
|
||||
"google/veo-3.1-lite-generate-preview": {"n":"Veo 3.1 Lite Preview","c":1024},
|
||||
"ibm/granite-4-h-micro": {"n":"Granite-4.0-H-Micro","c":131072,"o":131072},
|
||||
"ibm/granite-4-h-small": {"n":"Granite-4.0-H-Small","c":131072,"o":131072},
|
||||
"inclusionai/ling-3.0-flash-fin": {"n":"Ling 3.0 Flash Fin","c":262144,"o":32768},
|
||||
"meituan/longcat-2.0": {"n":"LongCat-2.0","c":1000000,"o":131072},
|
||||
"meta/llama-3.1-8b-instruct": {"n":"Llama-3.1-8B-Instruct","c":128000,"o":4096},
|
||||
"meta/llama-3.2-11b-vision-instruct": {"n":"Llama-3.2-11B-Vision-Instruct","c":128000,"o":4096},
|
||||
"meta/llama-3.2-1b": {"n":"Llama-3.2-1B","c":131072,"o":8192},
|
||||
"meta/llama-3.2-3b": {"n":"Llama-3.2-3B","c":131072,"o":8192},
|
||||
"meta/llama-3.3-70b-instruct": {"n":"Llama-3.3-70B-Instruct","c":128000,"o":4096},
|
||||
"meta/llama-4-maverick-17b-instruct": {"n":"Llama 4 Maverick 17B Instruct","c":1000000,"o":16384},
|
||||
"meta/llama-4-scout-17b-instruct": {"n":"Llama 4 Scout 17B Instruct","c":3500000,"o":16384},
|
||||
"meta/llama-guard-3-8b": {"n":"Llama-Guard-3-8B","c":128000,"o":4096},
|
||||
"meta/muse-glimmer-30b": {"n":"Muse Glimmer 30B","c":131072,"o":131072},
|
||||
"meta/muse-spark-1.1": {"n":"Muse Spark 1.1","c":1048576,"o":131072,"e":["minimal","low","medium","high","xhigh"]},
|
||||
"meta/muse-spark-1.2": {"n":"Muse Spark 1.2","c":1048576,"o":131072,"e":["minimal","low","medium","high","xhigh"]},
|
||||
"meta/muse-spark-1.3": {"n":"Muse Spark 1.3","c":1048576,"o":131072,"e":["minimal","low","medium","high","xhigh"]},
|
||||
"microsoft/mai-code-1-flash": {"n":"MAI-Code-1-Flash","c":256000,"o":128000},
|
||||
"microsoft/mai-code-1.1-flash": {"n":"MAI-Code-1.1-Flash","c":256000,"o":128000},
|
||||
"microsoft/phi-4-mini": {"n":"Phi-4-mini","c":128000,"o":4096},
|
||||
"minimax/MiniMax-M2": {"n":"MiniMax-M2","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M2-Her": {"n":"MiniMax-M2 Her","c":65536,"o":2048},
|
||||
"minimax/MiniMax-M2.1": {"n":"MiniMax-M2.1","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M2.5": {"n":"MiniMax-M2.5","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M2.5-highspeed": {"n":"MiniMax-M2.5-highspeed","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M2.7": {"n":"MiniMax-M2.7","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M2.7-highspeed": {"n":"MiniMax-M2.7-highspeed","c":204800,"o":131072},
|
||||
"minimax/MiniMax-M3": {"n":"MiniMax-M3","c":1048576,"o":512000},
|
||||
"mistral/codestral-22b-v0.1": {"n":"Codestral-22B-v0.1","c":32768,"o":8192},
|
||||
"mistral/codestral-latest": {"n":"Codestral (latest)","c":256000,"o":4096},
|
||||
"mistral/devstral-2512": {"n":"Devstral 2","c":262144,"o":262144},
|
||||
"mistral/devstral-medium-2507": {"n":"Devstral Medium","c":128000,"o":128000},
|
||||
"mistral/devstral-medium-latest": {"n":"Devstral 2 (latest)","c":262144,"o":262144},
|
||||
"mistral/devstral-small-2507": {"n":"Devstral Small","c":128000,"o":128000},
|
||||
"mistral/magistral-medium-latest": {"n":"Magistral Medium (latest)","c":128000,"o":16384},
|
||||
"mistral/magistral-small-2506": {"n":"Magistral Small","c":131072,"o":8192},
|
||||
"mistral/ministral-8b-instruct-2410": {"n":"Ministral 8B Instruct","c":131072,"o":8192},
|
||||
"mistral/mistral-large-2411": {"n":"Mistral Large 2.1","c":131072,"o":16384},
|
||||
"mistral/mistral-large-2512": {"n":"Mistral Large 3","c":262144,"o":262144},
|
||||
"mistral/mistral-large-latest": {"n":"Mistral Large (latest)","c":262144,"o":262144},
|
||||
"mistral/mistral-medium-2505": {"n":"Mistral Medium 3","c":131072,"o":131072},
|
||||
"mistral/mistral-medium-2604": {"n":"Mistral Medium 3.5","c":262144,"o":262144,"e":["high"]},
|
||||
"mistral/mistral-medium-latest": {"n":"Mistral Medium (latest)","c":262144,"o":262144,"e":["high"]},
|
||||
"mistral/mistral-nemo": {"n":"Mistral Nemo","c":128000,"o":128000},
|
||||
"mistral/mistral-small-2506": {"n":"Mistral Small 3.2","c":128000,"o":16384},
|
||||
"mistral/mistral-small-2603": {"n":"Mistral Small 4","c":256000,"o":256000,"e":["high"]},
|
||||
"mistral/mistral-small-3-1-24b-instruct-2503": {"n":"Mistral Small 3.1 24B","c":128000,"o":16384},
|
||||
"mistral/mistral-small-latest": {"n":"Mistral Small (latest)","c":256000,"o":256000,"e":["high"]},
|
||||
"mistral/pixtral-12b": {"n":"Pixtral 12B","c":128000,"o":128000},
|
||||
"mistral/pixtral-large-latest": {"n":"Pixtral Large (latest)","c":128000,"o":128000},
|
||||
"mistral/voxtral-small-latest": {"n":"Voxtral Small (latest)","c":32000,"o":32000},
|
||||
"moonshotai/kimi-k2-thinking": {"n":"Kimi K2 Thinking","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k2-thinking-turbo": {"n":"Kimi K2 Thinking Turbo","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k2.5": {"n":"Kimi K2.5","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k2.6": {"n":"Kimi K2.6","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k2.7-code": {"n":"Kimi K2.7 Code","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k2.7-code-highspeed": {"n":"Kimi K2.7 Code Highspeed","c":262144,"o":262144},
|
||||
"moonshotai/kimi-k3": {"n":"Kimi K3","c":1048576,"o":131072,"e":["low","high","max"]},
|
||||
"nvidia/llama-3.1-nemotron-70b-instruct": {"n":"Llama 3.1 Nemotron 70B Instruct","c":128000,"o":8192},
|
||||
"nvidia/llama-3.1-nemotron-safety-guard-8b-v3": {"n":"Llama 3.1 Nemotron Safety Guard 8B v3","c":128000,"o":4096},
|
||||
"nvidia/llama-3.1-nemotron-ultra-253b": {"n":"Llama 3.1 Nemotron Ultra 253B","c":128000,"o":8192},
|
||||
"nvidia/llama-3.3-nemotron-super-49b-v1": {"n":"Llama 3.3 Nemotron Super 49B v1","c":131072,"o":131072},
|
||||
"nvidia/llama-3.3-nemotron-super-49b-v1.5": {"n":"Llama 3.3 Nemotron Super 49B v1.5","c":131072,"o":131072},
|
||||
"nvidia/llama-nemotron-embed-vl-1b-v2": {"n":"Llama Nemotron Embed VL 1B v2","c":32768,"o":2048},
|
||||
"nvidia/llama-nemotron-rerank-vl-1b-v2": {"n":"Llama Nemotron Rerank VL 1B v2","c":128000,"o":4096},
|
||||
"nvidia/mistral-nemotron": {"n":"Mistral Nemotron","c":128000,"o":8192},
|
||||
"nvidia/nemotron-3-content-safety": {"n":"Nemotron 3 Content Safety","c":128000,"o":4096},
|
||||
"nvidia/nemotron-3-nano-30b-a3b": {"n":"Nemotron 3 Nano 30B A3B","c":262144,"o":262144},
|
||||
"nvidia/nemotron-3-nano-omni-30b-a3b-reasoning": {"n":"Nemotron 3 Nano Omni 30B A3B Reasoning","c":256000,"o":65536},
|
||||
"nvidia/nemotron-3-super-120b-a12b": {"n":"Nemotron 3 Super 120B A12B","c":262144,"o":262144},
|
||||
"nvidia/nemotron-3-ultra-550b-a55b": {"n":"Nemotron 3 Ultra 550B A55B","c":1000000,"o":128000},
|
||||
"nvidia/nemotron-3.5-content-safety": {"n":"Nemotron 3.5 Content Safety","c":128000,"o":8192},
|
||||
"nvidia/nemotron-3.5-lightning": {"n":"Nemotron 3.5 Lightning 30B A3B","c":262144,"o":262144},
|
||||
"nvidia/nemotron-cascade-2-30b-a3b": {"n":"Nemotron Cascade 2 30B A3B","c":256000,"o":32768},
|
||||
"nvidia/nemotron-content-safety-reasoning-4b": {"n":"Nemotron Content Safety Reasoning 4B","c":128000,"o":4096},
|
||||
"nvidia/nemotron-mini-4b-instruct": {"n":"Nemotron Mini 4B Instruct","c":128000,"o":8192},
|
||||
"nvidia/nemotron-nano-12b-v2-vl": {"n":"Nemotron Nano 12B v2 VL","c":128000,"o":128000},
|
||||
"nvidia/nemotron-nano-9b-v2": {"n":"Nemotron Nano 9B v2","c":131072,"o":131072},
|
||||
"nvidia/nemotron-voicechat": {"n":"Nemotron VoiceChat","c":128000,"o":8192},
|
||||
"openai/gpt-3.5-turbo": {"n":"GPT-3.5-turbo","c":16385,"o":4096},
|
||||
"openai/gpt-4": {"n":"GPT-4","c":8192,"o":8192},
|
||||
"openai/gpt-4-turbo": {"n":"GPT-4 Turbo","c":128000,"o":4096},
|
||||
"openai/gpt-4.1": {"n":"GPT-4.1","c":1047576,"o":32768},
|
||||
"openai/gpt-4.1-mini": {"n":"GPT-4.1 mini","c":1047576,"o":32768},
|
||||
"openai/gpt-4.1-nano": {"n":"GPT-4.1 nano","c":1047576,"o":32768},
|
||||
"openai/gpt-4o": {"n":"GPT-4o","c":128000,"o":16384},
|
||||
"openai/gpt-4o-2024-05-13": {"n":"GPT-4o (2024-05-13)","c":128000,"o":4096},
|
||||
"openai/gpt-4o-2024-08-06": {"n":"GPT-4o (2024-08-06)","c":128000,"o":16384},
|
||||
"openai/gpt-4o-2024-11-20": {"n":"GPT-4o (2024-11-20)","c":128000,"o":16384},
|
||||
"openai/gpt-4o-mini": {"n":"GPT-4o mini","c":128000,"o":16384},
|
||||
"openai/gpt-5": {"n":"GPT-5","c":400000,"o":128000,"e":["minimal","low","medium","high"]},
|
||||
"openai/gpt-5-chat-latest": {"n":"GPT-5 Chat (latest)","c":400000,"o":128000},
|
||||
"openai/gpt-5-codex": {"n":"GPT-5-Codex","c":400000,"o":128000,"e":["low","medium","high"]},
|
||||
"openai/gpt-5-mini": {"n":"GPT-5 Mini","c":400000,"o":128000,"e":["minimal","low","medium","high"]},
|
||||
"openai/gpt-5-nano": {"n":"GPT-5 Nano","c":400000,"o":128000,"e":["minimal","low","medium","high"]},
|
||||
"openai/gpt-5-pro": {"n":"GPT-5 Pro","c":400000,"o":272000,"e":["high"]},
|
||||
"openai/gpt-5.1": {"n":"GPT-5.1","c":400000,"o":128000,"e":["low","medium","high"]},
|
||||
"openai/gpt-5.1-chat-latest": {"n":"GPT-5.1 Chat","c":128000,"o":16384},
|
||||
"openai/gpt-5.1-codex": {"n":"GPT-5.1 Codex","c":400000,"o":128000,"e":["low","medium","high"]},
|
||||
"openai/gpt-5.1-codex-max": {"n":"GPT-5.1 Codex Max","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.1-codex-mini": {"n":"GPT-5.1 Codex mini","c":400000,"o":128000,"e":["low","medium","high"]},
|
||||
"openai/gpt-5.2": {"n":"GPT-5.2","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.2-chat-latest": {"n":"GPT-5.2 Chat","c":128000,"o":16384,"e":["medium"]},
|
||||
"openai/gpt-5.2-codex": {"n":"GPT-5.2 Codex","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.2-pro": {"n":"GPT-5.2 Pro","c":400000,"o":128000,"e":["medium","high","xhigh"]},
|
||||
"openai/gpt-5.3-chat-latest": {"n":"GPT-5.3 Chat (latest)","c":128000,"o":16384},
|
||||
"openai/gpt-5.3-codex": {"n":"GPT-5.3 Codex","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.3-codex-spark": {"n":"GPT-5.3 Codex Spark","c":128000,"o":32000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.4": {"n":"GPT-5.4","c":1050000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.4-mini": {"n":"GPT-5.4 mini","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.4-nano": {"n":"GPT-5.4 nano","c":400000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.4-pro": {"n":"GPT-5.4 Pro","c":1050000,"o":128000,"e":["medium","high","xhigh"]},
|
||||
"openai/gpt-5.5": {"n":"GPT-5.5","c":1050000,"o":128000,"e":["low","medium","high","xhigh"]},
|
||||
"openai/gpt-5.5-instant": {"n":"GPT-5.5 Instant","c":400000,"o":128000},
|
||||
"openai/gpt-5.5-pro": {"n":"GPT-5.5 Pro","c":1050000,"o":128000,"e":["medium","high","xhigh"]},
|
||||
"openai/gpt-5.6-luna": {"n":"GPT-5.6 Luna","c":1050000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"openai/gpt-5.6-sol": {"n":"GPT-5.6 Sol","c":1050000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"openai/gpt-5.6-terra": {"n":"GPT-5.6 Terra","c":1050000,"o":128000,"e":["low","medium","high","xhigh","max"]},
|
||||
"openai/gpt-oss-120b": {"n":"GPT OSS 120B","c":131072,"o":32768},
|
||||
"openai/gpt-oss-20b": {"n":"GPT OSS 20B","c":131072,"o":32768},
|
||||
"openai/gpt-oss-safeguard-120b": {"n":"GPT OSS Safeguard 120B","c":131072,"o":32768},
|
||||
"openai/gpt-realtime-2.1": {"n":"GPT-Realtime-2.1","c":128000,"o":32000,"e":["minimal","low","medium","high","xhigh"]},
|
||||
"openai/o1": {"n":"o1","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o1-pro": {"n":"o1-pro","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o3": {"n":"o3","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o3-deep-research": {"n":"o3-deep-research","c":200000,"o":100000},
|
||||
"openai/o3-mini": {"n":"o3-mini","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o3-pro": {"n":"o3-pro","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o4-mini": {"n":"o4-mini","c":200000,"o":100000,"e":["low","medium","high"]},
|
||||
"openai/o4-mini-deep-research": {"n":"o4-mini-deep-research","c":200000,"o":100000},
|
||||
"openai/whisper-large-v3": {"n":"Whisper 3 Large","c":448,"o":4096},
|
||||
"openai/whisper-large-v3-turbo": {"n":"Whisper Large v3 Turbo","c":448,"o":448},
|
||||
"openbmb/minicpm5-1b": {"n":"MiniCPM5-1B","c":131072,"o":131072},
|
||||
"perplexity/sonar": {"n":"Sonar","c":128000,"o":4096},
|
||||
"perplexity/sonar-deep-research": {"n":"Sonar Deep Research","c":128000,"o":32768,"e":["minimal","low","medium","high"]},
|
||||
"perplexity/sonar-pro": {"n":"Sonar Pro","c":200000,"o":8192},
|
||||
"perplexity/sonar-reasoning-pro": {"n":"Sonar Reasoning Pro","c":128000,"o":4096,"e":["minimal","low","medium","high"]},
|
||||
"poolside/laguna-m.1": {"n":"Laguna M.1","c":262144,"o":32768},
|
||||
"poolside/laguna-s-2.1": {"n":"Laguna S 2.1","c":1048576,"o":32768},
|
||||
"poolside/laguna-xs-2.1": {"n":"Laguna XS 2.1","c":262144,"o":32768},
|
||||
"poolside/laguna-xs.2": {"n":"Laguna XS.2","c":262144,"o":32768},
|
||||
"sakana/fugu": {"n":"Fugu","c":1000000,"e":["high","xhigh"]},
|
||||
"sakana/fugu-ultra": {"n":"Fugu Ultra","c":1000000,"e":["high","xhigh"]},
|
||||
"sakana/sakana-namazu": {"n":"Sakana Namazu","c":262144,"o":65536},
|
||||
"sarvam/sarvam-105b": {"n":"Sarvam 105B","c":131072,"o":131072,"e":["low","medium","high"]},
|
||||
"sarvam/sarvam-30b": {"n":"Sarvam 30B","c":128000,"o":128000,"e":["low","medium","high"]},
|
||||
"sdaia/allam-2-7b": {"n":"ALLaM-2-7b","c":4096,"o":4096},
|
||||
"stepfun/step-3.5-flash": {"n":"Step 3.5 Flash","c":256000,"o":256000,"e":["low","high"]},
|
||||
"stepfun/step-3.5-flash-2603": {"n":"Step 3.5 Flash 2603","c":256000,"o":256000,"e":["low","high"]},
|
||||
"stepfun/step-3.7-flash": {"n":"Step 3.7 Flash","c":256000,"o":256000,"e":["low","medium","high"]},
|
||||
"swiss-ai/apertus-70b": {"n":"Apertus 70B","c":65536,"o":8192},
|
||||
"swiss-ai/apertus-8b": {"n":"Apertus 8B","c":65536,"o":8192},
|
||||
"tencent/hy3": {"n":"Hy3","c":256000,"o":128000},
|
||||
"tencent/hy3-preview": {"n":"Hy3 preview","c":256000,"o":64000},
|
||||
"tencent/hy4-preview": {"n":"Hy4 preview","c":1024000,"o":64000},
|
||||
"thinkingmachines/inkling": {"n":"Inkling","c":1048576,"o":1048576},
|
||||
"thinkingmachines/inkling-small": {"n":"Inkling Small","c":1048576,"o":1048576},
|
||||
"trendyol/asure-12b": {"n":"Trendyol Asure 12B","c":131072},
|
||||
"upstage/solar-pro2": {"n":"Solar Pro 2","c":65536,"o":8192,"e":["minimal","high"]},
|
||||
"upstage/solar-pro3": {"n":"Solar Pro 3","c":131072,"o":8192,"e":["low","medium","high"]},
|
||||
"upstage/solar-pro4": {"n":"Solar Pro 4","c":524288,"o":131072,"e":["minimal","low","medium","high","xhigh","max"]},
|
||||
"xai/grok-4.1-fast": {"n":"Grok 4.1 Fast","c":2000000,"o":30000},
|
||||
"xai/grok-4.20-0309-non-reasoning": {"n":"Grok 4.20 (Non-Reasoning)","c":1000000,"o":30000},
|
||||
"xai/grok-4.20-0309-reasoning": {"n":"Grok 4.20 (Reasoning)","c":1000000,"o":30000},
|
||||
"xai/grok-4.3": {"n":"Grok 4.3","c":1000000,"o":30000,"e":["low","medium","high"]},
|
||||
"xai/grok-4.5": {"n":"Grok 4.5","c":500000,"o":500000,"e":["low","medium","high"]},
|
||||
"xai/grok-4.6": {"n":"Grok 4.6","c":500000,"o":500000,"e":["low","medium","high","xhigh"]},
|
||||
"xai/grok-build-0.1": {"n":"Grok Build 0.1","c":256000,"o":256000},
|
||||
"xai/grok-imagine-image-2.0": {"n":"Grok Imagine Image 2.0","c":8000},
|
||||
"xai/grok-imagine-video-1.5": {"n":"Grok Imagine Video 1.5","c":1024},
|
||||
"xiaomi/mimo-v2-flash": {"n":"MiMo-V2-Flash","c":262144,"o":65536},
|
||||
"xiaomi/mimo-v2-omni": {"n":"MiMo-V2-Omni","c":262144,"o":131072},
|
||||
"xiaomi/mimo-v2-pro": {"n":"MiMo-V2-Pro","c":1048576,"o":131072},
|
||||
"xiaomi/mimo-v2.5": {"n":"MiMo-V2.5","c":1048576,"o":131072},
|
||||
"xiaomi/mimo-v2.5-pro": {"n":"MiMo-V2.5-Pro","c":1048576,"o":131072},
|
||||
"xiaomi/mimo-v2.5-pro-ultraspeed": {"n":"MiMo-V2.5-Pro-UltraSpeed","c":1048576,"o":131072},
|
||||
"zhipuai/glm-4.5": {"n":"GLM-4.5","c":131072,"o":98304},
|
||||
"zhipuai/glm-4.5-air": {"n":"GLM-4.5-Air","c":131072,"o":98304},
|
||||
"zhipuai/glm-4.5-flash": {"n":"GLM-4.5-Flash","c":131072,"o":98304},
|
||||
"zhipuai/glm-4.5v": {"n":"GLM-4.5V","c":64000,"o":16384},
|
||||
"zhipuai/glm-4.6": {"n":"GLM-4.6","c":204800,"o":131072},
|
||||
"zhipuai/glm-4.6v": {"n":"GLM-4.6V","c":128000,"o":32768},
|
||||
"zhipuai/glm-4.6v-flash": {"n":"GLM-4.6V-Flash","c":128000,"o":32768},
|
||||
"zhipuai/glm-4.7": {"n":"GLM-4.7","c":204800,"o":131072},
|
||||
"zhipuai/glm-4.7-flash": {"n":"GLM-4.7-Flash","c":200000,"o":131072},
|
||||
"zhipuai/glm-4.7-flashx": {"n":"GLM-4.7-FlashX","c":200000,"o":131072},
|
||||
"zhipuai/glm-5": {"n":"GLM-5","c":204800,"o":131072},
|
||||
"zhipuai/glm-5-turbo": {"n":"GLM-5-Turbo","c":200000,"o":131072},
|
||||
"zhipuai/glm-5.1": {"n":"GLM-5.1","c":200000,"o":131072},
|
||||
"zhipuai/glm-5.2": {"n":"GLM-5.2","c":1000000,"o":131072,"e":["high","max"]},
|
||||
"zhipuai/glm-5.3": {"n":"GLM-5.3","c":1000000,"o":131072,"e":["low","high","max"]},
|
||||
"zhipuai/glm-5.3-flash": {"n":"GLM-5.3-Flash","c":1000000,"o":131072,"e":["low","high","max"]},
|
||||
"zhipuai/glm-5v-turbo": {"n":"GLM-5V-Turbo","c":200000,"o":131072},
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
import { describe, it, expect } from 'bun:test'
|
||||
import {
|
||||
normalizeModelId,
|
||||
lookupCatalog,
|
||||
resolveModelLimits,
|
||||
FALLBACK_CONTEXT,
|
||||
FALLBACK_OUTPUT,
|
||||
} from './modelCatalog.ts'
|
||||
import { MODEL_CATALOG } from './modelCatalog.generated.ts'
|
||||
|
||||
describe('modelCatalog', () => {
|
||||
describe('normalizeModelId', () => {
|
||||
it('normalizes provider prefixed IDs', () => {
|
||||
expect(normalizeModelId('cc/claude-sonnet-4.5')).toBe('claude-sonnet-4-5')
|
||||
expect(normalizeModelId('anthropic/claude-opus-4-7')).toBe('claude-opus-4-7')
|
||||
})
|
||||
|
||||
it('normalizes bare IDs', () => {
|
||||
expect(normalizeModelId('claude-sonnet-4.5')).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('normalizes multi-tier IDs', () => {
|
||||
expect(normalizeModelId('openrouter/anthropic/claude-sonnet-4.5')).toBe('anthropic/claude-sonnet-4-5')
|
||||
})
|
||||
|
||||
it('handles uppercase characters', () => {
|
||||
expect(normalizeModelId('CC/Claude-Sonnet-4.5')).toBe('claude-sonnet-4-5')
|
||||
})
|
||||
})
|
||||
|
||||
describe('lookupCatalog - verified IDs', () => {
|
||||
const verifiedMatches: [string, string][] = [
|
||||
['cc/claude-sonnet-4.5', 'anthropic/claude-sonnet-4-5'],
|
||||
['kr/claude-sonnet-4.5', 'anthropic/claude-sonnet-4-5'],
|
||||
['cc/claude-opus-4-5-20251101', 'anthropic/claude-opus-4-5-20251101'],
|
||||
['cx/gpt-5.6-sol', 'openai/gpt-5.6-sol'],
|
||||
['cx/gpt-5.3-codex', 'openai/gpt-5.3-codex'],
|
||||
['ag/gemini-3.1-pro-preview', 'google/gemini-3.1-pro-preview'],
|
||||
['vertex/gemini-3-flash-preview', 'google/gemini-3-flash-preview'],
|
||||
['openai/gpt-4o', 'openai/gpt-4o'],
|
||||
['openai/o3-mini', 'openai/o3-mini'],
|
||||
['deepseek/deepseek-r1', 'deepseek/deepseek-r1'],
|
||||
['cc/claude-opus-4-7', 'anthropic/claude-opus-4-7'],
|
||||
]
|
||||
|
||||
for (const [queryId, expectedCatalogKey] of verifiedMatches) {
|
||||
it(`resolves ${queryId} -> ${expectedCatalogKey}`, () => {
|
||||
const hit = lookupCatalog(queryId)
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.key).toBe(expectedCatalogKey)
|
||||
})
|
||||
}
|
||||
|
||||
it('returns null on unknown models', () => {
|
||||
expect(lookupCatalog('foo/definitely-not-real')).toBeNull()
|
||||
})
|
||||
|
||||
it('matches date suffix aliases (tier 4)', () => {
|
||||
// cc/claude-sonnet-4-6-20260101 does not exist in catalog, but anthropic/claude-sonnet-4-6 does
|
||||
const hit = lookupCatalog('cc/claude-sonnet-4-6-20260101')
|
||||
expect(hit).not.toBeNull()
|
||||
expect(hit!.key).toBe('anthropic/claude-sonnet-4-6')
|
||||
})
|
||||
})
|
||||
|
||||
describe('catalog integrity & fuzz invariant', () => {
|
||||
const catalogKeys = Object.keys(MODEL_CATALOG)
|
||||
|
||||
it('contains usable catalog entries', () => {
|
||||
expect(catalogKeys.length).toBeGreaterThanOrEqual(300)
|
||||
})
|
||||
|
||||
it('validates every entry in MODEL_CATALOG adheres to invariants', () => {
|
||||
for (const key of catalogKeys) {
|
||||
const entry = MODEL_CATALOG[key]
|
||||
expect(entry.c).toBeGreaterThan(0)
|
||||
if (entry.o !== undefined) {
|
||||
expect(entry.o).toBeGreaterThan(0)
|
||||
}
|
||||
if (entry.e !== undefined) {
|
||||
expect(entry.e.length).toBeGreaterThan(0)
|
||||
}
|
||||
|
||||
const limits = resolveModelLimits({ id: key })
|
||||
expect(limits.maxInputTokens).toBeGreaterThanOrEqual(1)
|
||||
expect(limits.maxOutputTokens).toBeGreaterThanOrEqual(1)
|
||||
expect(limits.maxInputTokens + limits.maxOutputTokens).toBeLessThanOrEqual(limits.contextWindow)
|
||||
expect(limits.source).toBe('models.dev')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('resolveModelLimits specifics', () => {
|
||||
it('preserves non-degenerate limits where output > context / 2 (zhipuai/glm-4.7)', () => {
|
||||
const hit = lookupCatalog('zhipuai/glm-4.7')
|
||||
if (hit) {
|
||||
const limits = resolveModelLimits({ id: 'zhipuai/glm-4.7' })
|
||||
expect(limits.maxOutputTokens).toBe(131072)
|
||||
expect(limits.maxInputTokens).toBe(73728)
|
||||
expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(204800)
|
||||
}
|
||||
})
|
||||
|
||||
it('preserves non-degenerate limits (openai/gpt-5-pro)', () => {
|
||||
const hit = lookupCatalog('openai/gpt-5-pro')
|
||||
if (hit) {
|
||||
const limits = resolveModelLimits({ id: 'openai/gpt-5-pro' })
|
||||
expect(limits.maxOutputTokens).toBe(272000)
|
||||
expect(limits.maxInputTokens).toBe(128000)
|
||||
expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(400000)
|
||||
}
|
||||
})
|
||||
|
||||
it('synthesizes degenerate output limit when declared output >= context (xai/grok-4.6)', () => {
|
||||
const hit = lookupCatalog('xai/grok-4.6')
|
||||
if (hit && hit.entry.o === hit.entry.c) {
|
||||
const limits = resolveModelLimits({ id: 'xai/grok-4.6' })
|
||||
expect(limits.maxOutputTokens).toBe(125000)
|
||||
expect(limits.maxInputTokens).toBe(375000)
|
||||
expect(limits.maxInputTokens + limits.maxOutputTokens).toBe(500000)
|
||||
}
|
||||
})
|
||||
|
||||
it('follows precedence: models.dev wins over gateway arguments', () => {
|
||||
const limits = resolveModelLimits({
|
||||
id: 'cc/claude-sonnet-4.5',
|
||||
contextWindow: 999999,
|
||||
maxOutputTokens: 1111,
|
||||
})
|
||||
expect(limits.source).toBe('models.dev')
|
||||
expect(limits.contextWindow).toBe(200000)
|
||||
expect(limits.maxOutputTokens).toBe(64000)
|
||||
expect(limits.maxInputTokens).toBe(136000)
|
||||
})
|
||||
|
||||
it('uses gateway limits when uncataloged', () => {
|
||||
const limits = resolveModelLimits({
|
||||
id: 'private/custom-model',
|
||||
contextWindow: 64000,
|
||||
maxOutputTokens: 8192,
|
||||
})
|
||||
expect(limits.source).toBe('gateway')
|
||||
expect(limits.contextWindow).toBe(64000)
|
||||
expect(limits.maxOutputTokens).toBe(8192)
|
||||
expect(limits.maxInputTokens).toBe(55808)
|
||||
})
|
||||
|
||||
it('uses defaults when uncataloged and no gateway limits provided', () => {
|
||||
const limits = resolveModelLimits({
|
||||
id: 'private/custom-model',
|
||||
})
|
||||
expect(limits.source).toBe('default')
|
||||
expect(limits.contextWindow).toBe(FALLBACK_CONTEXT)
|
||||
expect(limits.maxOutputTokens).toBe(FALLBACK_OUTPUT)
|
||||
expect(limits.maxInputTokens).toBe(FALLBACK_CONTEXT - FALLBACK_OUTPUT)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,153 @@
|
||||
// Types, normalize, lookup, and limit resolver for models.dev catalog
|
||||
import { MODEL_CATALOG } from './modelCatalog.generated.ts'
|
||||
|
||||
export interface CatalogEntry {
|
||||
/** Display name from models.dev - for tooltip/provenance only, never emitted into tool configs */
|
||||
n: string
|
||||
/** Context window limit, always > 0 */
|
||||
c: number
|
||||
/** Output token limit; absent when models.dev does not declare it */
|
||||
o?: number
|
||||
/** Sanitized reasoning efforts; absent when unavailable */
|
||||
e?: readonly string[]
|
||||
}
|
||||
|
||||
export type LimitSource = 'models.dev' | 'gateway' | 'default'
|
||||
|
||||
export interface ResolvedLimits {
|
||||
contextWindow: number
|
||||
maxInputTokens: number
|
||||
maxOutputTokens: number
|
||||
reasoningEfforts: string[]
|
||||
source: LimitSource
|
||||
catalogId: string | null
|
||||
catalogName: string | null
|
||||
}
|
||||
|
||||
export interface ModelLimitsInput {
|
||||
id: string
|
||||
contextWindow?: number
|
||||
maxOutputTokens?: number
|
||||
reasoning?: boolean
|
||||
}
|
||||
|
||||
export const FALLBACK_CONTEXT = 128000
|
||||
export const FALLBACK_OUTPUT = 16384
|
||||
export const DEGENERATE_SHARE = 0.25
|
||||
export const DEFAULT_EFFORTS = ['low', 'medium', 'high'] as const
|
||||
|
||||
/**
|
||||
* Normalizes a model identifier:
|
||||
* - strips provider prefix before the first slash (if any)
|
||||
* - converts to lowercase
|
||||
* - replaces dots with dashes
|
||||
*/
|
||||
export function normalizeModelId(id: string): string {
|
||||
const parts = id.split('/')
|
||||
const stripped = parts.length > 1 ? parts.slice(1).join('/') : id
|
||||
return stripped.toLowerCase().replace(/\./g, '-')
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy index mapping normalized aliases to catalog entries.
|
||||
*/
|
||||
let catalogIndex: Map<string, { key: string; entry: CatalogEntry }> | null = null
|
||||
|
||||
function getIndex(): Map<string, { key: string; entry: CatalogEntry }> {
|
||||
if (catalogIndex) {
|
||||
return catalogIndex
|
||||
}
|
||||
const index = new Map<string, { key: string; entry: CatalogEntry }>()
|
||||
for (const [key, entry] of Object.entries(MODEL_CATALOG)) {
|
||||
const item = { key, entry }
|
||||
// 1. Full normalized key: "anthropic/claude-sonnet-4-5"
|
||||
const fullNormalized = key.toLowerCase().replace(/\./g, '-')
|
||||
if (!index.has(fullNormalized)) {
|
||||
index.set(fullNormalized, item)
|
||||
}
|
||||
// 2. Stripped normalized alias: "claude-sonnet-4-5"
|
||||
const stripped = normalizeModelId(key)
|
||||
if (!index.has(stripped)) {
|
||||
index.set(stripped, item)
|
||||
}
|
||||
}
|
||||
catalogIndex = index
|
||||
return index
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a model in the catalog using 4 matching tiers:
|
||||
* 1. full id lowercased + '.' -> '-'
|
||||
* 2. normalizeModelId(id) (strip first segment)
|
||||
* 3. segment after the LAST slash (for 3-tier IDs like openrouter/anthropic/claude-x)
|
||||
* 4. tier 2 after stripping date suffix (-YYYYMMDD or -YYYY-MM-DD)
|
||||
*/
|
||||
export function lookupCatalog(id: string): { key: string; entry: CatalogEntry } | null {
|
||||
const index = getIndex()
|
||||
|
||||
// Tier 1: full id lowercased + '.' -> '-'
|
||||
const tier1 = id.toLowerCase().replace(/\./g, '-')
|
||||
const hit1 = index.get(tier1)
|
||||
if (hit1) return hit1
|
||||
|
||||
// Tier 2: normalizeModelId(id)
|
||||
const tier2 = normalizeModelId(id)
|
||||
const hit2 = index.get(tier2)
|
||||
if (hit2) return hit2
|
||||
|
||||
// Tier 3: last segment after last slash
|
||||
const lastSlashIndex = id.lastIndexOf('/')
|
||||
if (lastSlashIndex !== -1) {
|
||||
const lastSegment = id.slice(lastSlashIndex + 1).toLowerCase().replace(/\./g, '-')
|
||||
const hit3 = index.get(lastSegment)
|
||||
if (hit3) return hit3
|
||||
}
|
||||
|
||||
// Tier 4: strip date suffix (-YYYYMMDD or -YYYY-MM-DD) from tier 2
|
||||
const dateStripped = tier2.replace(/-\d{8}$/, '').replace(/-\d{4}-\d{2}-\d{2}$/, '')
|
||||
if (dateStripped !== tier2) {
|
||||
const hit4 = index.get(dateStripped)
|
||||
if (hit4) return hit4
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves contextWindow, maxInputTokens, maxOutputTokens, and reasoningEfforts
|
||||
* based on the precedence: models.dev -> gateway -> fallback defaults.
|
||||
*/
|
||||
export function resolveModelLimits(model: ModelLimitsInput): ResolvedLimits {
|
||||
const hit = lookupCatalog(model.id)
|
||||
|
||||
const context = hit?.entry.c ?? model.contextWindow ?? FALLBACK_CONTEXT
|
||||
const declared = hit?.entry.o ?? model.maxOutputTokens ?? FALLBACK_OUTPUT
|
||||
|
||||
// Guard: only synthesize when declared value is not positive or >= context window
|
||||
let out = (declared > 0 && declared < context)
|
||||
? declared
|
||||
: Math.max(1, Math.floor(context * DEGENERATE_SHARE))
|
||||
out = Math.min(out, context - 1)
|
||||
|
||||
const input = context - out // always >= 1
|
||||
|
||||
const reasoningEfforts = hit?.entry.e
|
||||
? [...hit.entry.e]
|
||||
: [...DEFAULT_EFFORTS]
|
||||
|
||||
const source: LimitSource = hit
|
||||
? 'models.dev'
|
||||
: (model.contextWindow || model.maxOutputTokens)
|
||||
? 'gateway'
|
||||
: 'default'
|
||||
|
||||
return {
|
||||
contextWindow: context,
|
||||
maxInputTokens: input,
|
||||
maxOutputTokens: out,
|
||||
reasoningEfforts,
|
||||
source,
|
||||
catalogId: hit?.key ?? null,
|
||||
catalogName: hit?.entry.n ?? null,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { describe, it, expect } from 'bun:test'
|
||||
import { tools } from './tools.ts'
|
||||
import type { ConnectionConfig, Model } from '../types.ts'
|
||||
|
||||
describe('tools configuration generators', () => {
|
||||
const dummyConn: ConnectionConfig = {
|
||||
baseUrl: 'https://gateway.example.com/v1',
|
||||
apiKey: 'sk-test-key-123',
|
||||
}
|
||||
|
||||
describe('copilot generator', () => {
|
||||
it('produces valid JSON array grouped by provider with vendor customendpoint', () => {
|
||||
const models: Model[] = [
|
||||
{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' },
|
||||
{ id: 'openai/gpt-4o', name: 'GPT-4o' },
|
||||
]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
|
||||
expect(Array.isArray(parsed)).toBe(true)
|
||||
expect(parsed.length).toBe(2)
|
||||
for (const group of parsed) {
|
||||
expect(group.vendor).toBe('customendpoint')
|
||||
expect(group.apiKey).toBe('sk-test-key-123')
|
||||
expect(group.apiType).toBe('chat-completions')
|
||||
expect(Array.isArray(group.models)).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves limits for cc/claude-sonnet-4.5 accurately (136000 / 64000)', () => {
|
||||
const models: Model[] = [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' }]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const model = parsed[0].models[0]
|
||||
|
||||
expect(model.maxInputTokens).toBe(136000)
|
||||
expect(model.maxOutputTokens).toBe(64000)
|
||||
expect(model.maxInputTokens + model.maxOutputTokens).toBe(200000)
|
||||
})
|
||||
|
||||
it('emits sanitized reasoning efforts for cc/claude-opus-4-7 when reasoning is true', () => {
|
||||
const models: Model[] = [{ id: 'cc/claude-opus-4-7', name: 'Claude Opus 4.7', reasoning: true }]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const model = parsed[0].models[0]
|
||||
|
||||
expect(model.thinking).toBe(true)
|
||||
expect(model.reasoningEffortFormat).toBe('chat-completions')
|
||||
expect(model.supportsReasoningEffort).toEqual(['low', 'medium', 'high', 'xhigh', 'max'])
|
||||
})
|
||||
|
||||
it('falls back to default efforts for reasoning model without effort catalog entry', () => {
|
||||
const models: Model[] = [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5', reasoning: true }]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const model = parsed[0].models[0]
|
||||
|
||||
expect(model.thinking).toBe(true)
|
||||
expect(model.supportsReasoningEffort).toEqual(['low', 'medium', 'high'])
|
||||
})
|
||||
|
||||
it('does not emit thinking or reasoning effort keys for non-reasoning models', () => {
|
||||
const models: Model[] = [{ id: 'openai/gpt-4o', name: 'GPT-4o', reasoning: false }]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const model = parsed[0].models[0]
|
||||
|
||||
expect(model.thinking).toBeUndefined()
|
||||
expect(model.supportsReasoningEffort).toBeUndefined()
|
||||
expect(model.reasoningEffortFormat).toBeUndefined()
|
||||
})
|
||||
|
||||
it('does not contain minOutput in serialized output', () => {
|
||||
const models: Model[] = [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' }]
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, models)
|
||||
expect(jsonStr.includes('minOutput')).toBe(false)
|
||||
})
|
||||
|
||||
it('handles empty selectedModels using fallback without throwing', () => {
|
||||
const jsonStr = tools.copilot.sampleTemplate(dummyConn, [])
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
expect(Array.isArray(parsed)).toBe(true)
|
||||
expect(parsed.length).toBeGreaterThan(0)
|
||||
})
|
||||
})
|
||||
|
||||
describe('claude-code and codex templates unchanged', () => {
|
||||
it('claude-code produces expected JSON config with comments', () => {
|
||||
const models: Model[] = [{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' }]
|
||||
const config = tools['claude-code'].sampleTemplate(dummyConn, models)
|
||||
expect(config).toContain('"model": "cc/claude-sonnet-4.5"')
|
||||
expect(config).toContain('"anthropic_api_base": "https://gateway.example.com/v1"')
|
||||
expect(config).toContain('// Target: ~/.claude/config.json')
|
||||
})
|
||||
|
||||
it('codex produces expected TOML config', () => {
|
||||
const models: Model[] = [{ id: 'openai/gpt-4o', name: 'GPT-4o' }]
|
||||
const toml = tools.codex.sampleTemplate(dummyConn, models)
|
||||
expect(toml).toContain('model = "openai/gpt-4o"')
|
||||
expect(toml).toContain('base_url = "https://gateway.example.com/v1"')
|
||||
})
|
||||
})
|
||||
})
|
||||
+8
-5
@@ -1,4 +1,5 @@
|
||||
import type { ToolId, ToolMeta, ConnectionConfig, Model } from '../types.ts'
|
||||
import { resolveModelLimits } from './modelCatalog.ts'
|
||||
|
||||
export const tools: Record<ToolId, ToolMeta> = {
|
||||
copilot: {
|
||||
@@ -20,7 +21,6 @@ export const tools: Record<ToolId, ToolMeta> = {
|
||||
name: 'Claude Sonnet 4.5',
|
||||
family: 'claude-sonnet-4-5',
|
||||
provider: 'Anthropic',
|
||||
contextWindow: 200000,
|
||||
vision: true,
|
||||
toolCalling: true,
|
||||
}
|
||||
@@ -46,20 +46,23 @@ export const tools: Record<ToolId, ToolMeta> = {
|
||||
apiKey: apiKeyVal,
|
||||
apiType: 'chat-completions',
|
||||
models: models.map((m) => {
|
||||
const limits = resolveModelLimits(m)
|
||||
const modelObj: Record<string, unknown> = {
|
||||
id: m.id,
|
||||
name: m.name || m.id,
|
||||
url: endpointUrl,
|
||||
toolCalling: m.toolCalling ?? true,
|
||||
vision: m.vision ?? true,
|
||||
maxInputTokens: m.contextWindow || 128000,
|
||||
// Guidance: maxInputTokens + maxOutputTokens <= context window
|
||||
maxOutputTokens: 16384,
|
||||
// Invariant: maxInputTokens + maxOutputTokens <= context window.
|
||||
// Resolved từ snapshot models.dev (src/data/modelCatalog.generated.ts),
|
||||
// rồi tới gateway, rồi hằng số.
|
||||
maxInputTokens: limits.maxInputTokens,
|
||||
maxOutputTokens: limits.maxOutputTokens,
|
||||
}
|
||||
|
||||
if (m.reasoning) {
|
||||
modelObj.thinking = true
|
||||
modelObj.supportsReasoningEffort = ['low', 'medium', 'high']
|
||||
modelObj.supportsReasoningEffort = limits.reasoningEfforts
|
||||
modelObj.reasoningEffortFormat = 'chat-completions'
|
||||
}
|
||||
|
||||
|
||||
@@ -117,6 +117,56 @@ describe('Install Link & Endpoint', () => {
|
||||
expect(res.body).toContain('while [[ "$CONFIG" == *"$API_KEY_PLACEHOLDER"* ]]')
|
||||
})
|
||||
|
||||
it('ensures parity by construction between preview template and install script', () => {
|
||||
const models = [
|
||||
{ id: 'cc/claude-sonnet-4.5', name: 'Claude Sonnet 4.5' },
|
||||
{ id: 'openai/o3-mini', name: 'o3-mini', reasoning: true },
|
||||
]
|
||||
const token = encodeInstallToken({
|
||||
v: 1,
|
||||
t: 'copilot',
|
||||
b: 'http://localhost:20128/v1',
|
||||
m: models,
|
||||
})
|
||||
|
||||
const conn = { baseUrl: 'http://localhost:20128/v1', apiKey: '__NINEROUTER_API_KEY__' }
|
||||
const expectedTemplate = tools.copilot.sampleTemplate(conn, models)
|
||||
|
||||
const shRes = renderInstallScript(token, 'sh')
|
||||
expect(shRes.status).toBe(200)
|
||||
expect(shRes.body).toContain(expectedTemplate)
|
||||
|
||||
const ps1Res = renderInstallScript(token, 'ps1')
|
||||
expect(ps1Res.status).toBe(200)
|
||||
expect(ps1Res.body).toContain(expectedTemplate)
|
||||
})
|
||||
|
||||
it('round-trips maxOutputTokens for uncataloged model through install token', () => {
|
||||
const models = [
|
||||
{
|
||||
id: 'custom-private/my-special-llm',
|
||||
name: 'My Special LLM',
|
||||
contextWindow: 65536,
|
||||
maxOutputTokens: 8192,
|
||||
},
|
||||
]
|
||||
const token = encodeInstallToken({
|
||||
v: 1,
|
||||
t: 'copilot',
|
||||
b: 'http://localhost:20128/v1',
|
||||
m: models,
|
||||
})
|
||||
|
||||
const decoded = decodeInstallToken(token)
|
||||
expect(decoded?.m[0].maxOutputTokens).toBe(8192)
|
||||
|
||||
const shRes = renderInstallScript(token, 'sh')
|
||||
expect(shRes.status).toBe(200)
|
||||
// Should resolve with maxOutputTokens 8192 and input 65536 - 8192 = 57344
|
||||
expect(shRes.body).toContain('"maxOutputTokens": 8192')
|
||||
expect(shRes.body).toContain('"maxInputTokens": 57344')
|
||||
})
|
||||
|
||||
it('handles invalid token gracefully', () => {
|
||||
const res = renderInstallScript('invalid-token', 'sh')
|
||||
expect(res.status).toBe(400)
|
||||
|
||||
@@ -46,6 +46,7 @@ export function renderInstallScript(
|
||||
name: m.name || m.id,
|
||||
provider: m.provider,
|
||||
contextWindow: m.contextWindow,
|
||||
maxOutputTokens: m.maxOutputTokens,
|
||||
vision: m.vision,
|
||||
toolCalling: m.toolCalling,
|
||||
reasoning: m.reasoning,
|
||||
|
||||
@@ -6,6 +6,7 @@ export interface MinimalModel {
|
||||
name?: string
|
||||
provider?: string
|
||||
contextWindow?: number
|
||||
maxOutputTokens?: number
|
||||
vision?: boolean
|
||||
toolCalling?: boolean
|
||||
reasoning?: boolean
|
||||
@@ -84,6 +85,7 @@ export function buildOneClickCommand(
|
||||
...(m.name ? { name: m.name } : {}),
|
||||
...(m.provider ? { provider: m.provider } : {}),
|
||||
...(m.contextWindow ? { contextWindow: m.contextWindow } : {}),
|
||||
...(m.maxOutputTokens ? { maxOutputTokens: m.maxOutputTokens } : {}),
|
||||
...(m.vision !== undefined ? { vision: m.vision } : {}),
|
||||
...(m.toolCalling !== undefined ? { toolCalling: m.toolCalling } : {}),
|
||||
...(m.reasoning !== undefined ? { reasoning: m.reasoning } : {}),
|
||||
|
||||
@@ -47,4 +47,52 @@ describe('fetchRemoteModels', () => {
|
||||
expect(models[0].id).toBe('cx/gpt-5.6-sol')
|
||||
expect(callCount).toBe(2)
|
||||
})
|
||||
|
||||
it('maps capabilities.maxOutput to Model.maxOutputTokens', async () => {
|
||||
const mockFetch = mock(() => {
|
||||
return Promise.resolve(new Response(JSON.stringify({
|
||||
data: [{
|
||||
id: 'custom/model-caps',
|
||||
owned_by: 'custom',
|
||||
capabilities: {
|
||||
contextWindow: 100000,
|
||||
maxOutput: 32768,
|
||||
},
|
||||
}],
|
||||
}), { status: 200 }))
|
||||
})
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
|
||||
const models = await fetchRemoteModels({
|
||||
baseUrl: 'https://gateway.example.com/v1',
|
||||
apiKey: 'sk-test-key',
|
||||
})
|
||||
|
||||
expect(models.length).toBe(1)
|
||||
expect(models[0].contextWindow).toBe(100000)
|
||||
expect(models[0].maxOutputTokens).toBe(32768)
|
||||
})
|
||||
|
||||
it('maps max_completion_tokens to Model.maxOutputTokens', async () => {
|
||||
const mockFetch = mock(() => {
|
||||
return Promise.resolve(new Response(JSON.stringify({
|
||||
data: [{
|
||||
id: 'custom/model-mct',
|
||||
owned_by: 'custom',
|
||||
context_length: 64000,
|
||||
max_completion_tokens: 16384,
|
||||
}],
|
||||
}), { status: 200 }))
|
||||
})
|
||||
globalThis.fetch = mockFetch as unknown as typeof fetch
|
||||
|
||||
const models = await fetchRemoteModels({
|
||||
baseUrl: 'https://gateway.example.com/v1',
|
||||
apiKey: 'sk-test-key',
|
||||
})
|
||||
|
||||
expect(models.length).toBe(1)
|
||||
expect(models[0].contextWindow).toBe(64000)
|
||||
expect(models[0].maxOutputTokens).toBe(16384)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -128,6 +128,12 @@ export async function fetchRemoteModels(connection: ConnectionConfig): Promise<M
|
||||
(item.contextWindow as number) ||
|
||||
undefined
|
||||
|
||||
const maxOutputTokens =
|
||||
caps.maxOutput ||
|
||||
item.max_completion_tokens ||
|
||||
(item.maxOutputTokens as number) ||
|
||||
undefined
|
||||
|
||||
const vision = caps.vision ?? (item.vision as boolean | undefined) ?? false
|
||||
const toolCalling = caps.tools ?? caps.toolCalling ?? (item.toolCalling as boolean | undefined) ?? false
|
||||
const reasoning = caps.reasoning ?? (item.reasoning as boolean | undefined) ?? false
|
||||
@@ -138,6 +144,7 @@ export async function fetchRemoteModels(connection: ConnectionConfig): Promise<M
|
||||
provider: parseProvider(item.id, item.owned_by),
|
||||
family: item.id.split('/').pop()?.replace(/[^a-zA-Z0-9_-]/g, '-'),
|
||||
contextWindow,
|
||||
maxOutputTokens,
|
||||
vision,
|
||||
toolCalling,
|
||||
reasoning,
|
||||
|
||||
@@ -4,6 +4,8 @@ export interface Model {
|
||||
id: string
|
||||
name: string
|
||||
contextWindow?: number
|
||||
/** Gateway-reported max completion tokens. Chỉ là tier-2 fallback; models.dev thắng. */
|
||||
maxOutputTokens?: number
|
||||
provider?: string
|
||||
description?: string
|
||||
family?: string
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" },
|
||||
{ "path": "./tsconfig.scripts.json" },
|
||||
{ "path": "./worker/tsconfig.json" }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.scripts.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"types": ["bun"],
|
||||
"skipLibCheck": true,
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["scripts"]
|
||||
}
|
||||
Reference in New Issue
Block a user