api9nin / scripts /migrate-registry.mjs
github-actions[bot]
deploy from github actions 2026-06-18
c8ae75d
Raw
History Blame Contribute Delete
9.3 kB
/**
* migrate-registry.mjs
* Migrates all registry files to Model-A schema:
* - models[] = ALL models (chat + media), field `kind` (default "llm")
* - media wrapper removed β†’ fields promoted top-level
* - *Config.models removed (data merged into models[])
* - format: terse, consistent indent
*
* Run: node --experimental-vm-modules migrate-registry.mjs [--dry]
*/
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; // script lives in registry/
const DRY = process.argv.includes("--dry");
// *Config.models field β†’ kind value
const CFG_KIND = {
ttsConfig: "tts",
sttConfig: "stt",
embeddingConfig: "embedding",
imageConfig: "image",
imageToTextConfig: "imageToText",
videoConfig: "video",
musicConfig: "music",
};
// Fields in *Config that are NOT models (keep on config)
const MODEL_ONLY_KEY = "models";
// Top-level registry fields that are NOT media-config (don't flatten these from media)
// serviceKinds + *Config + searchViaChat + mediaConfig + passthroughModels are media fields
// Everything else is already top-level
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 = {};
// 1. Top-level identity/transport fields (preserve order)
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];
}
// 2. Collect existing models[] (convert type→kind, skip if kind already set)
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));
// 3. Extract models from *Config.models (merge into models[])
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) {
// Check if same id+kind combo already exists to avoid true duplicates
const dup = existingModels.find(x => x.id === m.id && (x.kind ?? "llm") === kind);
if (dup) continue;
const { ...mClean } = m;
mediaModels.push({ ...mClean, kind });
}
}
// 4. Merge models (existing first, then media additions)
const allModels = [...existingModels, ...mediaModels];
// Only include models key if non-empty or explicitly defined
if (allModels.length > 0 || entry.models !== undefined) {
out.models = allModels;
}
// 5. Flatten media fields (without .models sub-arrays)
for (const [k, v] of Object.entries(media)) {
if (!MEDIA_WHITELIST.has(k)) continue;
if (CFG_KIND[k]) {
// Strip .models from config, keep rest
const { models: _m, ...cfgRest } = (v || {});
if (Object.keys(cfgRest).length > 0) out[k] = cfgRest;
} else {
out[k] = v;
}
}
// 6. Other top-level fields not in TRANSPORT_KEYS and not media (e.g. features, oauth, usage in transport)
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;
}
// Format a registry entry as clean JS (no JSON.stringify β€” write proper ES module)
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 "[]";
// Model arrays: 1 model per line (compact inline object)
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);
}
// Inline compact object: { id: "x", name: "y", kind: "tts", dimensions: 1536 }
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(", ")} }`;
}
// Config objects (ttsConfig etc) β€” inline single line if short, else multi-line
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 }`;
}
// Top-level registry entry formatter
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",
// media fields
"serviceKinds",
"ttsConfig", "sttConfig", "embeddingConfig",
"imageConfig", "imageToTextConfig", "videoConfig", "musicConfig",
"searchViaChat", "searchConfig", "fetchConfig", "modelsFetcher",
"passthroughModels", "mediaPriority",
// other
"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);
// Config objects (xConfig) β€” special inline format
if (CFG_KIND[k] || k === "searchViaChat" || k === "searchConfig" || k === "fetchConfig" || k === "modelsFetcher") {
lines.push(` ${key}: ${formatConfig(v)},`);
return;
}
// models[] β€” terse per-line
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;
}
// serviceKinds β€” inline array
if (k === "serviceKinds") {
lines.push(` serviceKinds: ${JSON.stringify(v)},`);
return;
}
// display β€” multi-line
if (k === "display") {
lines.push(` display: ${formatValue(v, 1)},`);
return;
}
// transport β€” multi-line
if (k === "transport") {
lines.push(` transport: ${formatValue(v, 1)},`);
return;
}
// Everything else
lines.push(` ${key}: ${formatValue(v, 1)},`);
}
for (const k of TOP_ORDER) emitKey(k);
// Emit any remaining keys not in TOP_ORDER
for (const k of Object.keys(entry)) emitKey(k);
lines.push("};");
return lines.join("\n") + "\n";
}
// --- Main ---
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");
// Extract import lines (for files that import shared constants)
const importLines = src.split("\n").filter(l => l.startsWith("import "));
const importSrc = importLines.join("\n");
// Dynamic import to get entry
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`);