fix: update logic for generate config

This commit is contained in:
2026-09-04 17:09:06 +07:00
parent dbea31f4a5
commit 7f3caa3fae
18 changed files with 1155 additions and 22 deletions
+3 -1
View File
@@ -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",
+203
View File
@@ -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)
})
+22 -6
View File
@@ -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">
+3 -3
View File
@@ -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>
-7
View File
@@ -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,
+371
View File
@@ -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},
}
+158
View File
@@ -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)
})
})
})
+153
View File
@@ -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,
}
}
+103
View File
@@ -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
View File
@@ -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'
}
+50
View File
@@ -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)
+1
View File
@@ -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,
+2
View File
@@ -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 } : {}),
+48
View File
@@ -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)
})
})
+7
View File
@@ -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,
+2
View File
@@ -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
+1
View File
@@ -3,6 +3,7 @@
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" },
{ "path": "./tsconfig.scripts.json" },
{ "path": "./worker/tsconfig.json" }
]
}
+20
View File
@@ -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"]
}