Files
config-center/tools/generate_field_tables_per_json.mjs
xyx 370c3d17d1 chore: preserve field audit artifacts and tools (#5)
* 备份同步前的配置审计修改

* 更新字段审计产物
2026-04-26 10:34:19 +08:00

373 lines
13 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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();