mirror of
https://git.openapi.site/https://github.com/desirecore/config-center.git
synced 2026-06-06 05:50:50 +08:00
chore: preserve field audit artifacts and tools (#5)
* 备份同步前的配置审计修改 * 更新字段审计产物
This commit is contained in:
238
tools/apply_all_updates_and_build_diff_report.mjs
Normal file
238
tools/apply_all_updates_and_build_diff_report.mjs
Normal file
@@ -0,0 +1,238 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = "/Users/xieyuanxiang/config-center";
|
||||
const ZENMUX_FILE = "/tmp/zenmux-models.json";
|
||||
const OUT_DIR = path.join(ROOT, "字段取值表");
|
||||
const DATE_TAG = "2026-04-23";
|
||||
|
||||
const TARGET_DIRS = [
|
||||
path.join(ROOT, "compute", "providers"),
|
||||
path.join(ROOT, "compute", "coding-plans"),
|
||||
];
|
||||
|
||||
const PROVIDER_OWNED_BY = {
|
||||
openai: ["openai"],
|
||||
anthropic: ["anthropic"],
|
||||
deepseek: ["deepseek"],
|
||||
google: ["google"],
|
||||
moonshot: ["moonshotai"],
|
||||
zhipu: ["z-ai"],
|
||||
"zhipu-embedding": ["z-ai"],
|
||||
dashscope: ["qwen"],
|
||||
minimax: ["minimax"],
|
||||
baidu: ["baidu"],
|
||||
tencent: ["tencent"],
|
||||
volcengine: ["bytedance", "volcengine"],
|
||||
xai: ["x-ai"],
|
||||
mistral: ["mistralai"],
|
||||
kwai: ["kuaishou"],
|
||||
lingyiwanwu: ["inclusionai"],
|
||||
siliconflow: ["qwen", "baai"],
|
||||
cohere: ["cohere"],
|
||||
perplexity: ["perplexity"],
|
||||
};
|
||||
|
||||
function normalize(s) {
|
||||
return String(s || "")
|
||||
.toLowerCase()
|
||||
.replace(/[._/]/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function stripTail(s) {
|
||||
return s
|
||||
.replace(/-(latest|preview|exp|experimental|stable)$/g, "")
|
||||
.replace(/-\d{8}$/g, "")
|
||||
.replace(/-\d{6}$/g, "")
|
||||
.replace(/-\d{4}-\d{2}-\d{2}$/g, "");
|
||||
}
|
||||
|
||||
function parseZen(m) {
|
||||
const [vendor, ...rest] = String(m.id).split("/");
|
||||
const model = rest.join("/");
|
||||
const norm = normalize(model);
|
||||
return {
|
||||
id: m.id,
|
||||
vendor,
|
||||
model,
|
||||
norm,
|
||||
stripped: stripTail(norm),
|
||||
context: m.context_length,
|
||||
prompt: m.pricings?.prompt?.[0],
|
||||
completion: m.pricings?.completion?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function jaccard(a, b) {
|
||||
const A = new Set(a.split("-").filter(Boolean));
|
||||
const B = new Set(b.split("-").filter(Boolean));
|
||||
let inter = 0;
|
||||
for (const x of A) if (B.has(x)) inter += 1;
|
||||
return inter / (new Set([...A, ...B]).size || 1);
|
||||
}
|
||||
|
||||
function match(provider, modelName, zenList) {
|
||||
const aliases = PROVIDER_OWNED_BY[provider] || [provider];
|
||||
const pool = zenList.filter((z) => aliases.includes(z.vendor));
|
||||
const localNorm = normalize(modelName);
|
||||
const localStripped = stripTail(localNorm);
|
||||
|
||||
const exact = pool.filter((z) => z.model === modelName);
|
||||
if (exact.length === 1) return { tier: "exact", z: exact[0] };
|
||||
|
||||
const norm = pool.filter((z) => z.norm === localNorm);
|
||||
if (norm.length === 1) return { tier: "normalized", z: norm[0] };
|
||||
|
||||
const stripped = pool.filter((z) => z.stripped === localStripped);
|
||||
if (stripped.length === 1) return { tier: "stripped", z: stripped[0] };
|
||||
|
||||
const ranked = pool
|
||||
.map((z) => ({ z, score: jaccard(localNorm, z.norm) }))
|
||||
.filter((x) => x.score >= 0.35)
|
||||
.sort((a, b) => b.score - a.score);
|
||||
if (ranked.length === 1) return { tier: "similar", z: ranked[0].z };
|
||||
return { tier: "none", z: null };
|
||||
}
|
||||
|
||||
function isUsdPerM(x) {
|
||||
return x && x.unit === "perMTokens" && x.currency === "USD" && typeof x.value === "number";
|
||||
}
|
||||
|
||||
function shouldUpdateContext(local, remote) {
|
||||
if (typeof remote !== "number") return false;
|
||||
if (typeof local !== "number") return true;
|
||||
if (local === remote) return false;
|
||||
const ratio = Math.abs(local - remote) / Math.max(local, remote);
|
||||
return ratio > 0.03;
|
||||
}
|
||||
|
||||
async function listFiles() {
|
||||
const out = [];
|
||||
for (const dir of TARGET_DIRS) {
|
||||
for (const f of await fs.readdir(dir)) {
|
||||
if (!f.endsWith(".json") || f === "_index.json") continue;
|
||||
out.push(path.join(dir, f));
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const raw = JSON.parse(await fs.readFile(ZENMUX_FILE, "utf8"));
|
||||
const zen = (raw.data || []).map(parseZen);
|
||||
const files = await listFiles();
|
||||
|
||||
const changes = [];
|
||||
|
||||
for (const abs of files) {
|
||||
const rel = path.relative(ROOT, abs);
|
||||
const doc = JSON.parse(await fs.readFile(abs, "utf8"));
|
||||
const provider = doc.provider || path.basename(abs, ".json");
|
||||
const currency = doc.priceCurrency || "USD";
|
||||
const models = Array.isArray(doc.models) ? doc.models : [];
|
||||
|
||||
let touched = false;
|
||||
|
||||
for (const m of models) {
|
||||
const mm = match(provider, m.modelName, zen);
|
||||
if (!mm.z) continue;
|
||||
|
||||
const modelChanges = [];
|
||||
const z = mm.z;
|
||||
|
||||
if (shouldUpdateContext(m.contextWindow, z.context)) {
|
||||
modelChanges.push({
|
||||
field: "contextWindow",
|
||||
from: m.contextWindow,
|
||||
to: z.context,
|
||||
});
|
||||
m.contextWindow = z.context;
|
||||
}
|
||||
|
||||
const allowPrice = currency === "USD" && ["exact", "normalized", "stripped", "similar"].includes(mm.tier);
|
||||
if (allowPrice && isUsdPerM(z.prompt) && m.inputPrice !== z.prompt.value) {
|
||||
modelChanges.push({
|
||||
field: "inputPrice",
|
||||
from: m.inputPrice,
|
||||
to: z.prompt.value,
|
||||
});
|
||||
m.inputPrice = z.prompt.value;
|
||||
}
|
||||
if (allowPrice && isUsdPerM(z.completion) && m.outputPrice !== z.completion.value) {
|
||||
modelChanges.push({
|
||||
field: "outputPrice",
|
||||
from: m.outputPrice,
|
||||
to: z.completion.value,
|
||||
});
|
||||
m.outputPrice = z.completion.value;
|
||||
}
|
||||
|
||||
if (modelChanges.length) {
|
||||
touched = true;
|
||||
changes.push({
|
||||
file: rel,
|
||||
provider,
|
||||
modelName: m.modelName,
|
||||
zenmux: z.id,
|
||||
matchTier: mm.tier,
|
||||
currency,
|
||||
changes: modelChanges,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (touched) {
|
||||
await fs.writeFile(abs, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
||||
}
|
||||
}
|
||||
|
||||
await fs.mkdir(OUT_DIR, { recursive: true });
|
||||
const jsonOut = path.join(OUT_DIR, `修改后差异总报告-${DATE_TAG}.json`);
|
||||
const mdOut = path.join(OUT_DIR, `修改后差异总报告-${DATE_TAG}.md`);
|
||||
|
||||
const summary = {
|
||||
generatedAt: new Date().toISOString(),
|
||||
totalChangedModels: changes.length,
|
||||
totalChangedFields: changes.reduce((n, c) => n + c.changes.length, 0),
|
||||
changedFiles: [...new Set(changes.map((c) => c.file))].length,
|
||||
};
|
||||
|
||||
await fs.writeFile(jsonOut, `${JSON.stringify({ summary, changes }, null, 2)}\n`, "utf8");
|
||||
|
||||
const lines = [];
|
||||
lines.push(`# 修改后差异总报告(${DATE_TAG})`);
|
||||
lines.push("");
|
||||
lines.push(`- generatedAt: \`${summary.generatedAt}\``);
|
||||
lines.push(`- changedFiles: **${summary.changedFiles}**`);
|
||||
lines.push(`- changedModels: **${summary.totalChangedModels}**`);
|
||||
lines.push(`- changedFields: **${summary.totalChangedFields}**`);
|
||||
lines.push("");
|
||||
lines.push("## 明细");
|
||||
lines.push("");
|
||||
if (!changes.length) {
|
||||
lines.push("- (none)");
|
||||
} else {
|
||||
for (const c of changes) {
|
||||
lines.push(`- ${c.file} :: ${c.modelName}`);
|
||||
lines.push(` - zenmux: ${c.zenmux} [${c.matchTier}], currency=${c.currency}`);
|
||||
for (const d of c.changes) {
|
||||
lines.push(` - ${d.field}: ${JSON.stringify(d.from)} -> ${JSON.stringify(d.to)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("## 数据来源");
|
||||
lines.push("");
|
||||
lines.push("- https://zenmux.ai/models");
|
||||
lines.push("- https://zenmux.ai/api/v1/models");
|
||||
lines.push("- 各 provider 官方模型/价格文档(见各子目录详细字段取值表)");
|
||||
lines.push("");
|
||||
|
||||
await fs.writeFile(mdOut, `${lines.join("\n")}\n`, "utf8");
|
||||
console.log(JSON.stringify({ summary, jsonOut, mdOut }, null, 2));
|
||||
}
|
||||
|
||||
await main();
|
||||
278
tools/batch_update_model_jsons.mjs
Normal file
278
tools/batch_update_model_jsons.mjs
Normal file
@@ -0,0 +1,278 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = "/Users/xieyuanxiang/config-center";
|
||||
const ZENMUX_FILE = "/tmp/zenmux-models.json";
|
||||
const REPORT_DIR = path.join(ROOT, "字段取值表");
|
||||
|
||||
const TARGET_DIRS = [
|
||||
path.join(ROOT, "compute", "providers"),
|
||||
path.join(ROOT, "compute", "coding-plans"),
|
||||
];
|
||||
|
||||
const PROVIDER_OWNED_BY = {
|
||||
openai: ["openai"],
|
||||
anthropic: ["anthropic"],
|
||||
deepseek: ["deepseek"],
|
||||
google: ["google"],
|
||||
moonshot: ["moonshotai"],
|
||||
zhipu: ["z-ai"],
|
||||
"zhipu-embedding": ["z-ai"],
|
||||
dashscope: ["qwen"],
|
||||
minimax: ["minimax"],
|
||||
baidu: ["baidu"],
|
||||
tencent: ["tencent"],
|
||||
volcengine: ["bytedance", "volcengine"],
|
||||
xai: ["x-ai"],
|
||||
mistral: ["mistralai"],
|
||||
kwai: ["kuaishou"],
|
||||
lingyiwanwu: ["inclusionai"],
|
||||
siliconflow: ["qwen", "baai"],
|
||||
cohere: ["cohere"],
|
||||
perplexity: ["perplexity"],
|
||||
};
|
||||
|
||||
const OFFICIAL_DOCS = {
|
||||
openai: ["https://platform.openai.com/docs/models", "https://platform.openai.com/docs/pricing"],
|
||||
anthropic: ["https://docs.anthropic.com/en/docs/about-claude/models/all-models", "https://docs.anthropic.com/en/docs/about-claude/pricing"],
|
||||
deepseek: ["https://api-docs.deepseek.com/quick_start/pricing"],
|
||||
google: ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/pricing"],
|
||||
moonshot: ["https://platform.moonshot.cn/docs/pricing/chat"],
|
||||
zhipu: ["https://docs.bigmodel.cn/cn/guide/models/text/", "https://www.bigmodel.cn/pricing"],
|
||||
dashscope: ["https://help.aliyun.com/zh/model-studio/getting-started/models", "https://help.aliyun.com/zh/model-studio/pricing"],
|
||||
minimax: ["https://platform.minimax.io/docs/guides/pricing-paygo"],
|
||||
baidu: ["https://cloud.baidu.com/doc/qianfan/"],
|
||||
tencent: ["https://cloud.tencent.com/document/product/1729"],
|
||||
volcengine: ["https://www.volcengine.com/docs/82379"],
|
||||
xai: ["https://docs.x.ai/docs/models"],
|
||||
mistral: ["https://docs.mistral.ai/getting-started/models", "https://mistral.ai/pricing"],
|
||||
cohere: ["https://docs.cohere.com/docs/models", "https://cohere.com/pricing"],
|
||||
siliconflow: ["https://www.siliconflow.com/models", "https://siliconflow.cn/pricing"],
|
||||
perplexity: ["https://docs.perplexity.ai"],
|
||||
};
|
||||
|
||||
function normalize(s) {
|
||||
return String(s || "")
|
||||
.toLowerCase()
|
||||
.replace(/[._/]/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function stripTail(s) {
|
||||
return s
|
||||
.replace(/-(latest|preview|exp|experimental|stable)$/g, "")
|
||||
.replace(/-\d{8}$/g, "")
|
||||
.replace(/-\d{6}$/g, "")
|
||||
.replace(/-\d{4}-\d{2}-\d{2}$/g, "");
|
||||
}
|
||||
|
||||
function parseZenModel(m) {
|
||||
const [vendor, ...rest] = String(m.id).split("/");
|
||||
const model = rest.join("/");
|
||||
const norm = normalize(model);
|
||||
return {
|
||||
id: m.id,
|
||||
vendor,
|
||||
model,
|
||||
norm,
|
||||
stripped: stripTail(norm),
|
||||
context: m.context_length,
|
||||
reasoning: m.capabilities?.reasoning,
|
||||
prompt: m.pricings?.prompt?.[0],
|
||||
completion: m.pricings?.completion?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function matchModel(provider, modelName, zenModels) {
|
||||
const aliases = PROVIDER_OWNED_BY[provider] || [provider];
|
||||
const pool = zenModels.filter((m) => aliases.includes(m.vendor));
|
||||
|
||||
const localNorm = normalize(modelName);
|
||||
const localStripped = stripTail(localNorm);
|
||||
|
||||
const exact = pool.filter((m) => m.model === modelName);
|
||||
if (exact.length === 1) return { tier: "exact", model: exact[0] };
|
||||
|
||||
const norm = pool.filter((m) => m.norm === localNorm);
|
||||
if (norm.length === 1) return { tier: "normalized", model: norm[0] };
|
||||
|
||||
const stripped = pool.filter((m) => m.stripped === localStripped);
|
||||
if (stripped.length === 1) return { tier: "stripped", model: stripped[0] };
|
||||
|
||||
const ranked = pool
|
||||
.map((m) => {
|
||||
const tokensA = new Set(localNorm.split("-").filter(Boolean));
|
||||
const tokensB = new Set(m.norm.split("-").filter(Boolean));
|
||||
let inter = 0;
|
||||
for (const t of tokensA) if (tokensB.has(t)) inter += 1;
|
||||
const union = new Set([...tokensA, ...tokensB]).size || 1;
|
||||
return { m, score: inter / union };
|
||||
})
|
||||
.filter((x) => x.score >= 0.35)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 3);
|
||||
|
||||
if (ranked.length === 1) return { tier: "similar", model: ranked[0].m };
|
||||
if (ranked.length > 1) return { tier: "ambiguous", candidates: ranked.map((x) => x.m.id) };
|
||||
return { tier: "none" };
|
||||
}
|
||||
|
||||
function isPriceUsable(entry) {
|
||||
return entry && entry.unit === "perMTokens" && entry.currency === "USD" && typeof entry.value === "number";
|
||||
}
|
||||
|
||||
function contextShouldUpdate(local, remote) {
|
||||
if (typeof remote !== "number") return false;
|
||||
if (typeof local !== "number") return true;
|
||||
if (local === remote) return false;
|
||||
const ratio = Math.abs(local - remote) / Math.max(local, remote);
|
||||
return ratio > 0.03;
|
||||
}
|
||||
|
||||
async function listJsonFiles() {
|
||||
const files = [];
|
||||
for (const dir of TARGET_DIRS) {
|
||||
for (const f of await fs.readdir(dir)) {
|
||||
if (!f.endsWith(".json") || f === "_index.json") continue;
|
||||
files.push(path.join(dir, f));
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
function reportNameFromPath(rel) {
|
||||
return rel.replaceAll("/", "__").replaceAll(".json", "");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const raw = JSON.parse(await fs.readFile(ZENMUX_FILE, "utf8"));
|
||||
const zenModels = (raw.data || []).map(parseZenModel);
|
||||
const files = await listJsonFiles();
|
||||
await fs.mkdir(REPORT_DIR, { recursive: true });
|
||||
|
||||
const summary = [];
|
||||
|
||||
for (const abs of files) {
|
||||
const rel = path.relative(ROOT, abs);
|
||||
const doc = JSON.parse(await fs.readFile(abs, "utf8"));
|
||||
const provider = doc.provider || path.basename(abs, ".json");
|
||||
const priceCurrency = doc.priceCurrency || "USD";
|
||||
const models = Array.isArray(doc.models) ? doc.models : [];
|
||||
|
||||
const changed = [];
|
||||
const unresolved = [];
|
||||
let fileChanged = false;
|
||||
|
||||
for (const model of models) {
|
||||
const mm = matchModel(provider, model.modelName, zenModels);
|
||||
if (!mm.model) {
|
||||
unresolved.push({
|
||||
modelName: model.modelName,
|
||||
reason: mm.tier === "ambiguous" ? `ambiguous:${(mm.candidates || []).join(",")}` : "no-zenmux-match",
|
||||
});
|
||||
continue;
|
||||
}
|
||||
const z = mm.model;
|
||||
const fieldsChanged = [];
|
||||
|
||||
if (contextShouldUpdate(model.contextWindow, z.context)) {
|
||||
const before = model.contextWindow;
|
||||
model.contextWindow = z.context;
|
||||
fieldsChanged.push(`contextWindow:${before}->${z.context}`);
|
||||
}
|
||||
|
||||
const allowPrice = priceCurrency === "USD" && (mm.tier === "exact" || mm.tier === "normalized");
|
||||
if (allowPrice && isPriceUsable(z.prompt) && model.inputPrice !== z.prompt.value) {
|
||||
const before = model.inputPrice;
|
||||
model.inputPrice = z.prompt.value;
|
||||
fieldsChanged.push(`inputPrice:${before}->${z.prompt.value}`);
|
||||
} else if (!allowPrice && model.inputPrice !== undefined) {
|
||||
unresolved.push({
|
||||
modelName: model.modelName,
|
||||
reason: `price-not-updated(currency=${priceCurrency},match=${mm.tier})`,
|
||||
});
|
||||
}
|
||||
|
||||
if (allowPrice && isPriceUsable(z.completion) && model.outputPrice !== z.completion.value) {
|
||||
const before = model.outputPrice;
|
||||
model.outputPrice = z.completion.value;
|
||||
fieldsChanged.push(`outputPrice:${before}->${z.completion.value}`);
|
||||
}
|
||||
|
||||
if (fieldsChanged.length) {
|
||||
fileChanged = true;
|
||||
changed.push({
|
||||
modelName: model.modelName,
|
||||
zenmux: z.id,
|
||||
matchTier: mm.tier,
|
||||
fieldsChanged,
|
||||
});
|
||||
} else {
|
||||
unresolved.push({
|
||||
modelName: model.modelName,
|
||||
reason: `matched-no-change(${z.id},${mm.tier})`,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (fileChanged) {
|
||||
await fs.writeFile(abs, `${JSON.stringify(doc, null, 2)}\n`, "utf8");
|
||||
}
|
||||
|
||||
const reportFile = path.join(REPORT_DIR, `${reportNameFromPath(rel)}-未确认字段报告.md`);
|
||||
const lines = [];
|
||||
lines.push(`# 未确认字段报告 - ${rel}`);
|
||||
lines.push("");
|
||||
lines.push(`- provider: \`${provider}\``);
|
||||
lines.push(`- priceCurrency: \`${priceCurrency}\``);
|
||||
lines.push(`- changedModels: **${changed.length}**`);
|
||||
lines.push(`- unresolvedItems: **${unresolved.length}**`);
|
||||
lines.push("");
|
||||
lines.push("## 官方来源");
|
||||
lines.push("");
|
||||
const docs = OFFICIAL_DOCS[provider] || [];
|
||||
if (docs.length) {
|
||||
for (const d of docs) lines.push(`- ${d}`);
|
||||
} else {
|
||||
lines.push("- (待补充该 provider 官方文档)");
|
||||
}
|
||||
lines.push("- https://zenmux.ai/models");
|
||||
lines.push("- https://zenmux.ai/api/v1/models");
|
||||
lines.push("");
|
||||
lines.push("## 已修改");
|
||||
lines.push("");
|
||||
if (!changed.length) {
|
||||
lines.push("- (none)");
|
||||
} else {
|
||||
for (const c of changed) {
|
||||
lines.push(`- \`${c.modelName}\` <= \`${c.zenmux}\` [${c.matchTier}]`);
|
||||
for (const f of c.fieldsChanged) lines.push(` - ${f}`);
|
||||
}
|
||||
}
|
||||
lines.push("");
|
||||
lines.push("## 未确认/未修改");
|
||||
lines.push("");
|
||||
if (!unresolved.length) {
|
||||
lines.push("- (none)");
|
||||
} else {
|
||||
for (const u of unresolved) lines.push(`- \`${u.modelName}\`: ${u.reason}`);
|
||||
}
|
||||
lines.push("");
|
||||
await fs.writeFile(reportFile, `${lines.join("\n")}\n`, "utf8");
|
||||
|
||||
summary.push({
|
||||
file: rel,
|
||||
changedModels: changed.length,
|
||||
unresolvedItems: unresolved.length,
|
||||
report: path.relative(ROOT, reportFile),
|
||||
});
|
||||
}
|
||||
|
||||
const summaryFile = path.join(REPORT_DIR, "全量处理汇总-2026-04-23.json");
|
||||
await fs.writeFile(summaryFile, `${JSON.stringify(summary, null, 2)}\n`, "utf8");
|
||||
console.log(JSON.stringify({ processedFiles: summary.length, summaryFile }, null, 2));
|
||||
}
|
||||
|
||||
await main();
|
||||
532
tools/generate_current_audit_artifacts.mjs
Normal file
532
tools/generate_current_audit_artifacts.mjs
Normal file
@@ -0,0 +1,532 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = process.env.CONFIG_CENTER_ROOT || process.cwd();
|
||||
const ZENMUX_FILE = process.env.ZENMUX_FILE || "/tmp/zenmux-models.json";
|
||||
const FIELD_TABLE_DIR = path.join(ROOT, "字段取值表");
|
||||
const OUTPUT_DIR = path.join(ROOT, "outputs", "field-audit");
|
||||
|
||||
const TARGET_DIRS = [
|
||||
path.join(ROOT, "compute", "providers"),
|
||||
path.join(ROOT, "compute", "coding-plans"),
|
||||
];
|
||||
|
||||
const PROVIDER_OWNED_BY = {
|
||||
openai: ["openai"],
|
||||
anthropic: ["anthropic"],
|
||||
deepseek: ["deepseek"],
|
||||
google: ["google"],
|
||||
moonshot: ["moonshotai"],
|
||||
zhipu: ["z-ai"],
|
||||
"zhipu-embedding": ["z-ai"],
|
||||
dashscope: ["qwen"],
|
||||
minimax: ["minimax"],
|
||||
baidu: ["baidu"],
|
||||
tencent: ["tencent"],
|
||||
volcengine: ["bytedance", "volcengine"],
|
||||
xai: ["x-ai"],
|
||||
mistral: ["mistralai"],
|
||||
kwai: ["kuaishou"],
|
||||
lingyiwanwu: ["inclusionai"],
|
||||
siliconflow: ["qwen", "baai"],
|
||||
cohere: ["cohere"],
|
||||
perplexity: ["perplexity"],
|
||||
};
|
||||
|
||||
const OFFICIAL_DOCS = {
|
||||
openai: ["https://platform.openai.com/docs/models", "https://platform.openai.com/docs/pricing"],
|
||||
anthropic: ["https://docs.anthropic.com/en/docs/about-claude/models/all-models", "https://docs.anthropic.com/en/docs/about-claude/pricing"],
|
||||
deepseek: ["https://api-docs.deepseek.com/quick_start/pricing"],
|
||||
google: ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/pricing"],
|
||||
moonshot: ["https://platform.moonshot.cn/docs/guide/overview", "https://platform.moonshot.cn/docs/pricing/chat"],
|
||||
zhipu: ["https://docs.bigmodel.cn/cn/guide/models/text/", "https://www.bigmodel.cn/pricing"],
|
||||
"zhipu-embedding": ["https://docs.bigmodel.cn/cn/guide/models/embedding"],
|
||||
dashscope: ["https://help.aliyun.com/zh/model-studio/getting-started/models", "https://help.aliyun.com/zh/model-studio/pricing"],
|
||||
minimax: ["https://platform.minimax.io/docs/api-reference/api-overview", "https://platform.minimax.io/docs/guides/pricing-paygo"],
|
||||
baichuan: ["https://platform.baichuan-ai.com/docs"],
|
||||
baidu: ["https://cloud.baidu.com/doc/qianfan/"],
|
||||
tencent: ["https://cloud.tencent.com/document/product/1729"],
|
||||
volcengine: ["https://www.volcengine.com/docs/82379"],
|
||||
xai: ["https://docs.x.ai/docs/models"],
|
||||
mistral: ["https://docs.mistral.ai/getting-started/models", "https://mistral.ai/pricing"],
|
||||
cohere: ["https://docs.cohere.com/docs/models", "https://cohere.com/pricing"],
|
||||
siliconflow: ["https://www.siliconflow.com/models", "https://siliconflow.cn/pricing"],
|
||||
perplexity: ["https://docs.perplexity.ai"],
|
||||
openrouter: ["https://openrouter.ai/models"],
|
||||
xunfei: ["https://www.xfyun.cn/doc/"],
|
||||
};
|
||||
|
||||
function normalize(value) {
|
||||
return String(value || "")
|
||||
.toLowerCase()
|
||||
.replace(/[._/]/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function stripTail(value) {
|
||||
return value
|
||||
.replace(/-(latest|preview|exp|experimental|stable)$/g, "")
|
||||
.replace(/-\d{8}$/g, "")
|
||||
.replace(/-\d{6}$/g, "")
|
||||
.replace(/-\d{4}-\d{2}-\d{2}$/g, "");
|
||||
}
|
||||
|
||||
function parseZenMuxModel(model) {
|
||||
const [vendor, ...rest] = String(model.id || "").split("/");
|
||||
const name = rest.join("/");
|
||||
const norm = normalize(name);
|
||||
return {
|
||||
id: model.id,
|
||||
vendor,
|
||||
modelName: name,
|
||||
norm,
|
||||
stripped: stripTail(norm),
|
||||
contextWindow: model.context_length,
|
||||
reasoning: model.capabilities?.reasoning,
|
||||
prompt: model.pricings?.prompt?.[0],
|
||||
completion: model.pricings?.completion?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function jaccard(left, right) {
|
||||
const a = new Set(left.split("-").filter(Boolean));
|
||||
const b = new Set(right.split("-").filter(Boolean));
|
||||
let overlap = 0;
|
||||
for (const item of a) if (b.has(item)) overlap += 1;
|
||||
return overlap / (new Set([...a, ...b]).size || 1);
|
||||
}
|
||||
|
||||
function matchModel(provider, modelName, zenModels) {
|
||||
const owners = PROVIDER_OWNED_BY[provider] || [provider];
|
||||
const pool = zenModels.filter((model) => owners.includes(model.vendor));
|
||||
const localNorm = normalize(modelName);
|
||||
const localStripped = stripTail(localNorm);
|
||||
|
||||
const exact = pool.filter((model) => model.modelName === modelName);
|
||||
if (exact.length === 1) return { tier: "exact", matched: exact[0], candidates: exact };
|
||||
|
||||
const normalized = pool.filter((model) => model.norm === localNorm);
|
||||
if (normalized.length === 1) return { tier: "normalized", matched: normalized[0], candidates: normalized };
|
||||
|
||||
const stripped = pool.filter((model) => model.stripped === localStripped);
|
||||
if (stripped.length === 1) return { tier: "stripped", matched: stripped[0], candidates: stripped };
|
||||
|
||||
const candidates = pool
|
||||
.map((model) => ({ model, score: jaccard(localNorm, model.norm) }))
|
||||
.filter((item) => item.score >= 0.35)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((item) => item.model);
|
||||
|
||||
if (candidates.length === 1) return { tier: "similar", matched: candidates[0], candidates };
|
||||
return { tier: candidates.length ? "ambiguous" : "none", matched: null, candidates };
|
||||
}
|
||||
|
||||
function formatValue(value) {
|
||||
if (value === undefined) return "(缺省)";
|
||||
return `\`${JSON.stringify(value)}\``;
|
||||
}
|
||||
|
||||
function formatList(values) {
|
||||
if (!values?.length) return "(none)";
|
||||
return values.map((value) => `\`${value}\``).join("、");
|
||||
}
|
||||
|
||||
function reportFolderName(relativePath) {
|
||||
return relativePath
|
||||
.replace(/^compute\//, "")
|
||||
.replaceAll("/", "__")
|
||||
.replace(/\.json$/, "");
|
||||
}
|
||||
|
||||
async function listJsonFiles() {
|
||||
const files = [];
|
||||
for (const dir of TARGET_DIRS) {
|
||||
for (const fileName of await fs.readdir(dir)) {
|
||||
if (!fileName.endsWith(".json") || fileName.startsWith("_")) continue;
|
||||
files.push(path.join(dir, fileName));
|
||||
}
|
||||
}
|
||||
return files.sort();
|
||||
}
|
||||
|
||||
async function readZenMuxModels() {
|
||||
try {
|
||||
const raw = JSON.parse(await fs.readFile(ZENMUX_FILE, "utf8"));
|
||||
return (raw.data || []).map(parseZenMuxModel);
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function buildFieldRows(doc, model, match) {
|
||||
const z = match.matched;
|
||||
return [
|
||||
{
|
||||
field: "modelName",
|
||||
current: model.modelName,
|
||||
suggested: z ? z.modelName : model.modelName,
|
||||
decision: z && ["exact", "normalized"].includes(match.tier) ? "保持" : "待确认",
|
||||
reason: z ? `ZenMux匹配(${match.tier}): ${z.id}` : "ZenMux无稳定匹配,需官方文档确认",
|
||||
},
|
||||
{
|
||||
field: "displayName",
|
||||
current: model.displayName,
|
||||
suggested: model.displayName,
|
||||
decision: "保持",
|
||||
reason: "展示字段,以本项目产品命名策略为准",
|
||||
},
|
||||
{
|
||||
field: "serviceType",
|
||||
current: model.serviceType,
|
||||
suggested: model.serviceType,
|
||||
decision: "保持",
|
||||
reason: "服务路由字段,以本项目分类约定为准",
|
||||
},
|
||||
{
|
||||
field: "contextWindow",
|
||||
current: model.contextWindow,
|
||||
suggested: z?.contextWindow ?? model.contextWindow,
|
||||
decision: typeof z?.contextWindow === "number" ? "待确认" : "待确认",
|
||||
reason: typeof z?.contextWindow === "number"
|
||||
? `ZenMux给出context_length=${z.contextWindow},最终以官方模型规格页确认`
|
||||
: "ZenMux无context可用,需官方规格页确认",
|
||||
},
|
||||
{
|
||||
field: "maxOutputTokens",
|
||||
current: model.maxOutputTokens,
|
||||
suggested: model.maxOutputTokens,
|
||||
decision: "待确认",
|
||||
reason: "ZenMux列表未提供统一max output字段,需官方模型详情页确认",
|
||||
},
|
||||
{
|
||||
field: "inputPrice",
|
||||
current: model.inputPrice,
|
||||
suggested: model.inputPrice,
|
||||
decision: "待确认",
|
||||
reason: `当前文件币种为${doc.priceCurrency || "USD"},价格需官方价格页确认`,
|
||||
},
|
||||
{
|
||||
field: "outputPrice",
|
||||
current: model.outputPrice,
|
||||
suggested: model.outputPrice,
|
||||
decision: "待确认",
|
||||
reason: `当前文件币种为${doc.priceCurrency || "USD"},价格需官方价格页确认`,
|
||||
},
|
||||
{
|
||||
field: "capabilities",
|
||||
current: model.capabilities,
|
||||
suggested: model.capabilities,
|
||||
decision: "保持",
|
||||
reason: "能力标签是本项目语义字段,不直接由第三方列表覆盖",
|
||||
},
|
||||
{
|
||||
field: "defaultTemperature",
|
||||
current: model.defaultTemperature,
|
||||
suggested: model.defaultTemperature,
|
||||
decision: model.defaultTemperature === null ? "建议修改" : "保持",
|
||||
reason: model.defaultTemperature === null
|
||||
? "schema拒绝null;不支持采样参数时应省略字段"
|
||||
: "当前值与schema兼容;是否采用默认采样值需官方文档确认",
|
||||
},
|
||||
{
|
||||
field: "defaultTopP",
|
||||
current: model.defaultTopP,
|
||||
suggested: model.defaultTopP,
|
||||
decision: model.defaultTopP === null ? "建议修改" : "保持",
|
||||
reason: model.defaultTopP === null
|
||||
? "schema拒绝null;不支持采样参数时应省略字段"
|
||||
: "当前值与schema兼容;是否采用默认采样值需官方文档确认",
|
||||
},
|
||||
{
|
||||
field: "extra",
|
||||
current: model.extra,
|
||||
suggested: model.extra,
|
||||
decision: "待确认",
|
||||
reason: "扩展字段为本项目schema,需按业务含义和官方补充信息复核",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const generatedAt = new Date().toISOString();
|
||||
const zenModels = await readZenMuxModels();
|
||||
const files = await listJsonFiles();
|
||||
await fs.mkdir(FIELD_TABLE_DIR, { recursive: true });
|
||||
await fs.mkdir(OUTPUT_DIR, { recursive: true });
|
||||
|
||||
const allModels = [];
|
||||
const skipped = [];
|
||||
const pendingByProvider = new Map();
|
||||
const fieldIndex = [];
|
||||
const nullSampling = [];
|
||||
|
||||
for (const absolutePath of files) {
|
||||
const relativePath = path.relative(ROOT, absolutePath);
|
||||
const doc = JSON.parse(await fs.readFile(absolutePath, "utf8"));
|
||||
const provider = doc.provider || path.basename(absolutePath, ".json");
|
||||
const outDir = path.join(FIELD_TABLE_DIR, reportFolderName(relativePath));
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
|
||||
const detailed = [
|
||||
`# 详细字段取值表 - ${relativePath}`,
|
||||
"",
|
||||
`- provider: \`${provider}\``,
|
||||
`- priceCurrency: \`${doc.priceCurrency || "USD"}\``,
|
||||
`- generatedAt: \`${generatedAt}\``,
|
||||
"",
|
||||
"## 来源",
|
||||
"",
|
||||
...(OFFICIAL_DOCS[provider] || ["(待补充provider官方文档)"]).map((item) => `- ${item}`),
|
||||
"- https://zenmux.ai/models",
|
||||
"- https://zenmux.ai/api/v1/models",
|
||||
"",
|
||||
];
|
||||
const unresolved = [
|
||||
`# 未确认字段报告 - ${relativePath}`,
|
||||
"",
|
||||
`- provider: \`${provider}\``,
|
||||
`- generatedAt: \`${generatedAt}\``,
|
||||
"",
|
||||
];
|
||||
|
||||
const models = Array.isArray(doc.models) ? doc.models : [];
|
||||
for (const model of models) {
|
||||
const match = matchModel(provider, model.modelName, zenModels);
|
||||
const rows = buildFieldRows(doc, model, match);
|
||||
const status = match.matched
|
||||
? `matched-${match.tier}`
|
||||
: match.tier === "ambiguous" ? "ambiguous" : "no-match";
|
||||
|
||||
allModels.push({
|
||||
file: relativePath,
|
||||
provider,
|
||||
modelName: model.modelName,
|
||||
displayName: model.displayName,
|
||||
serviceType: model.serviceType,
|
||||
capabilities: model.capabilities,
|
||||
contextWindow: model.contextWindow,
|
||||
maxOutputTokens: model.maxOutputTokens,
|
||||
inputPrice: model.inputPrice,
|
||||
outputPrice: model.outputPrice,
|
||||
defaultTemperature: model.defaultTemperature,
|
||||
defaultTopP: model.defaultTopP,
|
||||
matchTier: match.tier,
|
||||
zenmux: match.matched?.id ?? null,
|
||||
});
|
||||
|
||||
if (model.defaultTemperature === null) nullSampling.push(`${relativePath}::${model.modelName}::defaultTemperature`);
|
||||
if (model.defaultTopP === null) nullSampling.push(`${relativePath}::${model.modelName}::defaultTopP`);
|
||||
|
||||
skipped.push({
|
||||
file: relativePath,
|
||||
provider,
|
||||
modelName: model.modelName,
|
||||
reason: status,
|
||||
zenmux: match.matched?.id ?? null,
|
||||
candidates: match.candidates.map((item) => item.id),
|
||||
});
|
||||
|
||||
if (!match.matched || match.tier === "ambiguous") {
|
||||
const bucket = pendingByProvider.get(provider) || [];
|
||||
bucket.push({
|
||||
file: relativePath,
|
||||
modelName: model.modelName,
|
||||
reason: match.tier === "ambiguous"
|
||||
? `ambiguous match candidates: ${match.candidates.map((item) => item.id).join(",")}`
|
||||
: "no ZenMux match",
|
||||
});
|
||||
pendingByProvider.set(provider, bucket);
|
||||
}
|
||||
|
||||
detailed.push(`## ${model.modelName}`);
|
||||
detailed.push("");
|
||||
detailed.push(`- ZenMux匹配级别: \`${match.tier}\``);
|
||||
if (match.matched) detailed.push(`- ZenMux命中: \`${match.matched.id}\``);
|
||||
detailed.push(`- ZenMux候选: ${formatList(match.candidates.map((item) => item.id))}`);
|
||||
detailed.push("");
|
||||
detailed.push("| 字段 | 当前值 | 建议值 | 结论 | 依据/说明 |");
|
||||
detailed.push("|---|---|---|---|---|");
|
||||
for (const row of rows) {
|
||||
detailed.push(`| \`${row.field}\` | ${formatValue(row.current)} | ${formatValue(row.suggested)} | ${row.decision} | ${row.reason} |`);
|
||||
}
|
||||
detailed.push("");
|
||||
|
||||
const pendingRows = rows.filter((row) => row.decision === "待确认" || row.decision === "建议修改");
|
||||
if (pendingRows.length) {
|
||||
unresolved.push(`## ${model.modelName}`);
|
||||
unresolved.push("");
|
||||
for (const row of pendingRows) unresolved.push(`- \`${row.field}\`: ${row.reason}`);
|
||||
unresolved.push("");
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length === 4) {
|
||||
unresolved.push("- (none)");
|
||||
unresolved.push("");
|
||||
}
|
||||
|
||||
const detailedFile = path.join(outDir, "详细字段取值表.md");
|
||||
const unresolvedFile = path.join(outDir, "未确认字段报告.md");
|
||||
await fs.writeFile(detailedFile, `${detailed.join("\n")}\n`, "utf8");
|
||||
await fs.writeFile(unresolvedFile, `${unresolved.join("\n")}\n`, "utf8");
|
||||
fieldIndex.push({
|
||||
file: relativePath,
|
||||
provider,
|
||||
modelCount: models.length,
|
||||
folder: path.relative(ROOT, outDir),
|
||||
detailed: path.relative(ROOT, detailedFile),
|
||||
unresolved: path.relative(ROOT, unresolvedFile),
|
||||
});
|
||||
}
|
||||
|
||||
const matchedModels = allModels.filter((item) => item.zenmux).length;
|
||||
const summary = {
|
||||
actualModels: allModels.length,
|
||||
sourceModels: zenModels.length,
|
||||
matchedModels,
|
||||
unmatchedModels: allModels.filter((item) => item.matchTier === "none").length,
|
||||
ambiguousModels: allModels.filter((item) => item.matchTier === "ambiguous").length,
|
||||
nullSamplingFields: nullSampling.length,
|
||||
updatedModels: 0,
|
||||
updatedFiles: 0,
|
||||
skippedModels: allModels.length,
|
||||
};
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(FIELD_TABLE_DIR, "目录索引.json"),
|
||||
`${JSON.stringify(fieldIndex, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const reportJson = {
|
||||
source: ZENMUX_FILE,
|
||||
generatedAt,
|
||||
summary,
|
||||
updated: [],
|
||||
skipped,
|
||||
models: allModels,
|
||||
nullSampling,
|
||||
};
|
||||
await fs.writeFile(
|
||||
path.join(OUTPUT_DIR, "zenmux-sync-report.json"),
|
||||
`${JSON.stringify(reportJson, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const reportLines = [
|
||||
"# ZenMux Sync Report",
|
||||
"",
|
||||
`- source: \`${ZENMUX_FILE}\``,
|
||||
`- generatedAt: \`${generatedAt}\``,
|
||||
`- actualModels: **${summary.actualModels}**`,
|
||||
`- sourceModels: **${summary.sourceModels}**`,
|
||||
`- matchedModels: **${summary.matchedModels}**`,
|
||||
`- unmatchedModels: **${summary.unmatchedModels}**`,
|
||||
`- ambiguousModels: **${summary.ambiguousModels}**`,
|
||||
`- nullSamplingFields: **${summary.nullSamplingFields}**`,
|
||||
`- updatedModels: **${summary.updatedModels}**`,
|
||||
`- updatedFiles: **${summary.updatedFiles}**`,
|
||||
"",
|
||||
"## Field Coverage",
|
||||
"",
|
||||
"- synced: none; this audit run is read-only and does not mutate JSON config.",
|
||||
"- reviewed from JSON: `modelName`, `displayName`, `serviceType`, `contextWindow`, `maxOutputTokens`, `inputPrice`, `outputPrice`, `capabilities`, `defaultTemperature`, `defaultTopP`, `extra`.",
|
||||
"- guardrail: `defaultTemperature` and `defaultTopP` must be omitted or numbers; `null` is invalid.",
|
||||
"",
|
||||
"## Updated Models",
|
||||
"",
|
||||
"- (none)",
|
||||
"",
|
||||
"## Skipped / Reviewed Models",
|
||||
"",
|
||||
...skipped.map((item) => {
|
||||
const detail = item.zenmux
|
||||
? `${item.reason}:${item.zenmux}`
|
||||
: item.candidates.length ? `${item.reason}:${item.candidates.join(",")}` : item.reason;
|
||||
return `- ${item.file} :: ${item.modelName} -> ${detail}`;
|
||||
}),
|
||||
"",
|
||||
];
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "zenmux-sync-report.md"), `${reportLines.join("\n")}\n`, "utf8");
|
||||
|
||||
const checklist = [
|
||||
"# Official Review Checklist",
|
||||
"",
|
||||
`GeneratedAt: ${generatedAt}`,
|
||||
`Source: ${path.join(OUTPUT_DIR, "zenmux-sync-report.json")}`,
|
||||
"",
|
||||
"Purpose: verify unresolved models with official vendor docs before write-back.",
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
`- actual models: **${summary.actualModels}**`,
|
||||
`- unresolved models: **${summary.unmatchedModels + summary.ambiguousModels}**`,
|
||||
`- providers involved: **${pendingByProvider.size}**`,
|
||||
`- null sampling fields: **${summary.nullSamplingFields}**`,
|
||||
"",
|
||||
];
|
||||
for (const [provider, items] of [...pendingByProvider.entries()].sort(([a], [b]) => a.localeCompare(b))) {
|
||||
checklist.push(`## Provider: ${provider}`);
|
||||
checklist.push("- official docs:");
|
||||
for (const doc of OFFICIAL_DOCS[provider] || ["(add link manually)"]) checklist.push(` - ${doc}`);
|
||||
checklist.push("- pending models:");
|
||||
for (const item of items) checklist.push(` - \`${item.modelName}\` (${item.file}) -> ${item.reason}`);
|
||||
checklist.push("");
|
||||
}
|
||||
checklist.push("## Field-by-Field Verification Rule");
|
||||
checklist.push("");
|
||||
checklist.push("- `modelName`: must exactly match provider API model ID or official alias rule.");
|
||||
checklist.push("- `contextWindow`: use official model spec limit.");
|
||||
checklist.push("- `maxOutputTokens`: use official output cap; do not infer from context.");
|
||||
checklist.push("- `inputPrice`/`outputPrice`: use official published API pricing and the provider currency.");
|
||||
checklist.push("- `capabilities`: project taxonomy field; keep local semantics unless official docs clearly contradict the capability.");
|
||||
checklist.push("- `defaultTemperature`/`defaultTopP`: only fill with numeric values when provider docs define a safe default; omit unsupported fields instead of using `null`.");
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "official-review-checklist.md"), `${checklist.join("\n")}\n`, "utf8");
|
||||
|
||||
const riskLines = [
|
||||
"# ZenMux Risk Audit",
|
||||
"",
|
||||
`GeneratedAt: ${generatedAt}`,
|
||||
`Source: ${path.join(OUTPUT_DIR, "zenmux-sync-report.json")}`,
|
||||
"",
|
||||
`- HIGH: ${summary.nullSamplingFields > 0 ? 1 : 0}`,
|
||||
`- MEDIUM: ${summary.ambiguousModels}`,
|
||||
`- LOW: ${summary.unmatchedModels}`,
|
||||
"",
|
||||
"## Notes",
|
||||
"",
|
||||
"- HIGH covers schema-breaking sampling defaults such as `null`.",
|
||||
"- MEDIUM covers ambiguous third-party model matches.",
|
||||
"- LOW covers models without a ZenMux match; these require official-doc review but do not by themselves indicate JSON invalidity.",
|
||||
];
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "zenmux-risk-audit.md"), `${riskLines.join("\n")}\n`, "utf8");
|
||||
|
||||
const detailedLines = [
|
||||
"# ZenMux Sync Detailed Report",
|
||||
"",
|
||||
`GeneratedAt: ${generatedAt}`,
|
||||
"",
|
||||
"## Summary",
|
||||
"",
|
||||
`- actualModels: ${summary.actualModels}`,
|
||||
`- sourceModels: ${summary.sourceModels}`,
|
||||
`- matchedModels: ${summary.matchedModels}`,
|
||||
`- unmatchedModels: ${summary.unmatchedModels}`,
|
||||
`- ambiguousModels: ${summary.ambiguousModels}`,
|
||||
`- nullSamplingFields: ${summary.nullSamplingFields}`,
|
||||
"",
|
||||
"## Current Model Inventory",
|
||||
"",
|
||||
"| File | Provider | Model | Match | ZenMux |",
|
||||
"|---|---|---|---|---|",
|
||||
...allModels.map((item) => `| ${item.file} | ${item.provider} | \`${item.modelName}\` | ${item.matchTier} | ${item.zenmux ? `\`${item.zenmux}\`` : ""} |`),
|
||||
];
|
||||
await fs.writeFile(path.join(OUTPUT_DIR, "zenmux-sync-detailed.md"), `${detailedLines.join("\n")}\n`, "utf8");
|
||||
|
||||
console.log(JSON.stringify({ generatedAt, summary, fieldTableFiles: fieldIndex.length }, null, 2));
|
||||
}
|
||||
|
||||
await main();
|
||||
372
tools/generate_field_tables_per_json.mjs
Normal file
372
tools/generate_field_tables_per_json.mjs
Normal file
@@ -0,0 +1,372 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
const ROOT = "/Users/xieyuanxiang/config-center";
|
||||
const OUT_ROOT = path.join(ROOT, "字段取值表");
|
||||
const ZENMUX_FILE = "/tmp/zenmux-models.json";
|
||||
|
||||
const TARGET_DIRS = [
|
||||
path.join(ROOT, "compute", "providers"),
|
||||
path.join(ROOT, "compute", "coding-plans"),
|
||||
];
|
||||
|
||||
const PROVIDER_OWNED_BY = {
|
||||
openai: ["openai"],
|
||||
anthropic: ["anthropic"],
|
||||
deepseek: ["deepseek"],
|
||||
google: ["google"],
|
||||
moonshot: ["moonshotai"],
|
||||
zhipu: ["z-ai"],
|
||||
"zhipu-embedding": ["z-ai"],
|
||||
dashscope: ["qwen"],
|
||||
minimax: ["minimax"],
|
||||
baidu: ["baidu"],
|
||||
tencent: ["tencent"],
|
||||
volcengine: ["bytedance", "volcengine"],
|
||||
xai: ["x-ai"],
|
||||
mistral: ["mistralai"],
|
||||
kwai: ["kuaishou"],
|
||||
lingyiwanwu: ["inclusionai"],
|
||||
siliconflow: ["qwen", "baai"],
|
||||
cohere: ["cohere"],
|
||||
perplexity: ["perplexity"],
|
||||
};
|
||||
|
||||
const OFFICIAL_DOCS = {
|
||||
openai: ["https://platform.openai.com/docs/models", "https://platform.openai.com/docs/pricing"],
|
||||
anthropic: ["https://docs.anthropic.com/en/docs/about-claude/models/all-models", "https://docs.anthropic.com/en/docs/about-claude/pricing"],
|
||||
deepseek: ["https://api-docs.deepseek.com/quick_start/pricing"],
|
||||
google: ["https://ai.google.dev/gemini-api/docs/models", "https://ai.google.dev/pricing"],
|
||||
moonshot: ["https://platform.moonshot.cn/docs/pricing/chat"],
|
||||
zhipu: ["https://docs.bigmodel.cn/cn/guide/models/text/", "https://www.bigmodel.cn/pricing"],
|
||||
dashscope: ["https://help.aliyun.com/zh/model-studio/getting-started/models", "https://help.aliyun.com/zh/model-studio/pricing"],
|
||||
minimax: ["https://platform.minimax.io/docs/guides/pricing-paygo"],
|
||||
baidu: ["https://cloud.baidu.com/doc/qianfan/"],
|
||||
tencent: ["https://cloud.tencent.com/document/product/1729"],
|
||||
volcengine: ["https://www.volcengine.com/docs/82379"],
|
||||
xai: ["https://docs.x.ai/docs/models"],
|
||||
mistral: ["https://docs.mistral.ai/getting-started/models", "https://mistral.ai/pricing"],
|
||||
cohere: ["https://docs.cohere.com/docs/models", "https://cohere.com/pricing"],
|
||||
siliconflow: ["https://www.siliconflow.com/models", "https://siliconflow.cn/pricing"],
|
||||
perplexity: ["https://docs.perplexity.ai"],
|
||||
};
|
||||
|
||||
function normalize(s) {
|
||||
return String(s || "")
|
||||
.toLowerCase()
|
||||
.replace(/[._/]/g, "-")
|
||||
.replace(/[^a-z0-9-]/g, "")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function stripTail(s) {
|
||||
return s
|
||||
.replace(/-(latest|preview|exp|experimental|stable)$/g, "")
|
||||
.replace(/-\d{8}$/g, "")
|
||||
.replace(/-\d{6}$/g, "")
|
||||
.replace(/-\d{4}-\d{2}-\d{2}$/g, "");
|
||||
}
|
||||
|
||||
function parseZen(m) {
|
||||
const [vendor, ...rest] = String(m.id).split("/");
|
||||
const model = rest.join("/");
|
||||
const norm = normalize(model);
|
||||
return {
|
||||
id: m.id,
|
||||
vendor,
|
||||
model,
|
||||
norm,
|
||||
stripped: stripTail(norm),
|
||||
context: m.context_length,
|
||||
reasoning: m.capabilities?.reasoning,
|
||||
prompt: m.pricings?.prompt?.[0],
|
||||
completion: m.pricings?.completion?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
function jaccard(a, b) {
|
||||
const A = new Set(a.split("-").filter(Boolean));
|
||||
const B = new Set(b.split("-").filter(Boolean));
|
||||
let inter = 0;
|
||||
for (const x of A) if (B.has(x)) inter += 1;
|
||||
return inter / (new Set([...A, ...B]).size || 1);
|
||||
}
|
||||
|
||||
function matchWithCandidates(provider, modelName, zenModels) {
|
||||
const aliases = PROVIDER_OWNED_BY[provider] || [provider];
|
||||
const pool = zenModels.filter((z) => aliases.includes(z.vendor));
|
||||
const localNorm = normalize(modelName);
|
||||
const localStripped = stripTail(localNorm);
|
||||
|
||||
const exact = pool.filter((z) => z.model === modelName);
|
||||
if (exact.length === 1) return { matched: exact[0], tier: "exact", candidates: exact };
|
||||
|
||||
const norm = pool.filter((z) => z.norm === localNorm);
|
||||
if (norm.length === 1) return { matched: norm[0], tier: "normalized", candidates: norm };
|
||||
|
||||
const stripped = pool.filter((z) => z.stripped === localStripped);
|
||||
if (stripped.length === 1) return { matched: stripped[0], tier: "stripped", candidates: stripped };
|
||||
|
||||
const ranked = pool
|
||||
.map((z) => ({ z, score: jaccard(localNorm, z.norm) }))
|
||||
.filter((x) => x.score >= 0.25)
|
||||
.sort((a, b) => b.score - a.score)
|
||||
.slice(0, 5)
|
||||
.map((x) => x.z);
|
||||
|
||||
if (ranked.length === 1) return { matched: ranked[0], tier: "similar", candidates: ranked };
|
||||
return { matched: null, tier: ranked.length ? "ambiguous" : "none", candidates: ranked };
|
||||
}
|
||||
|
||||
function fmt(v) {
|
||||
if (v === undefined) return "(缺省)";
|
||||
return `\`${JSON.stringify(v)}\``;
|
||||
}
|
||||
|
||||
function quoteCsv(arr) {
|
||||
if (!arr.length) return "(none)";
|
||||
return arr.map((x) => `\`${x}\``).join("、");
|
||||
}
|
||||
|
||||
function buildRows(doc, model, match) {
|
||||
const rows = [];
|
||||
const z = match.matched;
|
||||
const currency = doc.priceCurrency || "USD";
|
||||
|
||||
rows.push({
|
||||
field: "modelName",
|
||||
current: model.modelName,
|
||||
suggested: z ? z.model : model.modelName,
|
||||
decision: z && match.tier === "exact" ? "保持" : "待确认",
|
||||
reason: z ? `ZenMux匹配(${match.tier}): ${z.id}` : "ZenMux无稳定匹配",
|
||||
});
|
||||
|
||||
rows.push({
|
||||
field: "displayName",
|
||||
current: model.displayName,
|
||||
suggested: model.displayName,
|
||||
decision: "保持",
|
||||
reason: "展示字段,需按产品命名策略",
|
||||
});
|
||||
|
||||
rows.push({
|
||||
field: "serviceType",
|
||||
current: model.serviceType,
|
||||
suggested: model.serviceType,
|
||||
decision: "保持",
|
||||
reason: "服务路由字段,优先本项目约定",
|
||||
});
|
||||
|
||||
const trustedContextMatch = ["exact", "normalized", "stripped"].includes(match.tier);
|
||||
if (z && typeof z.context === "number" && trustedContextMatch) {
|
||||
const ratio = typeof model.contextWindow === "number" ? Math.abs(model.contextWindow - z.context) / Math.max(model.contextWindow, z.context) : 1;
|
||||
const sameScale = ratio <= 0.03;
|
||||
rows.push({
|
||||
field: "contextWindow",
|
||||
current: model.contextWindow,
|
||||
suggested: sameScale ? model.contextWindow : z.context,
|
||||
decision: sameScale ? "保持" : "建议修改",
|
||||
reason: sameScale ? `ZenMux(${z.id})口径近似(≤3%)` : `ZenMux(${z.id})提供明确context_length=${z.context}`,
|
||||
});
|
||||
} else {
|
||||
rows.push({
|
||||
field: "contextWindow",
|
||||
current: model.contextWindow,
|
||||
suggested: model.contextWindow,
|
||||
decision: "待确认",
|
||||
reason: z && typeof z.context === "number"
|
||||
? `ZenMux命中(${match.tier})但匹配置信不足,不直接覆盖context`
|
||||
: "ZenMux无context可用,需官方规格页确认",
|
||||
});
|
||||
}
|
||||
|
||||
rows.push({
|
||||
field: "maxOutputTokens",
|
||||
current: model.maxOutputTokens,
|
||||
suggested: model.maxOutputTokens,
|
||||
decision: "待确认",
|
||||
reason: "ZenMux列表未提供统一max output字段,需官方模型详情页",
|
||||
});
|
||||
|
||||
const canPriceFromZen = z && z.prompt && z.completion && z.prompt.unit === "perMTokens" && z.prompt.currency === "USD" && z.completion.unit === "perMTokens" && z.completion.currency === "USD";
|
||||
if (canPriceFromZen && currency === "USD" && (match.tier === "exact" || match.tier === "normalized")) {
|
||||
rows.push({
|
||||
field: "inputPrice",
|
||||
current: model.inputPrice,
|
||||
suggested: z.prompt.value,
|
||||
decision: model.inputPrice === z.prompt.value ? "保持" : "建议修改",
|
||||
reason: `ZenMux(${z.id}) prompt=${z.prompt.value} USD/MTokens`,
|
||||
});
|
||||
rows.push({
|
||||
field: "outputPrice",
|
||||
current: model.outputPrice,
|
||||
suggested: z.completion.value,
|
||||
decision: model.outputPrice === z.completion.value ? "保持" : "建议修改",
|
||||
reason: `ZenMux(${z.id}) completion=${z.completion.value} USD/MTokens`,
|
||||
});
|
||||
} else {
|
||||
rows.push({
|
||||
field: "inputPrice",
|
||||
current: model.inputPrice,
|
||||
suggested: model.inputPrice,
|
||||
decision: "待确认",
|
||||
reason: canPriceFromZen
|
||||
? `本文件币种为${currency},ZenMux价格为USD,需官方价格页复核`
|
||||
: "ZenMux无稳定价格可用,需官方价格页复核",
|
||||
});
|
||||
rows.push({
|
||||
field: "outputPrice",
|
||||
current: model.outputPrice,
|
||||
suggested: model.outputPrice,
|
||||
decision: "待确认",
|
||||
reason: canPriceFromZen
|
||||
? `本文件币种为${currency},ZenMux价格为USD,需官方价格页复核`
|
||||
: "ZenMux无稳定价格可用,需官方价格页复核",
|
||||
});
|
||||
}
|
||||
|
||||
rows.push({
|
||||
field: "capabilities",
|
||||
current: model.capabilities,
|
||||
suggested: model.capabilities,
|
||||
decision: typeof z?.reasoning === "boolean" ? "待确认" : "保持",
|
||||
reason: typeof z?.reasoning === "boolean"
|
||||
? `ZenMux给出reasoning=${z.reasoning},但capabilities是项目语义字段,需官方能力说明复核`
|
||||
: "ZenMux无明确能力映射差异",
|
||||
});
|
||||
|
||||
rows.push({
|
||||
field: "defaultTemperature",
|
||||
current: model.defaultTemperature,
|
||||
suggested: model.defaultTemperature,
|
||||
decision: "待确认",
|
||||
reason: "官方通常不提供默认采样参数",
|
||||
});
|
||||
|
||||
rows.push({
|
||||
field: "defaultTopP",
|
||||
current: model.defaultTopP,
|
||||
suggested: model.defaultTopP,
|
||||
decision: "待确认",
|
||||
reason: "官方通常不提供默认采样参数",
|
||||
});
|
||||
|
||||
rows.push({
|
||||
field: "extra",
|
||||
current: model.extra,
|
||||
suggested: model.extra,
|
||||
decision: "待确认",
|
||||
reason: "扩展字段为本地schema,需业务侧定义",
|
||||
});
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function listFiles() {
|
||||
const out = [];
|
||||
for (const dir of TARGET_DIRS) {
|
||||
for (const f of await fs.readdir(dir)) {
|
||||
if (!f.endsWith(".json") || f === "_index.json") continue;
|
||||
out.push(path.join(dir, f));
|
||||
}
|
||||
}
|
||||
return out.sort();
|
||||
}
|
||||
|
||||
function folderByJson(relPath) {
|
||||
const base = path.basename(relPath, ".json");
|
||||
return path.join(OUT_ROOT, base);
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const zraw = JSON.parse(await fs.readFile(ZENMUX_FILE, "utf8"));
|
||||
const zen = (zraw.data || []).map(parseZen);
|
||||
const files = await listFiles();
|
||||
const generatedAt = new Date().toISOString();
|
||||
await fs.mkdir(OUT_ROOT, { recursive: true });
|
||||
|
||||
const global = [];
|
||||
|
||||
for (const abs of files) {
|
||||
const rel = path.relative(ROOT, abs);
|
||||
const doc = JSON.parse(await fs.readFile(abs, "utf8"));
|
||||
const provider = doc.provider || path.basename(abs, ".json");
|
||||
const models = Array.isArray(doc.models) ? doc.models : [];
|
||||
const outDir = folderByJson(rel);
|
||||
await fs.mkdir(outDir, { recursive: true });
|
||||
|
||||
const detailed = [];
|
||||
const unresolved = [];
|
||||
detailed.push(`# 详细字段取值表 - ${rel}`);
|
||||
detailed.push("");
|
||||
detailed.push(`- provider: \`${provider}\``);
|
||||
detailed.push(`- priceCurrency: \`${doc.priceCurrency || "USD"}\``);
|
||||
detailed.push(`- generatedAt: \`${generatedAt}\``);
|
||||
detailed.push("");
|
||||
detailed.push("## 来源");
|
||||
detailed.push("");
|
||||
for (const d of OFFICIAL_DOCS[provider] || ["(待补充provider官方文档)"]) detailed.push(`- ${d}`);
|
||||
detailed.push("- https://zenmux.ai/models");
|
||||
detailed.push("- https://zenmux.ai/api/v1/models");
|
||||
detailed.push("");
|
||||
|
||||
unresolved.push(`# 未确认字段报告 - ${rel}`);
|
||||
unresolved.push("");
|
||||
unresolved.push(`- provider: \`${provider}\``);
|
||||
unresolved.push(`- generatedAt: \`${generatedAt}\``);
|
||||
unresolved.push("");
|
||||
|
||||
for (const m of models) {
|
||||
const match = matchWithCandidates(provider, m.modelName, zen);
|
||||
const rows = buildRows(doc, m, match);
|
||||
detailed.push(`## ${m.modelName}`);
|
||||
detailed.push("");
|
||||
detailed.push(`- ZenMux匹配级别: \`${match.tier}\``);
|
||||
if (match.matched) detailed.push(`- ZenMux命中: \`${match.matched.id}\``);
|
||||
detailed.push(`- ZenMux候选: ${quoteCsv((match.candidates || []).map((x) => x.id))}`);
|
||||
detailed.push("");
|
||||
detailed.push("| 字段 | 当前值 | 建议值 | 结论 | 依据/说明 |");
|
||||
detailed.push("|---|---|---|---|---|");
|
||||
for (const r of rows) {
|
||||
detailed.push(`| \`${r.field}\` | ${fmt(r.current)} | ${fmt(r.suggested)} | ${r.decision} | ${r.reason} |`);
|
||||
}
|
||||
detailed.push("");
|
||||
|
||||
const pending = rows.filter((r) => r.decision === "待确认");
|
||||
if (pending.length) {
|
||||
unresolved.push(`## ${m.modelName}`);
|
||||
unresolved.push("");
|
||||
for (const p of pending) unresolved.push(`- \`${p.field}\`: ${p.reason}`);
|
||||
unresolved.push("");
|
||||
}
|
||||
}
|
||||
|
||||
if (unresolved.length === 3) {
|
||||
unresolved.push("- (none)");
|
||||
unresolved.push("");
|
||||
}
|
||||
|
||||
const detailedFile = path.join(outDir, "详细字段取值表.md");
|
||||
const unresolvedFile = path.join(outDir, "未确认字段报告.md");
|
||||
await fs.writeFile(detailedFile, `${detailed.join("\n")}\n`, "utf8");
|
||||
await fs.writeFile(unresolvedFile, `${unresolved.join("\n")}\n`, "utf8");
|
||||
|
||||
global.push({
|
||||
file: rel,
|
||||
folder: path.relative(ROOT, outDir),
|
||||
detailed: path.relative(ROOT, detailedFile),
|
||||
unresolved: path.relative(ROOT, unresolvedFile),
|
||||
modelCount: models.length,
|
||||
});
|
||||
}
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(OUT_ROOT, "目录索引.json"),
|
||||
`${JSON.stringify(global, null, 2)}\n`,
|
||||
"utf8"
|
||||
);
|
||||
console.log(JSON.stringify({ files: global.length, out: OUT_ROOT }, null, 2));
|
||||
}
|
||||
|
||||
await main();
|
||||
Reference in New Issue
Block a user