feat(admin): 计价配置(按模型·分输入/输出单价)—— 计费比率配置落地
计费需 token↔真钱比率,配置归管理端。本次落地"按模型·分输入/输出"粒度: 后端(gateway): - store.Pricing 模型(BaseModel + model_id 唯一 + input_per_1k/output_per_1k + currency), AutoMigrate 建 sundynix_pricing;ListPricing/UpsertPricing(OnConflict model_id 覆盖)。 - admin handler:GET /admin/pricing 列表、PUT /admin/pricing 设置(校验非负,币种默认 CNY), 挂在 RequireAdmin 组下。 前端(admin): - api:listPricing/savePricing(带 Bearer)。 - PricingPage:列出所有已登记模型(chat+embedding),每行可编辑 输入/输出每1K单价 + 币种,逐行保存。 - routes 新增「计价」页(配置组)。 实测:PUT→ok;GET 返回正确行;重复 PUT 同 model_id 仍 1 行且值更新(upsert 生效);表自动迁移。 前端 tsc 干净。下一步可做用量计量 × 单价折算(真正计费)。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -120,6 +120,28 @@ export async function testModel(m: ModelInput): Promise<{ ok: boolean; message:
|
||||
return (await res.json()) as { ok: boolean; message: string };
|
||||
}
|
||||
|
||||
// ---- 计价(token↔真钱,按模型分输入/输出)----
|
||||
export interface Pricing {
|
||||
model_id: string;
|
||||
input_per_1k: number;
|
||||
output_per_1k: number;
|
||||
currency: string;
|
||||
}
|
||||
|
||||
export async function listPricing(): Promise<Pricing[]> {
|
||||
const res = guard(await fetch(`${ADMIN}/pricing`, { headers: authHeaders() }));
|
||||
if (!res.ok) throw new Error(`list pricing failed: ${res.status}`);
|
||||
return ((await res.json()) as { pricing: Pricing[] }).pricing ?? [];
|
||||
}
|
||||
|
||||
export async function savePricing(p: Pricing): Promise<void> {
|
||||
const res = guard(await fetch(`${ADMIN}/pricing`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(p) }));
|
||||
if (!res.ok) {
|
||||
const d = (await res.json().catch(() => ({}))) as { error?: string };
|
||||
throw new Error(d.error ?? `save pricing failed: ${res.status}`);
|
||||
}
|
||||
}
|
||||
|
||||
// gatewayOnline 用公开的 /healthz 探活(不受鉴权影响)。
|
||||
export async function gatewayOnline(): Promise<boolean> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user