| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| import { readFileSync, writeFileSync, readdirSync } from "node:fs"; |
| import { fileURLToPath } from "node:url"; |
| import { dirname, join } from "node:path"; |
| import { createRequire } from "node:module"; |
|
|
| const __dirname = dirname(fileURLToPath(import.meta.url)); |
| const REGISTRY_DIR = __dirname; |
| const DRY = process.argv.includes("--dry"); |
|
|
| |
| const CFG_KIND = { |
| ttsConfig: "tts", |
| sttConfig: "stt", |
| embeddingConfig: "embedding", |
| imageConfig: "image", |
| imageToTextConfig: "imageToText", |
| videoConfig: "video", |
| musicConfig: "music", |
| }; |
|
|
| |
| const MODEL_ONLY_KEY = "models"; |
|
|
| |
| |
| |
| const MEDIA_WHITELIST = new Set([ |
| "serviceKinds", |
| "ttsConfig", "sttConfig", "embeddingConfig", |
| "imageConfig", "imageToTextConfig", "videoConfig", "musicConfig", |
| "searchViaChat", "searchConfig", "fetchConfig", |
| "modelsFetcher", "hasProviderSpecificData", "passthroughModels", |
| "mediaPriority", "hiddenKinds", |
| ]); |
|
|
| function migrateEntry(entry, filename) { |
| const out = {}; |
|
|
| |
| const TRANSPORT_KEYS = ["id", "alias", "aliases", "uiAlias", "display", "category", |
| "authType", "authHint", "authModes", "hasOAuth", "noAuth", |
| "hasProviderSpecificData", "thinkingConfig", "hiddenKinds", |
| "regions", "defaultRegion", "passthroughModels", "transport"]; |
| for (const k of TRANSPORT_KEYS) { |
| if (entry[k] !== undefined) out[k] = entry[k]; |
| } |
|
|
| |
| const existingModels = (entry.models || []).map(m => { |
| const { type, ...rest } = m; |
| const kind = m.kind ?? (type && type !== "llm" ? type : undefined); |
| return kind ? { ...rest, kind } : rest; |
| }); |
| const existingIds = new Set(existingModels.map(m => m.id)); |
|
|
| |
| const mediaModels = []; |
| const media = entry.media || {}; |
| for (const [cfgKey, kind] of Object.entries(CFG_KIND)) { |
| const cfg = media[cfgKey]; |
| if (!cfg?.models) continue; |
| for (const m of cfg.models) { |
| |
| const dup = existingModels.find(x => x.id === m.id && (x.kind ?? "llm") === kind); |
| if (dup) continue; |
| const { ...mClean } = m; |
| mediaModels.push({ ...mClean, kind }); |
| } |
| } |
|
|
| |
| const allModels = [...existingModels, ...mediaModels]; |
| |
| if (allModels.length > 0 || entry.models !== undefined) { |
| out.models = allModels; |
| } |
|
|
| |
| for (const [k, v] of Object.entries(media)) { |
| if (!MEDIA_WHITELIST.has(k)) continue; |
| if (CFG_KIND[k]) { |
| |
| const { models: _m, ...cfgRest } = (v || {}); |
| if (Object.keys(cfgRest).length > 0) out[k] = cfgRest; |
| } else { |
| out[k] = v; |
| } |
| } |
|
|
| |
| const SKIP = new Set([...TRANSPORT_KEYS, "models", "media", ...Object.keys(CFG_KIND), |
| "serviceKinds", "searchViaChat", "searchConfig", "fetchConfig", |
| "modelsFetcher", "passthroughModels", "mediaPriority"]); |
| for (const [k, v] of Object.entries(entry)) { |
| if (!SKIP.has(k)) out[k] = v; |
| } |
|
|
| return out; |
| } |
|
|
| |
| function formatValue(v, indent = 0) { |
| const pad = " ".repeat(indent); |
| const pad1 = " ".repeat(indent + 1); |
|
|
| if (v === null || v === undefined) return String(v); |
| if (typeof v === "boolean" || typeof v === "number") return String(v); |
| if (typeof v === "string") return JSON.stringify(v); |
|
|
| if (Array.isArray(v)) { |
| if (v.length === 0) return "[]"; |
| |
| const items = v.map(item => { |
| if (typeof item === "object" && item !== null && !Array.isArray(item)) { |
| return `${pad1}${formatInlineObject(item)}`; |
| } |
| return `${pad1}${formatValue(item, indent + 1)}`; |
| }); |
| return `[\n${items.join(",\n")},\n${pad}]`; |
| } |
|
|
| if (typeof v === "object") { |
| const keys = Object.keys(v); |
| if (keys.length === 0) return "{}"; |
| const lines = keys.map(k => { |
| const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k); |
| return `${pad1}${key}: ${formatValue(v[k], indent + 1)}`; |
| }); |
| return `{\n${lines.join(",\n")},\n${pad}}`; |
| } |
|
|
| return JSON.stringify(v); |
| } |
|
|
| |
| function formatInlineObject(obj) { |
| const parts = Object.entries(obj).map(([k, v]) => { |
| const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k); |
| return `${key}: ${JSON.stringify(v)}`; |
| }); |
| return `{ ${parts.join(", ")} }`; |
| } |
|
|
| |
| function formatConfig(cfg) { |
| const line = `{ ${Object.entries(cfg).map(([k,v])=>`${k}: ${JSON.stringify(v)}`).join(", ")} }`; |
| if (line.length <= 120) return line; |
| const pad1 = " ".repeat(2); |
| const lines = Object.entries(cfg).map(([k,v]) => `${pad1}${k}: ${JSON.stringify(v)}`); |
| return `{\n${lines.join(",\n")},\n }`; |
| } |
|
|
| |
| function formatEntry(entry, imports = "") { |
| const lines = []; |
| if (imports) lines.push(imports, ""); |
| lines.push("export default {"); |
|
|
| const TOP_ORDER = [ |
| "id", "alias", "aliases", "uiAlias", "display", "category", |
| "authType", "authHint", "authModes", "hasOAuth", "noAuth", |
| "hasProviderSpecificData", "thinkingConfig", "hiddenKinds", |
| "regions", "defaultRegion", "transport", |
| "models", |
| |
| "serviceKinds", |
| "ttsConfig", "sttConfig", "embeddingConfig", |
| "imageConfig", "imageToTextConfig", "videoConfig", "musicConfig", |
| "searchViaChat", "searchConfig", "fetchConfig", "modelsFetcher", |
| "passthroughModels", "mediaPriority", |
| |
| "oauth", "features", |
| ]; |
|
|
| const emitted = new Set(); |
|
|
| function emitKey(k) { |
| if (!(k in entry) || emitted.has(k)) return; |
| emitted.add(k); |
| const v = entry[k]; |
| const key = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(k) ? k : JSON.stringify(k); |
|
|
| |
| if (CFG_KIND[k] || k === "searchViaChat" || k === "searchConfig" || k === "fetchConfig" || k === "modelsFetcher") { |
| lines.push(` ${key}: ${formatConfig(v)},`); |
| return; |
| } |
|
|
| |
| if (k === "models" && Array.isArray(v)) { |
| if (v.length === 0) { lines.push(` models: [],`); return; } |
| lines.push(` models: [`); |
| for (const m of v) lines.push(` ${formatInlineObject(m)},`); |
| lines.push(` ],`); |
| return; |
| } |
|
|
| |
| if (k === "serviceKinds") { |
| lines.push(` serviceKinds: ${JSON.stringify(v)},`); |
| return; |
| } |
|
|
| |
| if (k === "display") { |
| lines.push(` display: ${formatValue(v, 1)},`); |
| return; |
| } |
|
|
| |
| if (k === "transport") { |
| lines.push(` transport: ${formatValue(v, 1)},`); |
| return; |
| } |
|
|
| |
| lines.push(` ${key}: ${formatValue(v, 1)},`); |
| } |
|
|
| for (const k of TOP_ORDER) emitKey(k); |
| |
| for (const k of Object.keys(entry)) emitKey(k); |
|
|
| lines.push("};"); |
| return lines.join("\n") + "\n"; |
| } |
|
|
| |
| const files = readdirSync(REGISTRY_DIR).filter(f => f.endsWith(".js") && f !== "index.js"); |
| let count = 0; |
|
|
| for (const file of files) { |
| const path = join(REGISTRY_DIR, file); |
| const src = readFileSync(path, "utf8"); |
|
|
| |
| const importLines = src.split("\n").filter(l => l.startsWith("import ")); |
| const importSrc = importLines.join("\n"); |
|
|
| |
| let entry; |
| try { |
| const mod = await import(`${join(REGISTRY_DIR, file)}?t=${Date.now()}`); |
| entry = mod.default; |
| } catch (e) { |
| console.error(`SKIP ${file}: ${e.message}`); |
| continue; |
| } |
|
|
| const migrated = migrateEntry(entry, file); |
| const output = formatEntry(migrated, importSrc); |
|
|
| if (DRY) { |
| console.log(`\n=== ${file} ===\n${output}`); |
| } else { |
| writeFileSync(path, output, "utf8"); |
| count++; |
| } |
| } |
|
|
| console.log(DRY ? `[DRY] Would migrate ${files.length} files` : `β
Migrated ${count} files`); |
|
|