ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

OpenCodex 提供商用量成本明细第一阶段:服务端成本聚合与前端数据管线的完整实现契约

OpenCodex 提供商用量成本明细第一阶段:服务端成本聚合与前端数据管线的完整实现契约 【免费下载链接】opencodexUniversal provider proxy for OpenAI Codex Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code项目地址https://gitcode.com/gh_mirrors/ope/opencodex点击查看免费下载本篇技术指南完整讲解 OpenCodex通用 LLM Provider 代理提供商用量成本明细Provider Usage Cost Breakdown功能的第一阶段——数据管线Data Plumbing如何在服务端用量汇总中新增按模型、按提供商的estimatedCostUsd聚合并把它沿着ProviderWorkspaceShell → Providers → ProviderDetails → ProviderUsage的 props 链条完整穿透到前端同时不渲染任何新 UI。读完后你将掌握该功能的成本归属规则单请求与 combo 多尝试请求、字段缺省契约、共享格式化器的行为边界以及可复制的验收检查项与验证命令。一、阶段定位与核心不变量该功能的目标是在工作区 UI 的每个提供商 Usage 页签中增加按模型的成本明细Phase 2 才会渲染 3 列 KPI 5 列模型表格而 Phase 1 的职责是把数据接好线。原始实施契约见 010_data_plumbing.md总体计划见 000_plan.md。Phase 1 必须遵守以下不变量invariants只保留一次用量请求/api/usage?range30d是唯一的用量请求不得引入第二次 fetch增量、非破坏的响应形状estimatedCostUsd以可选字段形式追加到现有UsageModel/UsageProvider响应结构中models[].provider只用作分组键它在进入每个提供商私有的ProviderModelUsageRow时被移除成本在服务端计算模型与提供商成本由每条请求匹配到的价格计算得出combo 请求会把每次物理尝试attempt的成本归入该尝试自己的模型/提供商桶Phase 1 不直接展示新值保留现有行为请求/token 总数、模型加载状态、配额行为、取消守卫cancellation guard、fetch 失败行为均不变本阶段不包含任何可见 UI、CSS 或 i18n 改动。一个值得注意的契约细节原始需求点名的 4 个文件不足以完成数据链路因为 gui/src/pages/Providers.tsx 是显式逐字段映射DetailSlotData而非展开传递的gui/src/components/provider-workspace/ProviderUsage.tsx 也必须接收新转发过来的 props 才能通过类型检查。因此这两处的最小改动属于编译闭环改动而不是 Phase 2 的 UI 工作。变更清单动作文件目的MODIFYsrc/usage/summary.ts新增按模型、按提供商的估算成本聚合MODIFYtests/usage-summary.test.ts固化单请求与 combo 成本归属到模型/提供商行MODIFYgui/src/components/provider-workspace/types.ts新增带服务端成本的共享模型用量契约MODIFYgui/src/provider-workspace/usage.ts新增共享的 USD 估算格式器MODIFYtests/provider-workspace-data.test.ts固化格式器对 null/非法/多语言环境的行为MODIFYgui/src/components/provider-workspace/ProviderWorkspaceShell.tsx在既有用量 fetch 中捕获模型行含服务端成本与汇总成本MODIFYgui/src/pages/Providers.tsx通过显式 props 映射器转发两个新的 detail-slot 字段MODIFYgui/src/components/provider-workspace/ProviderDetails.tsx接收并转发新数据给ProviderUsageMODIFYgui/src/components/provider-workspace/ProviderUsage.tsx接收 Phase 1 props不改变渲染输出二、服务端成本聚合扩展 usage 响应行2.1UsageModel与UsageProvider增加可选成本字段在 src/usage/summary.ts 中UsageModel与UsageProvider的接口在shareRatio之后追加可选字段export interface UsageModel { provider: string; model: string; resolvedModel?: string; requests: number; attemptCount: number; measuredRequests: number; reportedRequests: number; estimatedRequests: number; totalTokens: number; inputTokens: number; outputTokens: number; shareRatio: number; estimatedCostUsd?: number; // ← 新增可选未定价行保持缺省 } export interface UsageProvider { provider: string; requests: number; attemptCount: number; measuredRequests: number; reportedRequests: number; estimatedRequests: number; totalTokens: number; shareRatio: number; estimatedCostUsd?: number; // ← 新增 }当前仓库源码中这两个接口确实已经携带该可选字段见 src/usage/summary.ts 中UsageModel.estimatedCostUsdL131与UsageProvider.estimatedCostUsdL154并且同文件的UsageDayModel、UsageAccount、按日汇总等结构也采用了同样的可选成本 缺省即未定价约定。2.2 在buildModels()中追加第二轮 entries 遍历累加按模型成本实现位置在buildModels()内在既有的 token/状态累加循环之后、statusesByKey定稿循环之前追加一次对entries的遍历复用从./cost导入的estimateRequestCost与estimateComboCostfor (const entry of entries) { if (entry.attempts?.length) { const estimate estimateComboCost(entry.attempts); for (const attempt of estimate?.attempts ?? []) { const providerKey baseProviderLabel(attempt.provider); const model byKey.get(${providerKey}${attempt.model}); if (model) model.estimatedCostUsd (model.estimatedCostUsd ?? 0) attempt.cost.total; } continue; } const estimate estimateRequestCost({ provider: entry.provider, model: entry.model, usage: entry.usage, usageStatus: entry.usageStatus, }); if (!estimate) continue; const providerKey baseProviderLabel(entry.provider); const model byKey.get(${providerKey}${entry.model}); if (model) model.estimatedCostUsd (model.estimatedCostUsd ?? 0) estimate.cost.total; }buildProviders()采用完全对称的第二轮遍历只是按baseProviderLabel(entry.provider)直接取提供商桶累加。2.3 成本估算器的底层语义与失败关闭这两轮累加复用的估算器定义在 src/usage/cost.tsestimateRequestCostcost.ts#L616-L652先normalizeCostTokens归一化 token再resolveMatchedPrice解析价格用户覆盖层 → 目录价随后应用长上下文档位与 priority 乘数token 缺失、价格未匹配时返回nullestimateComboCostcost.ts#L574-L613对每次尝试按其自身费率计价并求和且失败关闭fail closed——只要有任何一次尝试未定价或不可归一化整体返回null而不是部分和。其返回值中的attempts数组保留了每次尝试各自的provider、model与cost.total这正是 combo 成本能按尝试归属到各模型/提供商桶的依据。由此可以推出该阶段的三条设计决策与全局汇总同价addEstimatedCostsummary.ts#L614 附近已经用同一对估算器定义了全局 summary 的计价行为第二轮遍历复用它们保证同一笔定价同时进入全局汇总、模型行、提供商行三者一致未定价行不填假零estimate为null时continueestimatedCostUsd保持**缺省absent**而非0消费端据此区分未定价与成本为零combo 不落到合成行estimateComboCost返回的逐次尝试估算保留了每次尝试的原生模型与提供商因此没有任何成本被记到合成的combo身份上——失败的尝试与成功的尝试各自独立计入自己的桶。baseProviderLabel负责把提供商名规范化为分组键例如把chatgpt归一为openai这既保证聚合键稳定也引出了下文第 7 节说明的已知局限。2.4 用测试固化成本归属契约tests/usage-summary.test.ts 中新增/扩展的期望分两类。其一固化已定价行与未定价行的字段契约在既有aggregates estimated cost via model-level prices and counts unpriced rows用例的全局成本断言之后追加expect(all.summary.estimatedCostUsd).toBeCloseTo(expected, 9); expect(all.models.find(row row.model gpt-5.5)?.estimatedCostUsd) .toBeCloseTo((100 * 5 10 * 30) / 1e6, 9); expect(all.providers.find(row row.provider openai)?.estimatedCostUsd) .toBeCloseTo((100 * 5 10 * 30) / 1e6, 9); expect(all.models.find(row row.model nope-model)?.estimatedCostUsd).toBeUndefined(); expect(all.providers.find(row row.provider nope)?.estimatedCostUsd).toBeUndefined();其二固化 combo 尝试级归属在keeps one logical combo request while attributing both physical attempts用例之后新增独立用例。构造一条含两次物理尝试的 combo 记录——第 1 次openai/gpt-5.5503估算 usage100 input token第 2 次anthropic/claude-fable-5200上报 usage10 input 2 outputtest(attributes combo attempt costs to each model and provider row, () { const combo entry({ ts: FIXED_NOW - 1, requestId: priced-combo, provider: combo, model: combo/free, usageStatus: estimated, usage: { inputTokens: 110, outputTokens: 2, totalTokens: 112, estimated: true }, totalTokens: 112, attempts: [ { ordinal: 1, provider: openai, model: gpt-5.5, adapter: openai-chat, status: 503, durationMs: 4, sendCount: 1, recoveryKinds: [], usageStatus: estimated, inputTokenEstimate: 100, usage: { inputTokens: 100, outputTokens: 0, estimated: true }, totalTokens: 100, }, { ordinal: 2, provider: anthropic, model: claude-fable-5, adapter: openai-chat, status: 200, durationMs: 3, sendCount: 1, recoveryKinds: [], usageStatus: reported, usage: { inputTokens: 10, outputTokens: 2 }, totalTokens: 12, }, ], }); const sum summarizeUsage([combo], 30d, FIXED_NOW); expect(sum.models.find(row row.provider openai row.model gpt-5.5)?.estimatedCostUsd) .toBeCloseTo((100 * 5 0 * 30) / 1e6, 9); expect(sum.models.find(row row.provider anthropic row.model claude-fable-5)?.estimatedCostUsd) .toBeCloseTo((10 * 10 2 * 50) / 1e6, 9); expect(sum.providers.find(row row.provider openai)?.estimatedCostUsd) .toBeCloseTo((100 * 5 0 * 30) / 1e6, 9); expect(sum.providers.find(row row.provider anthropic)?.estimatedCostUsd) .toBeCloseTo((10 * 10 2 * 50) / 1e6, 9); });这组断言防止两类回归把整条父级成本记到合成的combo模型/提供商上或者只把成本记到成功的那次尝试上503 的失败尝试同样应按自身价格计价入账。三、前端共享契约ProviderModelUsageRow与formatCostUsd3.1 提供商私有的模型行契约在 gui/src/components/provider-workspace/types.ts 的ProviderUsageTotals声明之后新增/** Per-model usage row from /api/usage, filtered by provider. */ export interface ProviderModelUsageRow { model: string; resolvedModel?: string; requests: number; totalTokens: number; inputTokens: number; outputTokens: number; shareRatio: number; estimatedCostUsd?: number; }设计要点行结构刻意不包含provider字段——Shell 把行存放在RecordproviderName, rows下提供商名就是键本身estimatedCostUsd保留服务端逐模型估算值不需要单独的聚合接口提供商 KPI 直接对选中提供商的模型行成本求和即可避免引入第二套成本汇总类型。3.2 USD 估算格式器在 gui/src/provider-workspace/usage.ts 的formatTokenCount之后追加纯函数/** Format a USD cost estimate for display. Returns — when null. */ export function formatCostUsd(value: number | null | undefined, locale en): string { if (value null || value undefined || !Number.isFinite(value) || value 0) return \u2014; return ~$${new Intl.NumberFormat(locale, { minimumFractionDigits: 4, maximumFractionDigits: 4, }).format(value)}; }行为边界null/undefined/NaN/负数一律渲染为—em dash合法值固定四位小数并带~$估算前缀~表明这是估算而非账单值小数分隔符随 locale 走Intl.NumberFormat。对应测试固化在 tests/provider-workspace-data.test.tstest(USD estimates use fixed four-decimal localized formatting and reject invalid values, () { expect(formatCostUsd(undefined)).toBe(\u2014); expect(formatCostUsd(null)).toBe(\u2014); expect(formatCostUsd(Number.NaN)).toBe(\u2014); expect(formatCostUsd(-0.01)).toBe(\u2014); expect(formatCostUsd(0)).toBe(~$0.0000); expect(formatCostUsd(1_234.5, en)).toBe(~$1,234.5000); expect(formatCostUsd(1_234.5, de)).toBe(~$1.234,5000); });该测试锁死了估算前缀、locale 分隔符与非法值拒绝三项契约防止 Phase 2 消费该 helper 前被误删或退化。四、前端数据管线从 Shell 到 Usage 页签的四级穿透4.1 Shell在既有 fetch 中捕获模型行与汇总成本gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx 做四处改动核心是扩展现有单次 fetch 而不是新增请求DetailSlotData增加两个字段export interface DetailSlotData { usageTotals?: ProviderUsageTotals; quotaReport?: ProviderQuotaReportView; modelUsage?: ProviderModelUsageRow[]; // ← 新增 usageSummaryEstimatedCost?: number; // ← 新增 availableModels: string[]; selectedModels: string[]; modelsLoading: boolean; modelsLoadFailed: boolean; onRetryModels?: () void; }新增两个状态容器紧跟既有usageTotals声明之后const [usageModels, setUsageModels] useStateRecordstring, ProviderModelUsageRow[]({}); const [usageSummary, setUsageSummary] useState{ estimatedCostUsd?: number }({});替换后的用量 fetch 效果体注意请求 URL、取消守卫、静默失败语义全部保持不变useEffect(() { let cancelled false; fetch(${apiBase}/api/usage?range30d) .then(r r.ok ? r.json() : null) .then((data: { providers?: Array{ provider: string; requests: number; totalTokens?: number }; models?: ArrayProviderModelUsageRow { provider: string }; summary?: { estimatedCostUsd?: number }; } | null) { if (cancelled || !data) return; const byProvider: Recordstring, ProviderUsageTotals {}; for (const p of data.providers ?? []) byProvider[p.provider] { requests: p.requests, totalTokens: p.totalTokens }; const modelsByProvider: Recordstring, ProviderModelUsageRow[] {}; for (const row of data.models ?? []) { const { provider, estimatedCostUsd, ...model } row; (modelsByProvider[provider] ?? []).push({ ...model, estimatedCostUsd }); } setUsageTotals(byProvider); setUsageModels(modelsByProvider); setUsageSummary({ estimatedCostUsd: data.summary?.estimatedCostUsd }); }) .catch(() {}); return () { cancelled true; }; }, [apiBase]);这里有一个必须保留的解构细节const { provider, estimatedCostUsd, ...model } row;之后用{ ...model, estimatedCostUsd }重组——estimatedCostUsd被单独拆出再放回是为了确保数值 0 也被原样保留而不是被 rest 解构与显式字段的顺序差异吞掉。最后在detail?.(selectedItem, { ... })的对象字面量中把选中提供商的行与全局汇总成本透传给 detail 槽位detail?.(selectedItem, { usageTotals: usageTotals[selectedItem.name], quotaReport: quotaReports[selectedItem.name], modelUsage: usageModels[selectedItem.name], usageSummaryEstimatedCost: usageSummary.estimatedCostUsd, availableModels: availableModels[selectedItem.name] ?? [], selectedModels: selectedModels[selectedItem.name] ?? [], modelsLoading, modelsLoadFailed, onRetryModels: retryModels, }) ?? (在 Shell 边界一次性分组的好处详情面板只拿到自己提供商的行全局汇总值以显式命名的usageSummaryEstimatedCost传递语义上明确它是全局 summary 估算而非本提供商的成本。4.2 Providers 页显式 props 映射器转发gui/src/pages/Providers.tsx 的detailrender prop 是逐字段显式映射不做展开必须在quotaReport之后补两行usageTotals{data.usageTotals} quotaReport{data.quotaReport} modelUsage{data.modelUsage} usageSummaryEstimatedCost{data.usageSummaryEstimatedCost} availableModels{data.availableModels}缺了这一步Shell 捕获的新数据会在到达ProviderDetails之前被静默丢弃——这是 Phase 1 必须闭合的数据管线接缝。4.3 ProviderDetails只做 props 组合边界gui/src/components/provider-workspace/ProviderDetails.tsx 接收新 props 并转发给ProviderUsage自身不持有用量派生状态、不做任何成本计算{tab usage ( ProviderUsage item{item} usageTotals{usageTotals} quotaReport{quotaReport} modelUsage{modelUsage} usageSummaryEstimatedCost{usageSummaryEstimatedCost} / )}4.4 ProviderUsage接收 props 但不渲染编译闭环改动gui/src/components/provider-workspace/ProviderUsage.tsx 的改动只涉及 prop 契约TypeScript 会拒绝未声明的 JSX props因此组件签名必须接受新值用void显式消费渲染 JSX 保持逐字节不变Phase 2 渲染 KPI/表格时再移除两条void语句export default function ProviderUsage({ item, usageTotals, quotaReport, modelUsage, usageSummaryEstimatedCost, }: { item: WorkspaceItem; usageTotals?: ProviderUsageTotals; quotaReport?: ProviderQuotaReportView; modelUsage?: ProviderModelUsageRow[]; usageSummaryEstimatedCost?: number; }) { const t useT(); const { locale } useI18n(); const timeLabels relativeTimeLabelsFromT(t); const hasUsage usageTotals?.requests ! undefined; const quota accountQuotaFromReport(quotaReport); void item; void modelUsage; void usageSummaryEstimatedCost; return ( // …既有 JSXPhase 1 保持逐字节不变五、验收检查项9 条源级不变量实施完毕后在源码层面应逐条核验UsageModel与UsageProvider暴露增量可选estimatedCostUsdProviderModelUsageRow携带同一模型行字段且没有新增重复的成本汇总类型一条已定价的非 combo 请求其估算值同时进入全局 summary、模型行与提供商行纯未定价行保持可选字段缺省每次已定价的 combo 尝试只把自身成本记入自身模型/提供商桶合成的combo身份不得收到任何估算值Shell 仍然只发起一次/api/usage?range30d请求每条data.models记录按其provider键存储存储后的行不再含provider字段且estimatedCostUsd值含数值 0被保留选中没有任何模型行的提供商时得到modelUsage undefined不构造假的空用量记录——空态呈现由 Phase 2 决定summary.estimatedCostUsd 0被保留为0不被当作不可用失败/非 OK 的用量请求不触碰初始用量状态与现状一致Phase 1 中渲染的ProviderUsageJSX 保持不变。六、验证命令以下命令在仓库根目录Phase 1 实施后执行bun test --isolate tests/usage-summary.test.ts期望退出码0用量汇总测试通过覆盖按模型、按提供商、未定价行与 combo 尝试成本归属。bun test --isolate tests/provider-workspace-data.test.ts期望退出码0provider-workspace 数据测试全部通过含新增 USD 格式器用例。cd gui bun x tsc --noEmit期望退出码0完整的ProviderWorkspaceShell → Providers → ProviderDetails → ProviderUsageprops 链条通过类型检查。git diff --check -- \ src/usage/summary.ts \ tests/usage-summary.test.ts \ gui/src/components/provider-workspace/types.ts \ gui/src/provider-workspace/usage.ts \ tests/provider-workspace-data.test.ts \ gui/src/components/provider-workspace/ProviderWorkspaceShell.tsx \ gui/src/pages/Providers.tsx \ gui/src/components/provider-workspace/ProviderDetails.tsx \ gui/src/components/provider-workspace/ProviderUsage.tsx期望退出码0且无输出。Phase 1 不要求浏览器/视觉检查——因为渲染 JSX 与样式未变浏览器 QA 在 Phase 2 渲染成本 KPI 与模型表格时才成为必需。七、已知局限chatgpt/openai 键不匹配计划文档000_plan.md将其列为已接受的已知局限服务端baseProviderLabel会把chatgpt归一化为openai而工作区可能展示独立的chatgpt提供商此时以selectedItem.name为键的用量数据新旧数据均是会匹配不到用量显示为不可用。这是既有问题同样影响已有的 usage totals 显示不是本功能引入的回归带回归测试的 GUI 规范用量键 helper 被推迟到后续独立工作项。八、小结Phase 1 的价值在于以最小的侵入性建立了一条端到端类型安全、测试固化的成本数据链路服务端复用既有估算器完成逐模型/逐提供商聚合并遵守缺省即未定价、combo 按尝试归属的契约前端在单次 fetch 内完成捕获与分组经由 Shell → Providers → Details → Usage 四级 props 穿透最终由ProviderUsage以void占位完成编译闭环。Phase 2 的 UI 工作KPI、模型表格、i18n因此可以在一个数据完全就绪、渲染零变化的中间态之上安全展开。赞分享【免费下载链接】opencodexUniversal provider proxy for OpenAI Codex Claude Code — use any LLM (Claude, Gemini, Grok, DeepSeek, Ollama…) with Codex CLI, App, SDK, and Claude Code项目地址https://gitcode.com/gh_mirrors/ope/opencodex点击查看免费下载相关推荐opencodex 服务端大文件重构实战把 2322 行的 server.ts 拆成内聚模块的分阶段实施方法论opencodex 服务端大文件重构实战把 2322 行的 server.ts 拆成内聚模块的分阶段实施方法论 导读 本文以 opencodex 仓库中的一次解锁Java生态宝藏从零构建企业级知识图谱的技术架构深度剖析解锁Java生态宝藏从零构建企业级知识图谱的技术架构深度剖析 在当今数据爆炸的时代企业面临着海量信息孤岛和碎片化数据的严峻挑战。传统的数据管理方式已无法满足文档知识库CANN/pypto-gym Qwen3Next样例Qwen3Next 样例 Examples 本目录包含了 PyPTO Qwen3Next 模型的开发样例代码。我们对 Qwen3Next 的核心注意力机制进行了人工智能大模型算子库模型优化AI 技能CANNAscend上一篇如何快速免费导出QQ空间全部历史说说下一篇如何永久保存微信聊天记录WeChatMsg让你的数字记忆永不丢失创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表