mirror of
https://github.com/Nezumi-2711/9router-config-generate.git
synced 2026-09-22 13:38:32 +00:00
fix: update logic for generate config
This commit is contained in:
@@ -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)
|
||||
})
|
||||
Reference in New Issue
Block a user