Skip to content

供应商支持按模型配置倍率 / 单价覆盖(现有倍率是模型无关的标量) #1401

Description

@Cassiel-girl

供应商支持按模型配置倍率 / 单价覆盖(现有倍率是模型无关的标量)

版本:v0.9.2(ccbad37,撰写时 devmain 同点)

背景

上游中转商的定价通常是按模型分档的:同一家供应商,Opus 系列按 1.2 倍计费,claude-fable-5 按 2.0 倍计费。

但 CC Hub 目前的两个倍率都是模型无关的标量

  • provider_groups.cost_multipliersrc/drizzle/schema.ts:176
  • providers.cost_multipliersrc/drizzle/schema.ts:200

最终扣费为 基础价 × provider_multiplier × group_multipliersrc/lib/utils/cost-calculation.ts:877-878)。

结果是:只要按 Opus 把分组倍率配成 1.2,用户实际调用 claude-fable-5 时也会按 1.2 计费,而上游按 2.0 收,中间的差额是净亏损。反之若配成 2.0,则 Opus 用户被多收。

model_prices 表是全局按模型名的(src/drizzle/schema.ts:748),没有 per-provider 覆盖,所以也无法从价格表侧解决——改了会影响所有供应商。

当前 workaround 及其代价

目前只能把同一个上游拆成两条供应商记录,用 allowed_models 白名单分开,再各设 cost_multiplier

供应商-Opus    → allowed_models: opus 系列, cost_multiplier: 1.2
供应商-Fable5  → allowed_models: claude-fable-5, cost_multiplier: 2.0

能跑通,但代价是实打实的:

  1. 熔断状态被拆开:熔断是 per-provider-id 的(isCircuitOpen(provider.id))。上游整体故障时,两条记录要各自撞一次墙才熔断。
  2. 限额被拆开:5h / 日 / 周 / 月 / 总额都按 provider 记(RateLimitService.checkCostLimitsWithLease(provider.id, ...))。同一个上游账户的余额无法用一条限额统一卡住,只能手工把额度拆分,而实际消耗比例是浮动的。
  3. 条目数按乘法增长:N 个上游 × M 档模型价。接入方增加或上游新增模型档位时不可维护。
  4. 权重 / 优先级 / 代理 / 超时等配置重复维护,改一次策略要改多处,容易漏。
  5. 路由链日志与排行榜里同一个上游被拆成两个供应商,排查和对账时对不上。

本质上这是在用路由的原语去表达计费的语义

提议

方案 A:providers.model_multipliers(改动小,建议先做)

新增 jsonb 字段,复用已有的匹配规则类型:

// 复用 src/types/provider.ts:57 的 ProviderModelRedirectMatchType
// ("exact" | "prefix" | "suffix" | "contains" | "regex")
interface ProviderModelMultiplierRule {
  matchType: ProviderModelRedirectMatchType;
  pattern: string;
  multiplier: number;
}
// providers.model_multipliers: ProviderModelMultiplierRule[] | null

语义:按数组顺序取第一条命中的规则;无命中则回退到现有的 providers.cost_multiplier。默认 null,对存量部署完全无行为变化。

改动面比较收敛:

  • src/drizzle/schema.ts + migration:加字段
  • src/app/v1/_lib/proxy/response-handler.ts:6220const costMultiplier = provider.costMultiplier):改为按模型解析
  • src/app/v1/_lib/proxy/response-handler.ts:1252 buildCostCalculationOptions():透传
  • src/types/cost-breakdown.ts:25,27:已有 provider_multiplier / group_multiplier,追加 model_multiplier
  • message_request / usage_ledger:已有 cost_multiplier / group_cost_multiplier 列,同构追加一列或并入 cost_breakdown
  • 供应商表单 UI + i18n(估计是工作量的大头)

匹配器不用新写——allowed_modelssrc/types/provider.ts:65)和 model_redirects 已经在用同一套 matchType。

方案 B:按供应商覆盖价格表(更彻底,可作为后续演进)

(provider_id, model) → priceData,覆盖全局 model_prices

理由:上游网页给出的本来就是单价表而不是倍率;而且不少中转对 cache read / cache write 的折扣比例与官方不同,一个标量倍率乘所有 token 类型算出来仍然是偏的。倍率可以看作单价覆盖的简化形式,两者不冲突,A 先落地、B 后续演进是合理路径。

需要明确的交互点

  1. 按哪个模型名匹配:应遵循已有的系统设置 BillingModelSource"original" | "redirected"src/types/system-config.ts:5,见 计费依据能改成实际转发的模型吗?可以设置吗? #1397 / 计费模型设置为重定向之后的模型来计费,这个配置好像是没生效的? #1296),保持与计费口径一致,不要引入第二套判定。
  2. 与分组倍率的关系:建议维持相乘(base × model_multiplier × group_multiplier),分组倍率退回其本来的语义——「这个客户群体加价多少」,而不是「这个模型贵多少」。
  3. 可读性基础价 × 模型倍率 × 供应商倍率 × 分组倍率 四个数相乘,配置者无法心算。表单里需要一个最终单价预览:选定模型后直接显示每 M token 实际扣费。
  4. 日志展示:请求详情里应能看到三层倍率各自的取值和命中的规则,否则用户会来问「为什么这笔按 X 计费」。

附带建议:按模型维度的路由总览

路由能力其实已经够用(allowed_models + model_redirects + group_priorities + 调度模拟器),缺的是可见性——配置分散在三个字段里,页面上看不出「claude-fable-5 这个模型最终会走哪几家、什么顺序、什么价」。

建议加一个按模型维度的总览:选一个模型 → 列出候选供应商、生效优先级、权重、当前熔断状态、预计单价。这块和上面第 3 点的「最终单价预览」其实是同一块 UI,可以一起做。

兼容性

新字段默认 null / 空数组,不配置时行为与现在完全一致,无需数据迁移。已经用「拆供应商」绕过的用户可以自行合并,也可以保持不动。


English Summary

Problem. Both cost multipliers in CC Hub are model-agnostic scalars: provider_groups.cost_multiplier and providers.cost_multiplier, applied as base × provider_multiplier × group_multiplier (cost-calculation.ts:877-878). Upstream relay vendors, however, price per model — e.g. 1.2× for Opus and 2.0× for claude-fable-5 on the same account. Setting the group multiplier to 1.2 undercharges every Fable 5 request; setting it to 2.0 overcharges Opus users. model_prices is global per model name with no per-provider override, so it can't be fixed from the pricing side either.

Current workaround is to split one upstream into two provider rows separated by allowed_models. That splits circuit-breaker state (per provider id), splits spend limits (per provider id) so one upstream account balance can't be capped by a single limit, multiplies row count by N vendors × M price tiers, duplicates weight/priority/proxy config, and shows one upstream as two providers in the call-chain log and leaderboard. It expresses billing semantics with routing primitives.

Proposal A (small): add providers.model_multipliers{matchType, pattern, multiplier}[], reusing the existing ProviderModelRedirectMatchType matcher already shared by allowed_models and model_redirects. First matching rule wins; fall back to providers.cost_multiplier. Defaults to null, so existing deployments are unaffected. Touches the schema, the multiplier resolution at response-handler.ts:6220, buildCostCalculationOptions(), cost_breakdown, the two ledger tables, and the provider form.

Proposal B (later): per-provider price-table overrides (provider_id, model) → priceData. Vendors publish unit prices rather than multipliers, and several relays apply non-standard cache read/write discounts that a single scalar can't express.

Open points: model matching should follow the existing BillingModelSource setting ("original" | "redirected", see #1397 / #1296) rather than introduce a second rule; the three multipliers should stay multiplicative with group multiplier reverting to its real meaning (per-customer-tier markup); the form needs an effective-unit-price preview since four multiplied numbers aren't reviewable by hand; request details should show which rule matched.

Related: a per-model routing overview page (candidate providers, effective priority, weight, circuit state, effective price for a chosen model) would share the same UI as the price preview. Routing capability already exists — visibility is what's missing.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    Status
    Backlog

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions