feat: 项目级成本预算系统(按模型×token 精确计费 + 超额自动暂停)
精确到项目级的成本/预算护栏,成本按「模型 × token 用量」折算(⑧ ★ 新特性)。
定价(单源):
- src/model/pricing.ts:MODEL_PRICES(opus/sonnet/haiku,USD/1M token,分
input/output/cacheRead/cacheWrite)+ computeCost/addUsage,前缀容错、未知模型记 0
token 捕获链(worker → daemon):
- cc.ts:从 SDK result 消息抓 usage,跨 resume/回退累计;CCResult.usage
- runner/reviewer:结果对象带 usage + modelUsed
- protocol.ts:新增 'usage' outbox 记录;pipeline 每次 CC(executor/复审/planner/
conflict)后 emit usage(worker 侧写文件,不碰 DB)
- ingest.ts:'usage' → store.setRunUsage(累加、按模型折算)→ enforceBudget
存储 + 护栏:
- schema/db.ts:runs.usage/cost_usd、projects.budget_usd/budget_period(幂等迁移)
- store.ts:setRunUsage(累加)、projectSpend、costSummary(按项目/模型/总计)、
enforceBudget(超额 → 置 paused + 广播 budget.exceeded)
- 编排器天然停领:orchestrator 既有「跳过 paused 项目」即生效,无需改
API:
- GET /api/usage:并入成本明细 {session,weekly,cost:{period,total,byProject,byModel}},?period/?project
- GET /api/health:{ok,db,inflight,at}
- PATCH /api/projects:支持 budgetUsd/budgetPeriod
前端:
- adapt.js:agentSummary 用真实成本/token;adaptProject 透传预算字段
- app.jsx:budget.exceeded → 告警 toast
- ConfigPanel:预算 $ + 周期(day|month)受控输入
验证:typecheck 干净;206 测试通过(含新增 budget.test.ts 4 例:定价折算/累加/
costSummary/enforceBudget);前端 build + 截图确认预算输入渲染、0 错误。
注:生效需合并后重启 daemon(迁移幂等加列、向后兼容旧库)。
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
// 模型计价(单源)。成本 = Σ(各类 token × 单价),单价 USD / 每 1M token。
|
||||
// 预算系统据此把 run 的 token 用量折算成精确到项目级的成本。价目随官方调整,集中此处维护。
|
||||
|
||||
export interface UsageTokens {
|
||||
input: number; // 非缓存输入 token
|
||||
output: number; // 输出 token
|
||||
cacheRead: number; // 缓存命中读取 token
|
||||
cacheWrite: number; // 缓存写入(创建)token
|
||||
}
|
||||
|
||||
export const EMPTY_USAGE: UsageTokens = { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 };
|
||||
|
||||
interface Price { input: number; output: number; cacheRead: number; cacheWrite: number }
|
||||
|
||||
// 单价:USD / 1M token
|
||||
const MODEL_PRICES: Record<string, Price> = {
|
||||
'claude-opus-4-8': { input: 15, output: 75, cacheRead: 1.5, cacheWrite: 18.75 },
|
||||
'claude-sonnet-4-6': { input: 3, output: 15, cacheRead: 0.3, cacheWrite: 3.75 },
|
||||
'claude-haiku-4-5': { input: 1, output: 5, cacheRead: 0.1, cacheWrite: 1.25 },
|
||||
};
|
||||
|
||||
// 模型名归一:精确命中优先,否则按已知前缀匹配(容忍日期/区域后缀)
|
||||
function priceOf(model: string): Price | null {
|
||||
if (MODEL_PRICES[model]) return MODEL_PRICES[model];
|
||||
for (const key of Object.keys(MODEL_PRICES)) if (model.startsWith(key)) return MODEL_PRICES[key];
|
||||
return null;
|
||||
}
|
||||
|
||||
/** 把一个 run 的 token 用量按其模型折算成 USD。未知模型 → 0(不阻断,仅成本记 0)。 */
|
||||
export function computeCost(model: string, u: UsageTokens): number {
|
||||
const p = priceOf(model);
|
||||
if (!p) return 0;
|
||||
const M = 1_000_000;
|
||||
return (u.input * p.input + u.output * p.output + u.cacheRead * p.cacheRead + u.cacheWrite * p.cacheWrite) / M;
|
||||
}
|
||||
|
||||
export function addUsage(a: UsageTokens, b: UsageTokens): UsageTokens {
|
||||
return {
|
||||
input: a.input + b.input, output: a.output + b.output,
|
||||
cacheRead: a.cacheRead + b.cacheRead, cacheWrite: a.cacheWrite + b.cacheWrite,
|
||||
};
|
||||
}
|
||||
|
||||
export function totalTokens(u: UsageTokens): number {
|
||||
return u.input + u.output + u.cacheRead + u.cacheWrite;
|
||||
}
|
||||
|
||||
export function isKnownModel(model: string): boolean {
|
||||
return priceOf(model) !== null;
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
import type { Complexity } from './complexity.js';
|
||||
import type { TaskStatus, GateKind } from './status.js';
|
||||
import type { UsageTokens } from './pricing.js';
|
||||
|
||||
export type { UsageTokens };
|
||||
|
||||
export type Id = string;
|
||||
|
||||
@@ -31,6 +34,9 @@ export interface Project {
|
||||
autoApproveExec?: boolean; // 双复审 approve 后跳过 exec_review 自动合并(默认 false)
|
||||
/** 分项检查命令(JSON,如 {"lint":"npm run lint","typecheck":"npm run typecheck"})*/
|
||||
checks?: string | null;
|
||||
/** 项目级成本预算(按模型×token 折算累计;超额自动暂停)*/
|
||||
budgetUsd?: number | null; // 当期预算上限 USD(null=不限)
|
||||
budgetPeriod?: 'day' | 'month'; // 预算周期(默认 month)
|
||||
}
|
||||
|
||||
export interface ApprovalRecord {
|
||||
@@ -98,6 +104,8 @@ export interface Run {
|
||||
error: string | null;
|
||||
workerPid: number | null; // 执行该 run 的 worker 进程 pid(多进程执行;daemon 写,判活用)
|
||||
lastSeq: number; // 已 ingest 的 outbox 最大 seq(daemon 写,幂等游标;重启续读)
|
||||
usage?: UsageTokens | null; // token 用量(worker 从 SDK 抓取,daemon ingest 落库)
|
||||
costUsd?: number | null; // 该 run 折算成本 USD(按 model × usage,见 model/pricing.ts)
|
||||
}
|
||||
|
||||
export type EventType =
|
||||
@@ -105,6 +113,7 @@ export type EventType =
|
||||
| 'approval.requested' | 'approval.granted' | 'approval.rejected'
|
||||
| 'run.started' | 'run.finished'
|
||||
| 'merge.failed' | 'merge.remediated'
|
||||
| 'budget.exceeded'
|
||||
| 'project.synced';
|
||||
|
||||
export interface Event {
|
||||
|
||||
Reference in New Issue
Block a user