272 lines
9.1 KiB
JavaScript
272 lines
9.1 KiB
JavaScript
/**
|
|
* 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`);
|