diff --git a/README.en.md b/README.en.md index 421fdb6..0ac47af 100644 --- a/README.en.md +++ b/README.en.md @@ -191,15 +191,16 @@ Display settings in the header use a more comfortable type scale by default and ### API-equivalent cost -The Dashboard shows regular, Fast, and all tokens, with a mode filter. Raw Fast tokens are never multiplied. Fast cost uses Standard base rates multiplied by ChatGPT Codex credit factors: 2.5 for Astra, the GPT-5.6 family, and GPT-5.5; 2 for GPT-5.4. Models without a confirmed factor remain unpriced. History is classified only from explicit evidence for the same turn; unconfirmed usage is provisionally regular. See the [Fast accounting, backfill, and API guide (Chinese)](docs/fast-mode-accounting.md). +The Dashboard shows regular, Fast, and all tokens, with a mode filter. Raw Fast tokens are never multiplied. Fast cost uses Standard base rates multiplied by ChatGPT Codex credit factors: 2.5 for GPT-6.1 Sol, GPT-6 Astra/Sol/Luna, the GPT-5.6 family, and GPT-5.5; 2 for GPT-5.4. Models without a confirmed factor remain unpriced. History is classified only from explicit evidence for the same turn; unconfirmed usage is provisionally regular. See the [Fast accounting, backfill, and API guide (Chinese)](docs/fast-mode-accounting.md). The estimator streams the normalized events that already passed source de-duplication and attribution filtering. It runs at query time, writes no cost data to SQLite, and leaves existing token totals unchanged. Arithmetic uses fixed-point nano-USD. Cached Input and Cache Write are removed from regular Input, and Reasoning is already included in Output, so neither is charged twice. -The bundled Standard text price catalog was updated on **2026-09-23**. All values are USD / 1M tokens: +The bundled Standard text price catalog was updated on **2026-10-07**. All values are USD / 1M tokens: | Model | Input | Cached | Cache Write | Output | |---|---:|---:|---:|---:| | [GPT-6 Astra](https://developers.openai.com/api/docs/models/gpt-6-astra) | 10.00 | 1.00 | 12.50 | 50.00 | +| [GPT-6.1 Sol](https://developers.openai.com/api/docs/models/gpt-6.1-sol) | 2.00 | 0.10 | 2.50 | 10.00 | | [GPT-6 Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) | 2.00 | 0.20 | 2.50 | 10.00 | | [GPT-6 Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) | 0.10 | 0.01 | 0.125 | 0.50 | | [GPT-5.6 Sol](https://developers.openai.com/api/docs/models/gpt-5.6-sol) | 5.00 | 0.50 | 6.25 | 30.00 | @@ -211,14 +212,16 @@ The bundled Standard text price catalog was updated on **2026-09-23**. All value | [GPT-5.3-Codex](https://developers.openai.com/api/docs/models/gpt-5.3-codex) | 1.75 | 0.175 | not published | 14.00 | | [GPT-5.2-Codex](https://developers.openai.com/api/docs/models/gpt-5.2-codex) | 1.75 | 0.175 | not published | 14.00 | -GPT-6 Astra/Sol/Luna and GPT-5.6 Cache Write use the official 1.25× regular Input rule. Local JSONL stores cumulative token activity and cannot reliably reconstruct the per-request boundaries used for API billing. The estimator uses the Standard base rates above, adjusts explicitly identified Fast usage, and does not infer long-context multipliers. The UI always shows estimated cost together with token pricing coverage; unknown models are never treated as zero-cost. +GPT-6.1 Sol, GPT-6 Astra/Sol/Luna and GPT-5.6 Cache Write use the official 1.25× regular Input rule. Local JSONL stores cumulative token activity and cannot reliably reconstruct the per-request boundaries used for API billing. The estimator uses the Standard base rates above, adjusts explicitly identified Fast usage, and does not infer long-context multipliers. The UI always shows estimated cost together with token pricing coverage; unknown models are never treated as zero-cost. + +Separate `codex-auto-review` usage defaults to zero cost while retaining token counts. This local accounting policy follows the Tibo statement reported by the maintainer; as of 2026-10-07, an explicit official public free-pricing statement has not been verified. It is not a published API price. Existing manual overrides still take precedence; remove that override to restore zero pricing. Only this exact model ID is covered: enabling Auto-review does not make main-model usage free. Historical estimates also use the current policy. Internal models can be explicitly mapped to one built-in public model or assigned custom rates in the Dashboard. Overrides take effect without restarting: ```json { "pricing_overrides": { - "codex-auto-review": { "alias_of": "gpt-5.6-luna" }, + "internal-review": { "alias_of": "gpt-6.1-sol" }, "internal-model": { "input_usd_per_million": "1.00", "cached_input_usd_per_million": "0.10", diff --git a/README.md b/README.md index 2f1b4db..b8e976e 100644 --- a/README.md +++ b/README.md @@ -191,15 +191,16 @@ Dashboard 固定为“概览 / 每日 / 明细”三个一级视图。概览默 ### 5. API 等价成本 -Dashboard 展示常规、Fast 和全部 token,并提供模式筛选。Fast 原始 token 不乘倍率;费用按 Standard 基价乘 ChatGPT Codex 额度倍率:Astra、GPT-5.6 系列、GPT-5.5 为 2.5 倍,GPT-5.4 为 2 倍。未知倍率的型号保留为未定价。历史模式仅凭同一 turn 的明确证据补齐,未确认部分暂归常规。详见 [Fast 模式口径、历史补齐与接口说明](docs/fast-mode-accounting.md)。 +Dashboard 展示常规、Fast 和全部 token,并提供模式筛选。Fast 原始 token 不乘倍率;费用按 Standard 基价乘 ChatGPT Codex 额度倍率:GPT-6.1 Sol、GPT-6 Astra/Sol/Luna、GPT-5.6 系列、GPT-5.5 为 2.5 倍,GPT-5.4 为 2 倍。未知倍率的型号保留为未定价。历史模式仅凭同一 turn 的明确证据补齐,未确认部分暂归常规。详见 [Fast 模式口径、历史补齐与接口说明](docs/fast-mode-accounting.md)。 费用在查询时流式读取已经过去重、归属规则筛选后的规范事件,不写入 SQLite,也不会改变原有 Token 统计。计算使用定点 nano-USD:Cached Input 与 Cache Write 从 Input 中扣除,Reasoning 已包含在 Output 中,不会重复收费。 -内置 Standard 文本价格目录更新于 **2026-09-23**,单位均为 USD / 1M Token: +内置 Standard 文本价格目录更新于 **2026-10-07**,单位均为 USD / 1M Token: | 模型 | Input | Cached | Cache Write | Output | |---|---:|---:|---:|---:| | [GPT-6 Astra](https://developers.openai.com/api/docs/models/gpt-6-astra) | 10.00 | 1.00 | 12.50 | 50.00 | +| [GPT-6.1 Sol](https://developers.openai.com/api/docs/models/gpt-6.1-sol) | 2.00 | 0.10 | 2.50 | 10.00 | | [GPT-6 Sol](https://developers.openai.com/api/docs/models/gpt-6-sol) | 2.00 | 0.20 | 2.50 | 10.00 | | [GPT-6 Luna](https://developers.openai.com/api/docs/models/gpt-6-luna) | 0.10 | 0.01 | 0.125 | 0.50 | | [GPT-5.6 Sol](https://developers.openai.com/api/docs/models/gpt-5.6-sol) | 5.00 | 0.50 | 6.25 | 30.00 | @@ -211,14 +212,16 @@ Dashboard 展示常规、Fast 和全部 token,并提供模式筛选。Fast 原 | [GPT-5.3-Codex](https://developers.openai.com/api/docs/models/gpt-5.3-codex) | 1.75 | 0.175 | 未公开 | 14.00 | | [GPT-5.2-Codex](https://developers.openai.com/api/docs/models/gpt-5.2-codex) | 1.75 | 0.175 | 未公开 | 14.00 | -GPT-6 Astra/Sol/Luna 和 GPT-5.6 的 Cache Write 使用官方“普通 Input 的 1.25 倍”规则。本机 JSONL 保存的是累计 Token 活动,不能可靠还原 API 账单中的逐请求边界;估算采用上表 Standard 基价,并对明确 Fast 的用量折算,不推断长上下文倍率。页面始终同时展示费用和 Token 定价覆盖率,未知模型不会被当成零费用。 +GPT-6.1 Sol、GPT-6 Astra/Sol/Luna 和 GPT-5.6 的 Cache Write 使用官方“普通 Input 的 1.25 倍”规则。本机 JSONL 保存的是累计 Token 活动,不能可靠还原 API 账单中的逐请求边界;估算采用上表 Standard 基价,并对明确 Fast 的用量折算,不推断长上下文倍率。页面始终同时展示费用和 Token 定价覆盖率,未知模型不会被当成零费用。 + +本软件将独立的 `codex-auto-review` 默认按零价估算,Token 仍正常统计。这是根据维护者收到的 Tibo 免费说明采用的本地计价规则;截至 2026-10-07,尚未找到明确的官方公开免费条款,不作为官方 API 报价。已有手动覆写继续优先,删除该项覆写后恢复默认零价。只匹配这一模型标识,不会使开启 Auto-review 的主模型免费;历史估算同样使用当前规则。 内部模型可以在 Dashboard 的“定价设置”中映射到一个明确的内置公开模型,或填写自定义单价。等价配置如下,保存后无需重启: ```json { "pricing_overrides": { - "codex-auto-review": { "alias_of": "gpt-5.6-luna" }, + "internal-review": { "alias_of": "gpt-6.1-sol" }, "internal-model": { "input_usd_per_million": "1.00", "cached_input_usd_per_million": "0.10", diff --git a/docs/fast-mode-accounting.md b/docs/fast-mode-accounting.md index 1e48eb3..a463cdf 100644 --- a/docs/fast-mode-accounting.md +++ b/docs/fast-mode-accounting.md @@ -18,11 +18,11 @@ schema v7 到 v8 为增量迁移,不清除账目或扫描游标,不要求全 ## 费用口径 -常规沿用既有 Standard API 价格及本机定价覆写。Fast 的相同 token 分类基础费用乘以 ChatGPT Codex 额度倍率:GPT-6 Astra/Sol/Luna、GPT-5.6 Sol/Terra/Luna、GPT-5.5 为 2.5;GPT-5.4 为 2。别名与日期快照通过模型目录解析,不将 mini、Spark、未知模型或未确认的自定义模型自动套入倍率。 +常规沿用既有 Standard API 价格及本机定价覆写。Fast 的相同 token 分类基础费用乘以 ChatGPT Codex 额度倍率:GPT-6.1 Sol、GPT-6 Astra/Sol/Luna、GPT-5.6 Sol/Terra/Luna、GPT-5.5 为 2.5;GPT-5.4 为 2。别名与日期快照通过模型目录解析,不将 mini、Spark、未知模型或未确认的自定义模型自动套入倍率。 界面统一称为“API 等价成本”,以一句话显示 API 价格和 Fast 额度规则的更新日期。计算仍为额度倍率加权的估算,不是实际 API Priority 账单,也不是账号额度扣除记录。未知单价、缺失缓存写入价格、缺少 Fast 倍率的部分继续显示为未定价。 -倍率核对于 2026-09-23:[OpenAI Fast 模式说明](https://learn.chatgpt.com/docs/agent-configuration/speed)。历史估算使用当前目录与倍率,不模拟历史账单。 +GPT-6.1 Sol 倍率核对于 2026-10-07,既有型号规则保留;这里使用订阅内含额度倍率(2.5x),不是购买 credits / API 的 Fast 倍率(2x)。来源:[OpenAI Fast 模式说明](https://learn.chatgpt.com/docs/agent-configuration/speed)。历史估算使用当前目录与倍率,不模拟历史账单。 ## API 与导出 diff --git a/docs/releases/v2.7.2.md b/docs/releases/v2.7.2.md new file mode 100644 index 0000000..f5c672c --- /dev/null +++ b/docs/releases/v2.7.2.md @@ -0,0 +1,31 @@ +# codex-usage v2.7.2 + +新增 GPT-6.1 Sol 定价,并将独立的 `codex-auto-review` 审批用量默认按零费用估算。Token 统计保持不变,无需为此次定价更新重建历史数据。 + +| 模型 | Input | Cached Input | Cache Write | Output | +|---|---:|---:|---:|---:| +| [GPT-6.1 Sol](https://developers.openai.com/api/docs/models/gpt-6.1-sol) | 2.00 | 0.10 | 2.50 | 10.00 | + +以上为 2026-10-07 核对的 Standard 短上下文文本价格,单位 USD / 1M Token。 + +- 支持 `gpt-6.1-sol`、有效日期后缀快照和本机模型映射。明确标记的 Fast 用量沿用 [Codex 订阅内含额度的 2.5× 倍率](https://learn.chatgpt.com/docs/agent-configuration/speed),与 API / 购买 credits 的 Fast 计费口径不同。 +- `codex-auto-review` 默认 Input、Cached Input、Cache Write、Output 均为 $0;常规和 Fast 视图均保留原始 Token,并计入已定价覆盖率。只匹配这一模型标识,开启 Auto-review 不会使主模型免费。 +- Auto-review 零价是根据维护者收到的 Tibo 免费说明采用的软件计价规则;截至 2026-10-07,尚未核实明确的官方公开免费条款,不作为官方 API 报价。 +- **已有 Auto-review 手动覆写继续优先。** 在定价设置中删除该项覆写或选择“使用默认零价”并保存,即可恢复默认零价。 +- Dashboard、synthetic Demo 和中英文文档同步更新;未知模型仍保持未定价,历史估算使用当前规则。 +- 固定两个日期回归测试的时区,避免因运行电脑的时区不同产生误报。 + +验证:完整 Go 测试、`go vet`、27 项 Dashboard / Demo 浏览器测试,覆盖费用汇总、Session 明细、Fast、零价覆盖率和旧覆写恢复默认后的缓存刷新。 + +--- + +Add GPT-6.1 Sol pricing and default separate `codex-auto-review` approval usage to zero cost. Raw token counts are retained; this pricing update requires no historical rebuild. + +- GPT-6.1 Sol Standard short-context prices per 1M tokens: **$2 input, $0.10 cached input, $2.50 cache write, $10 output**, verified on October 7, 2026. Supports dated snapshots, explicit aliases, and the existing 2.5× Codex included-subscription Fast weighting. +- Auto-review defaults to zero in both regular and Fast estimates, with tokens included in pricing coverage. This exact-model rule does not make main-model usage free. +- This is a local accounting policy based on a Tibo statement reported by the maintainer, not a verified published API price. +- **Existing manual Auto-review overrides still take precedence.** Remove the override or select “Use default zero pricing” and save to restore the default. +- Update the Dashboard, synthetic Demo, and bilingual documentation. Unknown models remain unpriced; historical estimates use current rules. +- Make two existing date regression tests independent of the host timezone. + +Validation: full Go tests, `go vet`, and 27 browser regressions covering aggregate and session estimates, Fast weighting, zero-cost coverage, and cache invalidation after resetting an override. diff --git a/internal/app/app.go b/internal/app/app.go index 4c7918b..4608fbc 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -31,7 +31,7 @@ import ( ) var ( - Version = "2.7.1" + Version = "2.7.2" Commit = "dev" BuildDate = "unknown" ) diff --git a/internal/pricing/auto_review_test.go b/internal/pricing/auto_review_test.go new file mode 100644 index 0000000..9118b3c --- /dev/null +++ b/internal/pricing/auto_review_test.go @@ -0,0 +1,63 @@ +package pricing + +import ( + "testing" + + "github.com/zJay26/codex-usage/internal/model" +) + +func TestAutoReviewDefaultZeroPreservesUsage(t *testing.T) { + for _, basis := range []string{Basis, FastWeightedBasis} { + for _, tier := range []string{"default", "fast"} { + for _, usage := range []model.TokenUsage{ + {Input: 1000, CachedInput: 200, CacheWriteInput: 100, Output: 100, ReasoningOutput: 50, Total: 1100}, + {Total: 1100}, + } { + b, err := NewBuilderForBasis(nil, basis) + if err != nil { + t.Fatal(err) + } + e := model.UsageEvent{Model: " CODEX-AUTO-REVIEW ", Usage: usage, ServiceMode: model.ModeFromTier(tier, "test")} + if err := b.Add(e); err != nil { + t.Fatal(err) + } + r := b.Report() + if r.Summary.USD != "0.000000000" || r.Summary.StandardBaseUSD != "0.000000000" || r.Summary.FastSurchargeUSD != "0.000000000" || r.Summary.PricedTokens != 1100 || r.Summary.UnpricedTokens != 0 || r.Summary.CoverageRatio != 1 || len(r.Summary.Reasons) != 0 { + t.Fatalf("%s/%s: %+v", basis, tier, r.Summary) + } + if len(r.Models) != 1 || !r.Models[0].Usage.Equal(usage) { + t.Fatalf("usage changed: %+v", r.Models) + } + } + } + } +} + +func TestAutoReviewExplicitOverrideStillWins(t *testing.T) { + for _, override := range []Override{ + {AliasOf: "gpt-6.1-sol"}, + {InputUSDPerMillion: "2", CachedInputUSDPerMillion: "0.1", CacheWriteInputUSDPerMillion: "2.5", OutputUSDPerMillion: "10"}, + } { + b, err := NewBuilder(map[string]Override{"codex-auto-review": override}) + if err != nil { + t.Fatal(err) + } + if err := b.Add(model.UsageEvent{Model: "codex-auto-review", Usage: model.TokenUsage{Input: 1000000, Total: 1000000}}); err != nil { + t.Fatal(err) + } + if got := b.Report().Summary; got.USD != "2.000000000" || got.UnpricedTokens != 0 { + t.Fatalf("override lost: %+v", got) + } + } +} + +func TestAutoReviewDoesNotHideInvalidUsageOrMakeMainModelFree(t *testing.T) { + bad, err := evaluateEvent(model.UsageEvent{Model: "codex-auto-review", Usage: model.TokenUsage{Input: 10, CachedInput: 11, Total: 10}}, nil) + if err != nil || bad.unpricedTokens != 10 || len(bad.reasons) != 1 || bad.reasons[0].Kind != "invalid_token_categories" { + t.Fatalf("invalid usage hidden: %+v %v", bad, err) + } + main, err := evaluateEvent(model.UsageEvent{Model: "gpt-6.1-sol", AgentType: "guardian", Usage: model.TokenUsage{Input: 1000000, Total: 1000000}}, nil) + if err != nil || main.regularNano != 2000000000 { + t.Fatalf("main model incorrectly free: %+v %v", main, err) + } +} diff --git a/internal/pricing/catalog.go b/internal/pricing/catalog.go index 96d4d78..ee33eb1 100644 --- a/internal/pricing/catalog.go +++ b/internal/pricing/catalog.go @@ -8,7 +8,7 @@ import ( ) const ( - CatalogAsOf = "2026-09-23" + CatalogAsOf = "2026-10-07" Currency = "USD" Basis = "current_standard_api_text_token_prices" ) @@ -45,6 +45,20 @@ type ResolvedRate struct { } var builtInCatalog = []CatalogEntry{ + { + // Local accounting policy, not a published API model price. Keep exact + // matching and allow explicit overrides from older installations. + Model: "codex-auto-review", DisplayName: "Codex Auto-review", + InputUSDPerMillion: "0.00", CachedInputUSDPerMillion: "0.00", + CacheWriteInputUSDPerMillion: "0.00", OutputUSDPerMillion: "0.00", + }, + { + Model: "gpt-6.1-sol", DisplayName: "GPT-6.1 Sol", + SnapshotPatterns: []string{"gpt-6.1-sol-YYYY-MM-DD"}, + InputUSDPerMillion: "2.00", CachedInputUSDPerMillion: "0.10", + CacheWriteInputUSDPerMillion: "2.50", OutputUSDPerMillion: "10.00", + Source: "https://developers.openai.com/api/docs/models/gpt-6.1-sol", + }, { Model: "gpt-6-astra", DisplayName: "GPT-6 Astra", SnapshotPatterns: []string{"gpt-6-astra-YYYY-MM-DD"}, @@ -147,7 +161,7 @@ func NormalizeOverrides(input map[string]Override) (map[string]Override, error) raw.CachedInputUSDPerMillion = strings.TrimSpace(raw.CachedInputUSDPerMillion) raw.CacheWriteInputUSDPerMillion = strings.TrimSpace(raw.CacheWriteInputUSDPerMillion) raw.OutputUSDPerMillion = strings.TrimSpace(raw.OutputUSDPerMillion) - if _, ok := resolveBuiltIn(model); ok { + if _, ok := resolveBuiltIn(model); ok && model != "codex-auto-review" { return nil, fmt.Errorf("内置官方模型 %q 不能被本机覆写", model) } if raw.AliasOf != "" { @@ -205,6 +219,12 @@ func Resolve(model string, overrides map[string]Override) (ResolvedRate, bool, e func resolveBuiltIn(model string) (CatalogEntry, bool) { model = strings.ToLower(strings.TrimSpace(model)) for _, entry := range builtInCatalog { + if len(entry.SnapshotPatterns) == 0 { + if model == entry.Model { + return entry, true + } + continue + } if modelMatches(model, entry.Model) { return entry, true } diff --git a/internal/pricing/catalog_test.go b/internal/pricing/catalog_test.go index 56c741e..7e01f96 100644 --- a/internal/pricing/catalog_test.go +++ b/internal/pricing/catalog_test.go @@ -24,6 +24,7 @@ func TestResolveGPT6SolAndLunaPricing(t *testing.T) { input, cached, cacheWrite, output int64 }{ {"gpt-6-sol", 2000, 200, 2500, 10000}, + {"gpt-6.1-sol", 2000, 100, 2500, 10000}, {"gpt-6-luna", 100, 10, 125, 500}, } { t.Run(tt.name, func(t *testing.T) { @@ -66,13 +67,17 @@ func TestResolveBuiltInAndVersionedSnapshot(t *testing.T) { func TestEveryCatalogModelAndSnapshotPatternResolvesExactly(t *testing.T) { for _, entry := range Catalog() { - for _, name := range []string{entry.Model, entry.Model + "-2026-07-31"} { + names := []string{entry.Model} + if len(entry.SnapshotPatterns) > 0 { + names = append(names, entry.Model+"-2026-07-31") + } + for _, name := range names { rate, ok, err := Resolve(name, nil) if err != nil || !ok || rate.CanonicalModel != entry.Model { t.Fatalf("%q did not resolve to %q: rate=%#v ok=%v err=%v", name, entry.Model, rate, ok, err) } } - if len(entry.SnapshotPatterns) == 0 { + if len(entry.SnapshotPatterns) == 0 && entry.Model != "codex-auto-review" { t.Fatalf("catalog entry %q does not publish its snapshot pattern", entry.Model) } } @@ -146,7 +151,7 @@ func TestNormalizeOverridesRejectsInvalidValues(t *testing.T) { } func TestModelMatchingDoesNotGuessFamilies(t *testing.T) { - for _, name := range []string{"gpt-6", "gpt-6-sol-preview", "gpt-6-luna-turbo", "gpt-6-luna-2026-02-30", "gpt-5.6-turbo", "gpt-5.4-pro", "codex-auto-review", ""} { + for _, name := range []string{"gpt-6", "gpt-6-sol-preview", "gpt-6-luna-turbo", "gpt-6-luna-2026-02-30", "gpt-5.6-turbo", "gpt-5.4-pro", "gpt-6.1-sol-preview", "codex-auto-review-2026-10-07", "internal-review", ""} { if _, ok, err := Resolve(name, nil); err != nil || ok { t.Fatalf("model %q should remain unpriced, ok=%v err=%v", name, ok, err) } diff --git a/internal/pricing/estimator.go b/internal/pricing/estimator.go index 308cd25..d7a4bb3 100644 --- a/internal/pricing/estimator.go +++ b/internal/pricing/estimator.go @@ -276,6 +276,17 @@ func evaluateEvent(event model.UsageEvent, overrides map[string]Override) (evalu return out, nil } if usage.Input == 0 && usage.Output == 0 && total > 0 { + // Zero-cost usage needs no category split; explicit paid overrides still + // require one. Invalid/contradictory categories are rejected above. + rate, found, err := Resolve(modelName, overrides) + if err != nil { + return out, err + } + if found && rate.InputNanoPerToken == 0 && rate.CachedNanoPerToken == 0 && + rate.CacheWriteNanoPerToken != nil && *rate.CacheWriteNanoPerToken == 0 && rate.OutputNanoPerToken == 0 { + out.pricedTokens = total + return out, nil + } out.unpricedTokens = total out.reasons = append(out.reasons, UnpricedReason{ Kind: "missing_token_categories", Model: modelName, Tokens: total, diff --git a/internal/pricing/estimator_test.go b/internal/pricing/estimator_test.go index 9ec1329..e3ff15c 100644 --- a/internal/pricing/estimator_test.go +++ b/internal/pricing/estimator_test.go @@ -60,6 +60,7 @@ func TestGPT6SolAndLunaCategoryEstimates(t *testing.T) { name, regular, cached, write, output, standard, fast string }{ {"gpt-6-sol", "0.001400000", "0.000040000", "0.000250000", "0.001000000", "0.002690000", "0.006725000"}, + {"gpt-6.1-sol", "0.001400000", "0.000020000", "0.000250000", "0.001000000", "0.002670000", "0.006675000"}, {"gpt-6-luna", "0.000070000", "0.000002000", "0.000012500", "0.000050000", "0.000134500", "0.000336250"}, } { t.Run(tt.name, func(t *testing.T) { @@ -126,7 +127,7 @@ func TestEvaluateEventReportsSpecificUnpricedReasons(t *testing.T) { }{ { name: "unknown model", - event: model.UsageEvent{Model: "codex-auto-review", Confidence: model.ConfidenceExact, Usage: model.TokenUsage{Input: 80, Output: 20, Total: 100}}, + event: model.UsageEvent{Model: "internal-review", Confidence: model.ConfidenceExact, Usage: model.TokenUsage{Input: 80, Output: 20, Total: 100}}, reason: "unknown_model", missed: 100, }, { diff --git a/internal/pricing/fast.go b/internal/pricing/fast.go index 32c407e..52eb151 100644 --- a/internal/pricing/fast.go +++ b/internal/pricing/fast.go @@ -8,7 +8,7 @@ import ( ) const FastWeightedBasis = "codex_fast_weighted" -const FastRulesAsOf = "2026-09-23" +const FastRulesAsOf = "2026-10-07" const FastRulesSource = "https://learn.chatgpt.com/docs/agent-configuration/speed" func ValidBasis(basis string) bool { @@ -40,7 +40,7 @@ func FastMultiplier(canonical string) (int64, int64, bool) { canonical = rate.CanonicalModel } switch canonical { - case "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5": + case "gpt-6.1-sol", "gpt-6-astra", "gpt-6-sol", "gpt-6-luna", "gpt-5.6-sol", "gpt-5.6-terra", "gpt-5.6-luna", "gpt-5.5": return 5, 2, true case "gpt-5.4": return 2, 1, true @@ -68,6 +68,12 @@ func evaluateWithBasis(event model.UsageEvent, overrides map[string]Override, ba if err != nil { return out, err } + // A resolved zero rate stays priced at zero even when no Fast multiplier + // exists (including explicit zero-cost local overrides). + if found && rate.InputNanoPerToken == 0 && rate.CachedNanoPerToken == 0 && + rate.CacheWriteNanoPerToken != nil && *rate.CacheWriteNanoPerToken == 0 && rate.OutputNanoPerToken == 0 { + return out, nil + } n, d, ok := FastMultiplier(rate.CanonicalModel) if !found || !ok { out.reasons = append(out.reasons, UnpricedReason{Kind: "fast_multiplier_missing", Model: event.Model, Tokens: out.pricedTokens, Detail: "没有已确认的 ChatGPT Codex Fast 额度倍率"}) diff --git a/internal/pricing/fast_test.go b/internal/pricing/fast_test.go index a105d1e..330bebc 100644 --- a/internal/pricing/fast_test.go +++ b/internal/pricing/fast_test.go @@ -20,6 +20,7 @@ func TestFastWeightedPricing(t *testing.T) { for _, tt := range []struct{ name, want string }{ {"gpt-6-astra", "150.000000000"}, {"gpt-5.6-sol", "87.500000000"}, {"gpt-6-sol", "30.000000000"}, {"gpt-6-luna", "1.500000000"}, + {"gpt-6.1-sol", "30.000000000"}, {"gpt-6.1-sol-2026-09-29", "30.000000000"}, {"gpt-6-sol-2026-09-22", "30.000000000"}, {"gpt-6-luna-2026-09-22", "1.500000000"}, {"gpt-5.6-terra", "35.000000000"}, {"gpt-5.6-luna", "3.500000000"}, {"gpt-5.5", "87.500000000"}, {"gpt-5.6", "87.500000000"}, diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 5bb0371..f956bcf 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -40,7 +40,7 @@ func TestDashboardAPIAndExport(t *testing.T) { } if _, err := st.InsertEvent(context.Background(), model.UsageEvent{ ID: "unpriced-fixture", Timestamp: at.Add(time.Minute), ObservedAt: at.Add(time.Minute), SessionID: "session-2", - Model: "codex-auto-review", Source: "codex_desktop", AgentType: "main", + Model: "internal-review", Source: "codex_desktop", AgentType: "main", Usage: model.TokenUsage{Input: 10, Output: 10, Total: 20}, Provenance: model.ProvenanceSessionJSONL, Confidence: model.ConfidenceExact, }, "unpriced-fixture.jsonl"); err != nil { @@ -229,7 +229,7 @@ func TestDashboardAPIAndExport(t *testing.T) { } request, _ := http.NewRequest(http.MethodPut, httpServer.URL+"/api/v1/pricing/overrides", strings.NewReader( - `{"overrides":{"codex-auto-review":{"alias_of":"gpt-5.6-luna"}}}`, + `{"overrides":{"internal-review":{"alias_of":"gpt-5.6-luna"}}}`, )) request.Header.Set("Content-Type", "application/json") request.Header.Set("Origin", httpServer.URL) @@ -239,7 +239,7 @@ func TestDashboardAPIAndExport(t *testing.T) { } payload, _ := io.ReadAll(response.Body) response.Body.Close() - if response.StatusCode != http.StatusOK || savedOverrides["codex-auto-review"].AliasOf != "gpt-5.6-luna" { + if response.StatusCode != http.StatusOK || savedOverrides["internal-review"].AliasOf != "gpt-5.6-luna" { t.Fatalf("pricing override was not saved: status=%d body=%s saved=%#v", response.StatusCode, payload, savedOverrides) } @@ -444,6 +444,9 @@ func TestParseSince(t *testing.T) { } func TestParseFilterDateUsesOneLocalNaturalDay(t *testing.T) { + previousLocal := time.Local + time.Local = time.FixedZone("test", 8*60*60) + t.Cleanup(func() { time.Local = previousLocal }) filter, err := parseFilter(url.Values{"date": {"2026-07-30"}}) if err != nil { t.Fatal(err) diff --git a/internal/usage/responses_test.go b/internal/usage/responses_test.go index 7ba35f3..9f78e3a 100644 --- a/internal/usage/responses_test.go +++ b/internal/usage/responses_test.go @@ -8,6 +8,7 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/zJay26/codex-usage/internal/model" "github.com/zJay26/codex-usage/internal/store" @@ -155,6 +156,9 @@ func TestResponseConflictRollsBackAndBadShapeDoesNotOwnTurn(t *testing.T) { } func TestResponseAttributionDateAndMode(t *testing.T) { + previousLocal := time.Local + time.Local = time.UTC + t.Cleanup(func() { time.Local = previousLocal }) st, home, path := accountingFixture(t) appendAccounting(t, path, accountingMeta+accountingTurn("one")+responseLine("scope-test", "one", "a", "2026-09-08T01:00:00Z", usage(10, 5, 0, 2, 1, 12), usage(10, 5, 0, 2, 1, 12))+accountingTurn("two")+responseLine("scope-test", "two", "b", "2026-09-09T01:00:00Z", usage(20, 0, 0, 3, 1, 23), usage(20, 0, 0, 3, 1, 23))) scanResponses(t, st, home) diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 1b161c4..1eef7cf 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1622,7 +1622,7 @@ function renderPricing(payload) { } function renderCatalog(catalog) { - $("#pricingCatalog").innerHTML = `
${escapeHTML(t("pricing.catalogModel"))}InputCachedWriteOutput
${catalog.map((entry) => `
${escapeHTML(entry.display_name)}${escapeHTML(entry.input_usd_per_million)}${escapeHTML(entry.cached_input_usd_per_million)}${escapeHTML(entry.cache_write_input_usd_per_million || "—")}${escapeHTML(entry.output_usd_per_million)}
`).join("")}`; + $("#pricingCatalog").innerHTML = `
${escapeHTML(t("pricing.catalogModel"))}InputCachedWriteOutput
${catalog.map((entry) => `
${entry.source ? `${escapeHTML(entry.display_name)}` : `${escapeHTML(entry.display_name)}`}${escapeHTML(entry.input_usd_per_million)}${escapeHTML(entry.cached_input_usd_per_million)}${escapeHTML(entry.cache_write_input_usd_per_million || "—")}${escapeHTML(entry.output_usd_per_million)}
`).join("")}`; } function appendOverrideCard(model, override = null, observedTokens = null) { @@ -1639,7 +1639,7 @@ function appendOverrideCard(model, override = null, observedTokens = null) { card.dataset.pricingModel = model; card.innerHTML = `
${escapeHTML(model)}${observedTokens == null ? escapeHTML(t("pricing.localCustom")) : escapeHTML(t("pricing.observed", { tokens: fullToken(observedTokens) }))}
- +
diff --git a/internal/web/static/i18n.js b/internal/web/static/i18n.js index c68087f..d2ccb8f 100644 --- a/internal/web/static/i18n.js +++ b/internal/web/static/i18n.js @@ -297,7 +297,7 @@ "filter.reset": "清空", "filter.apply": "应用筛选", "pricing.title": "API 等价成本定价", - "pricing.intro": "内置价格随程序版本保存,运行时不联网。内部模型保持未定价,除非你明确映射或填写单价。", + "pricing.intro": "内置价格随程序版本保存,运行时不联网。本软件将独立的 codex-auto-review 默认按 $0 估算,保留 Token;这是本软件计价规则,非公开 API 报价。已有手动覆写优先,删除后恢复零价。其他未知模型仍未定价。", "pricing.loading": "正在读取本机定价…", "pricing.addOverride": "新增模型覆写", "pricing.placeholder": "例如 internal-model", @@ -381,9 +381,10 @@ "pricing.localCustom": "本机自定义模型", "pricing.mode": "处理方式", "pricing.keepUnpriced": "保持未定价", + "pricing.defaultZero": "使用默认零价", "pricing.alias": "映射到内置模型", "pricing.custom": "填写自定义单价", - "pricing.publicModel": "内置公开模型", + "pricing.publicModel": "内置模型", "pricing.chooseModel": "请选择模型", "pricing.rateHint": "单位均为 USD / 1M Token;最多三位小数。别名只能指向内置模型。", "pricing.aliasMissing": "{model} 还没有选择内置模型", @@ -691,7 +692,7 @@ "filter.reset": "Clear", "filter.apply": "Apply filters", "pricing.title": "API-equivalent cost pricing", - "pricing.intro": "Built-in prices ship with the binary and require no runtime network access. Internal models remain unpriced unless you explicitly map them or enter rates.", + "pricing.intro": "Prices ship with the binary. Separate codex-auto-review usage defaults to $0 while retaining tokens: a local accounting policy, not a published API price. Existing overrides take precedence; remove one to restore zero pricing. Other unknown models remain unpriced.", "pricing.loading": "Loading local pricing…", "pricing.addOverride": "Add model override", "pricing.placeholder": "e.g. internal-model", @@ -775,9 +776,10 @@ "pricing.localCustom": "Local custom model", "pricing.mode": "Handling", "pricing.keepUnpriced": "Keep unpriced", + "pricing.defaultZero": "Use default zero pricing", "pricing.alias": "Map to a built-in model", "pricing.custom": "Enter custom rates", - "pricing.publicModel": "Built-in public model", + "pricing.publicModel": "Built-in model", "pricing.chooseModel": "Choose a model", "pricing.rateHint": "All rates are USD / 1M tokens with up to three decimal places. Aliases can target built-in models only.", "pricing.aliasMissing": "{model} does not have a built-in model selected", diff --git a/scripts/build.ps1 b/scripts/build.ps1 index 60cf4f7..ee16f37 100644 --- a/scripts/build.ps1 +++ b/scripts/build.ps1 @@ -1,5 +1,5 @@ param( - [string]$Version = "2.7.1" + [string]$Version = "2.7.2" ) $ErrorActionPreference = "Stop" diff --git a/scripts/build.sh b/scripts/build.sh index 6dd2e1e..255c4a6 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -3,7 +3,7 @@ set -euo pipefail project_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" dist="$project_root/dist" -version="${VERSION:-2.7.1}" +version="${VERSION:-2.7.2}" build_date="$(date -u +%Y-%m-%dT%H:%M:%SZ)" commit="$(git -C "$project_root" rev-parse --short HEAD 2>/dev/null || printf source)" if git -C "$project_root" rev-parse --is-inside-work-tree >/dev/null 2>&1 && diff --git a/scripts/demo-api.js b/scripts/demo-api.js index 9f5e526..c248105 100644 --- a/scripts/demo-api.js +++ b/scripts/demo-api.js @@ -21,7 +21,9 @@ total: 10_170_000 }; const catalog = [ + { model: "codex-auto-review", display_name: "Codex Auto-review", input_usd_per_million: "0.00", cached_input_usd_per_million: "0.00", cache_write_input_usd_per_million: "0.00", output_usd_per_million: "0.00", source: "" }, { model: "gpt-6-astra", display_name: "GPT-6 Astra", input_usd_per_million: "10.00", cached_input_usd_per_million: "1.00", cache_write_input_usd_per_million: "12.50", output_usd_per_million: "50.00", source: "#synthetic-pricing" }, + { model: "gpt-6.1-sol", display_name: "GPT-6.1 Sol", input_usd_per_million: "2.00", cached_input_usd_per_million: "0.10", cache_write_input_usd_per_million: "2.50", output_usd_per_million: "10.00", source: "#synthetic-pricing" }, { model: "gpt-6-sol", display_name: "GPT-6 Sol", input_usd_per_million: "2.00", cached_input_usd_per_million: "0.20", cache_write_input_usd_per_million: "2.50", output_usd_per_million: "10.00", source: "#synthetic-pricing" }, { model: "gpt-6-luna", display_name: "GPT-6 Luna", input_usd_per_million: "0.10", cached_input_usd_per_million: "0.01", cache_write_input_usd_per_million: "0.125", output_usd_per_million: "0.50", source: "#synthetic-pricing" }, { model: "gpt-5.6-sol", display_name: "GPT-5.6 Sol", input_usd_per_million: "5.00", cached_input_usd_per_million: "0.50", cache_write_input_usd_per_million: "6.25", output_usd_per_million: "30.00", source: "#synthetic-pricing" }, @@ -36,7 +38,7 @@ ] : [ { key: diagnostics ? "gpt-5.4" : "gpt-6-sol", share: .57, events: 71, sessions: 15 }, { key: diagnostics ? "gpt-5.6-terra" : "gpt-6-astra", share: .31, events: 32, sessions: 8 }, - { key: diagnostics ? "codex-auto-review" : "gpt-6-luna", share: .12, events: 11, sessions: 3 } + { key: diagnostics ? "internal-review" : "gpt-6-luna", share: .12, events: 11, sessions: 3 } ]; const agents = promo ? [ { key: "main", share: .71, events: 12_380, sessions: 214 }, @@ -230,9 +232,9 @@ estimate.standard_base_usd = (Number(estimate.standard_base_usd)+base).toFixed(9); const missingWrite = rate.cache_write_input_usd_per_million == null ? u.cache_write_input : 0; unpriced("cache_write_rate_missing", part.key, missingWrite); - const factor = {"gpt-6-astra":2.5,"gpt-6-sol":2.5,"gpt-6-luna":2.5,"gpt-5.6-sol":2.5,"gpt-5.6-terra":2.5,"gpt-5.6-luna":2.5,"gpt-5.5":2.5,"gpt-5.4":2}[alias]; - if (weighted && mode === "fast" && !factor) { unpriced("fast_multiplier_missing", part.key, total-missingWrite); continue; } - const multiplier = weighted && mode === "fast" ? factor : 1; + const factor = {"gpt-6-astra":2.5,"gpt-6.1-sol":2.5,"gpt-6-sol":2.5,"gpt-6-luna":2.5,"gpt-5.6-sol":2.5,"gpt-5.6-terra":2.5,"gpt-5.6-luna":2.5,"gpt-5.5":2.5,"gpt-5.4":2}[alias]; + if (weighted && mode === "fast" && !factor && base !== 0) { unpriced("fast_multiplier_missing", part.key, total-missingWrite); continue; } + const multiplier = weighted && mode === "fast" ? (factor || 1) : 1; for (const [key, amount] of Object.entries(categories)) estimate[key] = (Number(estimate[key])+amount*multiplier).toFixed(9); estimate.usd = (Number(estimate.usd)+base*multiplier).toFixed(9); estimate[mode === "fast" ? "fast_mode_usd" : "regular_mode_usd"] = (Number(estimate[mode === "fast" ? "fast_mode_usd" : "regular_mode_usd"])+base*multiplier).toFixed(9); @@ -260,7 +262,7 @@ for (const point of payload.points) for (const mode of ["regular","fast","unknown"]) payload.modes[mode]=addUsage(payload.modes[mode],point.modes[mode]); payload.summary = demoEstimate(payload.modes, url.searchParams.get("model"), url.searchParams.get("cost_basis") === "codex_fast_weighted"); payload.basis = url.searchParams.get("cost_basis") || "current_standard_api_text_token_prices"; - payload.fast_rules_as_of = "2026-09-23"; + payload.fast_rules_as_of = "2026-10-07"; } return payload; } @@ -329,7 +331,7 @@ const input = Math.round(total * .82); const output = total - input; const usage = { input, cached_input: Math.round(input * .56), cache_write_input: Math.round(input * .025), output, reasoning_output: Math.round(output * .37), total }; - const custom = Boolean(pricingOverrides["codex-auto-review"]); + const custom = Boolean(pricingOverrides["internal-review"]); const priced = Math.round(total * (custom ? 1 : .88)); const unpriced = total - priced; const usd = (priced / 1_000_000 * 3.18).toFixed(9); @@ -346,7 +348,7 @@ priced_tokens: priced, unpriced_tokens: unpriced, coverage_ratio: total ? priced / total : 0, - reasons: unpriced ? [{ kind: "unknown_model", model: "codex-auto-review", tokens: unpriced, detail: "Synthetic model has no public API rate or local override." }] : [] + reasons: unpriced ? [{ kind: "unknown_model", model: "internal-review", tokens: unpriced, detail: "Synthetic model has no public API rate or local override." }] : [] } }); } @@ -398,14 +400,14 @@ usd: totalCost.toFixed(9), regular_input_usd: totalCost.toFixed(9), cached_input_usd: "0.000000000", cache_write_input_usd: "0.000000000", output_usd: "0.000000000", priced_tokens: pricedTokens, unpriced_tokens: unpricedTokens, coverage_ratio: pricedTokens + unpricedTokens ? pricedTokens / (pricedTokens + unpricedTokens) : 0, - reasons: unpricedTokens ? [{ kind: "unknown_model", model: "codex-auto-review", tokens: unpricedTokens, detail: "Synthetic model has no public API rate or local override." }] : [] + reasons: unpricedTokens ? [{ kind: "unknown_model", model: "internal-review", tokens: unpricedTokens, detail: "Synthetic model has no public API rate or local override." }] : [] }; return { - basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-09-23", bucket: "day", summary: estimate, points, + basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-10-07", bucket: "day", summary: estimate, points, models: models.map((item) => { const itemUsage = scaledUsage(usage, item.share); if (promo) return { key: item.key, usage: itemUsage, fast_share: fastShareOf(points), estimate: {} }; - const unknown = item.key === "codex-auto-review" && !pricingOverrides[item.key]; + const unknown = item.key === "internal-review" && !pricingOverrides[item.key]; return { key: item.key, usage: itemUsage, estimate: { ...estimate, usd: unknown ? "0.000000000" : (totalCost * item.share).toFixed(9), priced_tokens: unknown ? 0 : itemUsage.total, unpriced_tokens: unknown ? itemUsage.total : 0, coverage_ratio: unknown ? 0 : 1, reasons: unknown ? estimate.reasons : [] } }; }) }; @@ -459,15 +461,15 @@ return { basis: "current_standard_api_text_token_prices", currency: "USD", - catalog_as_of: "2026-09-23", + catalog_as_of: "2026-10-07", catalog, overrides: pricingOverrides, - unpriced_models: !diagnostics || pricingOverrides["codex-auto-review"] ? [] : [{ key: "codex-auto-review", usage: scaledUsage(baseUsage, .12), events: 9, sessions: 3 }] + unpriced_models: !diagnostics || pricingOverrides["internal-review"] ? [] : [{ key: "internal-review", usage: scaledUsage(baseUsage, .12), events: 9, sessions: 3 }] }; } const promoUpdate = promo && params.get("update") === "available"; - const demoUpdates = promo ? { current_version: "2.7.1", latest_version: promoUpdate ? "2.8.0" : "2.7.1", release_url: "https://github.com/zJay26/codex-usage/releases", auto_check: true, available: promoUpdate, can_install: true, phase: "idle", download_dir: "~/Downloads/codex-usage", default_download_dir: "~/Downloads/codex-usage", custom_download_dir: "", can_open_download_dir: false } : { current_version: "2.7.1-demo", latest_version: "2.7.1", release_url: "https://github.com/zJay26/codex-usage/releases", auto_check: true, available: false, can_install: false, phase: "idle", download_dir: "C:\\Users\\Demo\\Downloads\\codex-usage", default_download_dir: "C:\\Users\\Demo\\Downloads\\codex-usage", custom_download_dir: "", can_open_download_dir: false }; + const demoUpdates = promo ? { current_version: "2.7.2", latest_version: promoUpdate ? "2.8.0" : "2.7.2", release_url: "https://github.com/zJay26/codex-usage/releases", auto_check: true, available: promoUpdate, can_install: true, phase: "idle", download_dir: "~/Downloads/codex-usage", default_download_dir: "~/Downloads/codex-usage", custom_download_dir: "", can_open_download_dir: false } : { current_version: "2.7.2-demo", latest_version: "2.7.2", release_url: "https://github.com/zJay26/codex-usage/releases", auto_check: true, available: false, can_install: false, phase: "idle", download_dir: "C:\\Users\\Demo\\Downloads\\codex-usage", default_download_dir: "C:\\Users\\Demo\\Downloads\\codex-usage", custom_download_dir: "", can_open_download_dir: false }; async function syntheticFetch(input, init = {}) { const raw = typeof input === "string" ? input : input.url; const url = new URL(raw, root.location.href); @@ -484,7 +486,7 @@ return jsonResponse(demoUpdates); } if (endpoint === "/api/v1/status") return jsonResponse({ - version: "2.7.1-demo", scanning: false, + version: "2.7.2-demo", scanning: false, status: { machine: promo ? { id: "workstation", label: "Workstation · Windows 11", hostname: "workstation", os: "windows", arch: "amd64" } : { id: "synthetic-machine", label: "Synthetic Windows · demo", hostname: "synthetic-host", os: "windows", arch: "amd64" }, last_scan: now.toISOString(), accounting_mode: "jsonl_only", otel_active: false, diff --git a/scripts/mock-dashboard.mjs b/scripts/mock-dashboard.mjs index 574aeaf..f29c011 100644 --- a/scripts/mock-dashboard.mjs +++ b/scripts/mock-dashboard.mjs @@ -20,7 +20,9 @@ const agents = [ ]; let pricingOverrides = {}; const catalog = [ + { model: "codex-auto-review", display_name: "Codex Auto-review", input_usd_per_million: "0.00", cached_input_usd_per_million: "0.00", cache_write_input_usd_per_million: "0.00", output_usd_per_million: "0.00", source: "" }, { model: "gpt-6-astra", display_name: "GPT-6 Astra", input_usd_per_million: "10.00", cached_input_usd_per_million: "1.00", cache_write_input_usd_per_million: "12.50", output_usd_per_million: "50.00", source: "https://developers.openai.com/api/docs/models/gpt-6-astra" }, + { model: "gpt-6.1-sol", display_name: "GPT-6.1 Sol", input_usd_per_million: "2.00", cached_input_usd_per_million: "0.10", cache_write_input_usd_per_million: "2.50", output_usd_per_million: "10.00", source: "https://developers.openai.com/api/docs/models/gpt-6.1-sol" }, { model: "gpt-6-sol", display_name: "GPT-6 Sol", input_usd_per_million: "2.00", cached_input_usd_per_million: "0.20", cache_write_input_usd_per_million: "2.50", output_usd_per_million: "10.00", source: "https://developers.openai.com/api/docs/models/gpt-6-sol" }, { model: "gpt-6-luna", display_name: "GPT-6 Luna", input_usd_per_million: "0.10", cached_input_usd_per_million: "0.01", cache_write_input_usd_per_million: "0.125", output_usd_per_million: "0.50", source: "https://developers.openai.com/api/docs/models/gpt-6-luna" }, { model: "gpt-5.6-sol", display_name: "GPT-5.6 Sol", input_usd_per_million: "5.00", cached_input_usd_per_million: "0.50", cache_write_input_usd_per_million: "6.25", output_usd_per_million: "30.00", source: "https://developers.openai.com/api/docs/models/gpt-5.6-sol" }, @@ -65,22 +67,22 @@ function costEstimate(url) { const input = Math.round(total * .82); const output = total - input; const dailyUsage = { input, cached_input: Math.round(input * .56), cache_write_input: Math.round(input * .025), output, reasoning_output: Math.round(output * .37), total }; - const priced = Math.round(total * (pricingOverrides["codex-auto-review"] ? 1 : .916)); + const priced = Math.round(total * (pricingOverrides["internal-review"] ? 1 : .916)); const unpriced = total - priced; const usd = total ? (total / 1_000_000 * 3.18).toFixed(9) : "0.000000000"; - points.push({ date: localDateKey(date), time: new Date(date).toISOString(), usage: dailyUsage, estimate: { usd, regular_input_usd: usd, cached_input_usd: "0.000000000", cache_write_input_usd: "0.000000000", output_usd: "0.000000000", priced_tokens: priced, unpriced_tokens: unpriced, coverage_ratio: total ? priced / total : 0, reasons: unpriced ? [{ kind: "unknown_model", model: "codex-auto-review", tokens: unpriced, detail: "没有公开 API 单价或本机定价覆写" }] : [] } }); + points.push({ date: localDateKey(date), time: new Date(date).toISOString(), usage: dailyUsage, estimate: { usd, regular_input_usd: usd, cached_input_usd: "0.000000000", cache_write_input_usd: "0.000000000", output_usd: "0.000000000", priced_tokens: priced, unpriced_tokens: unpriced, coverage_ratio: total ? priced / total : 0, reasons: unpriced ? [{ kind: "unknown_model", model: "internal-review", tokens: unpriced, detail: "没有公开 API 单价或本机定价覆写" }] : [] } }); } const totalUsage = points.reduce((sum, point) => Object.fromEntries(Object.keys(point.usage).map((key) => [key, (sum[key] || 0) + point.usage[key]])), {}); const pricedTokens = points.reduce((sum, point) => sum + point.estimate.priced_tokens, 0); const unpricedTokens = points.reduce((sum, point) => sum + point.estimate.unpriced_tokens, 0); const totalCost = points.reduce((sum, point) => sum + Number(point.estimate.usd), 0); - const estimate = { usd: totalCost.toFixed(9), regular_input_usd: totalCost.toFixed(9), cached_input_usd: "0.000000000", cache_write_input_usd: "0.000000000", output_usd: "0.000000000", priced_tokens: pricedTokens, unpriced_tokens: unpricedTokens, coverage_ratio: pricedTokens + unpricedTokens ? pricedTokens / (pricedTokens + unpricedTokens) : 0, reasons: unpricedTokens ? [{ kind: "unknown_model", model: "codex-auto-review", tokens: unpricedTokens, detail: "没有公开 API 单价或本机定价覆写" }] : [] }; + const estimate = { usd: totalCost.toFixed(9), regular_input_usd: totalCost.toFixed(9), cached_input_usd: "0.000000000", cache_write_input_usd: "0.000000000", output_usd: "0.000000000", priced_tokens: pricedTokens, unpriced_tokens: unpricedTokens, coverage_ratio: pricedTokens + unpricedTokens ? pricedTokens / (pricedTokens + unpricedTokens) : 0, reasons: unpricedTokens ? [{ kind: "unknown_model", model: "internal-review", tokens: unpricedTokens, detail: "没有公开 API 单价或本机定价覆写" }] : [] }; return { - basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-09-23", bucket: "day", summary: estimate, points, + basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-10-07", bucket: "day", summary: estimate, points, models: [ { key: "gpt-5.4", usage: { ...totalUsage, total: Math.round(totalUsage.total * .63) }, estimate: { ...estimate, usd: (totalCost * .71).toFixed(9), priced_tokens: Math.round(totalUsage.total * .63), unpriced_tokens: 0, coverage_ratio: 1, reasons: [] } }, { key: "gpt-5.6-terra", usage: { ...totalUsage, total: Math.round(totalUsage.total * .26) }, estimate: { ...estimate, usd: (totalCost * .25).toFixed(9), priced_tokens: Math.round(totalUsage.total * .26), unpriced_tokens: 0, coverage_ratio: 1, reasons: [] } }, - { key: "codex-auto-review", usage: { ...totalUsage, total: Math.round(totalUsage.total * .11) }, estimate: { ...estimate, usd: pricingOverrides["codex-auto-review"] ? (totalCost * .04).toFixed(9) : "0.000000000", priced_tokens: pricingOverrides["codex-auto-review"] ? Math.round(totalUsage.total * .11) : 0, unpriced_tokens: pricingOverrides["codex-auto-review"] ? 0 : Math.round(totalUsage.total * .11), coverage_ratio: pricingOverrides["codex-auto-review"] ? 1 : 0 } } + { key: "internal-review", usage: { ...totalUsage, total: Math.round(totalUsage.total * .11) }, estimate: { ...estimate, usd: pricingOverrides["internal-review"] ? (totalCost * .04).toFixed(9) : "0.000000000", priced_tokens: pricingOverrides["internal-review"] ? Math.round(totalUsage.total * .11) : 0, unpriced_tokens: pricingOverrides["internal-review"] ? 0 : Math.round(totalUsage.total * .11), coverage_ratio: pricingOverrides["internal-review"] ? 1 : 0 } } ] }; } @@ -88,7 +90,7 @@ function costEstimate(url) { const server = http.createServer(async (request, response) => { const url = new URL(request.url, `http://127.0.0.1:${port}`); if (url.pathname === "/api/v1/status") return json(response, { - version: "2.7.1-preview", scanning: false, + version: "2.7.2-preview", scanning: false, status: { machine: { id: "62c0172d-36c4-4ec9-a074-02b9ec2b45e1", label: "WORKSTATION-19 · windows", hostname: "WORKSTATION-19", os: "windows", arch: "amd64" }, last_scan: now.toISOString(), accounting_mode: "jsonl_only", otel_active: false, @@ -104,15 +106,15 @@ const server = http.createServer(async (request, response) => { } if (url.pathname === "/api/v1/cost-estimate") return json(response, costEstimate(url)); if (url.pathname === "/api/v1/pricing" && request.method === "GET") return json(response, { - basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-09-23", catalog, overrides: pricingOverrides, - unpriced_models: pricingOverrides["codex-auto-review"] ? [] : [{ key: "codex-auto-review", usage: { ...usage, total: 438_000 }, events: 9, sessions: 3 }] + basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-10-07", catalog, overrides: pricingOverrides, + unpriced_models: pricingOverrides["internal-review"] ? [] : [{ key: "internal-review", usage: { ...usage, total: 438_000 }, events: 9, sessions: 3 }] }); if (url.pathname === "/api/v1/pricing/overrides" && request.method === "PUT") { let body = ""; for await (const chunk of request) body += chunk; try { pricingOverrides = JSON.parse(body).overrides || {}; - return json(response, { basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-09-23", catalog, overrides: pricingOverrides, unpriced_models: pricingOverrides["codex-auto-review"] ? [] : [{ key: "codex-auto-review", usage: { ...usage, total: 438_000 }, events: 9, sessions: 3 }] }); + return json(response, { basis: "current_standard_api_text_token_prices", currency: "USD", catalog_as_of: "2026-10-07", catalog, overrides: pricingOverrides, unpriced_models: pricingOverrides["internal-review"] ? [] : [{ key: "internal-review", usage: { ...usage, total: 438_000 }, events: 9, sessions: 3 }] }); } catch { return json(response, { error: "无效请求体" }, 400); } diff --git a/tests/dashboard.spec.mjs b/tests/dashboard.spec.mjs index bf8d77d..516c06d 100644 --- a/tests/dashboard.spec.mjs +++ b/tests/dashboard.spec.mjs @@ -851,8 +851,10 @@ test("custom minute range queries real event totals, validates bounds, and uses } finally { await context.close(); } }); -test("GPT-6 Sol and Luna price recorded usage and appear in the catalog", async ({ page }, testInfo) => { +test("GPT-6.1 Sol and Auto-review pricing agree across the catalog, summary and sessions", async ({ page }, testInfo) => { const cases = [ + { model: "codex-auto-review", recorded: "codex-auto-review", display: "Codex Auto-review", rates: ["0.00", "0.00", "0.00", "0.00"], standard: "0.000000000", fast: "0.000000000" }, + { model: "gpt-6.1-sol", recorded: "gpt-6.1-sol-2026-09-29", display: "GPT-6.1 Sol", rates: ["2.00", "0.10", "2.50", "10.00"], standard: "0.002670000", fast: "0.006675000" }, { model: "gpt-6-sol", recorded: "gpt-6-sol", display: "GPT-6 Sol", rates: ["2.00", "0.20", "2.50", "10.00"], standard: "0.002690000", fast: "0.006725000" }, { model: "gpt-6-luna", recorded: "gpt-6-luna-2026-09-22", display: "GPT-6 Luna", rates: ["0.10", "0.01", "0.125", "0.50"], standard: "0.000134500", fast: "0.000336250" } ]; @@ -900,10 +902,29 @@ test("GPT-6 Sol and Luna price recorded usage and appear in the catalog", async const card = page.locator('[data-pricing-model="e2e-gpt6-alias"]'); await card.locator("[data-rate-mode]").selectOption("alias"); for (const item of cases) { - const row = page.locator("#pricingCatalog .catalog-row").filter({ has: page.getByRole("link", { name: item.display, exact: true }) }); - await expect(row.locator("span")).toHaveText(item.rates); + const row = page.locator("#pricingCatalog .catalog-row").filter({ has: page.getByText(item.display, { exact: true }) }); + await expect(row.locator("span")).toHaveText(item.model === "codex-auto-review" ? [item.display, ...item.rates] : item.rates); await card.locator("[data-alias]").selectOption(item.model); await expect(card.locator("[data-alias]")).toHaveValue(item.model); } await page.screenshot({ path: testInfo.outputPath("gpt6-pricing-catalog.png") }); + + // An older explicit Auto-review alias remains valid; resetting it in the UI + // restores zero pricing and invalidates cached cost/session estimates. + const override = await page.request.put(`${baseURL}/api/v1/pricing/overrides`, { + headers: { Origin: baseURL }, data: { overrides: { "codex-auto-review": { alias_of: "gpt-6.1-sol" } } } + }); + expect(override.ok()).toBeTruthy(); + expect((await get("cost-estimate?model=codex-auto-review")).summary.usd).toBe("0.002670000"); + await page.goto(dashboardURL, { waitUntil: "networkidle" }); + await page.locator("#pricingButton").click(); + const autoReview = page.locator('[data-pricing-model="codex-auto-review"]'); + await expect(autoReview.locator('[data-rate-mode] option[value="unpriced"]')).toHaveText("使用默认零价"); + await autoReview.locator("[data-rate-mode]").selectOption("unpriced"); + await page.locator("#savePricing").click(); + await expect(page.locator("#pricingDialog")).not.toBeVisible(); + expect((await get("cost-estimate?model=codex-auto-review")).summary.usd).toBe("0.000000000"); + const resetSessions = await get("session-estimates?model=codex-auto-review&cost_basis=codex_fast_weighted"); + expect(resetSessions.items[0].estimate.usd).toBe("0.000000000"); + expect(resetSessions.items[0].estimate.priced_tokens).toBe(1100); }); diff --git a/tests/demo.spec.mjs b/tests/demo.spec.mjs index f376295..1547bd1 100644 --- a/tests/demo.spec.mjs +++ b/tests/demo.spec.mjs @@ -142,13 +142,28 @@ test("synthetic demo supports minute ranges with matching summary and daily tota await expect(page.locator("#customRangeError")).toBeHidden(); }); +test("synthetic Auto-review stays zero-priced in regular and Fast modes", async ({ page }) => { + await page.goto(`${baseURL}?lang=en`, { waitUntil: "networkidle" }); + const reports = await page.evaluate(async () => Promise.all(["", "&cost_basis=codex_fast_weighted"].map(async (basis) => { + const response = await fetch(`api/v1/cost-estimate?model=codex-auto-review&mode=fast${basis}`); + return response.json(); + }))); + for (const report of reports) { + expect(report.summary.usd).toBe("0.000000000"); + expect(report.summary.priced_tokens).toBeGreaterThan(0); + expect(report.summary.unpriced_tokens).toBe(0); + expect(report.summary.coverage_ratio).toBe(1); + } +}); + test("synthetic catalog supports GPT-6 aliases and Fast estimates", async ({ page }) => { await page.goto(`${baseURL}?lang=en&scenario=diagnostics`, { waitUntil: "networkidle" }); await page.locator("#pricingButton").click(); await page.locator(".catalog-disclosure summary").click(); - const card = page.locator('[data-pricing-model="codex-auto-review"]'); + const card = page.locator('[data-pricing-model="internal-review"]'); await card.locator("[data-rate-mode]").selectOption("alias"); for (const item of [ + { model: "gpt-6.1-sol", display: "GPT-6.1 Sol", rates: ["2.00", "0.10", "2.50", "10.00"] }, { model: "gpt-6-sol", display: "GPT-6 Sol", rates: ["2.00", "0.20", "2.50", "10.00"] }, { model: "gpt-6-luna", display: "GPT-6 Luna", rates: ["0.10", "0.01", "0.125", "0.50"] } ]) { @@ -157,9 +172,9 @@ test("synthetic catalog supports GPT-6 aliases and Fast estimates", async ({ pag await card.locator("[data-alias]").selectOption(item.model); await expect(card.locator("[data-alias]")).toHaveValue(item.model); const result = await page.evaluate(async (model) => { - await fetch("api/v1/pricing/overrides", { method: "PUT", body: JSON.stringify({ overrides: { "codex-auto-review": { alias_of: model } } }) }); - const base = await (await fetch("api/v1/cost-estimate?model=codex-auto-review&mode=fast")).json(); - const fast = await (await fetch("api/v1/cost-estimate?model=codex-auto-review&mode=fast&cost_basis=codex_fast_weighted")).json(); + await fetch("api/v1/pricing/overrides", { method: "PUT", body: JSON.stringify({ overrides: { "internal-review": { alias_of: model } } }) }); + const base = await (await fetch("api/v1/cost-estimate?model=internal-review&mode=fast")).json(); + const fast = await (await fetch("api/v1/cost-estimate?model=internal-review&mode=fast&cost_basis=codex_fast_weighted")).json(); return { base: base.summary, fast: fast.summary }; }, item.model); expect(Number(result.base.usd)).toBeGreaterThan(0); @@ -182,9 +197,9 @@ test("language priority, persistence, ARIA, pricing, scan, theme, and mobile lay expect(new URL(page.url()).searchParams.get("lang")).toBe("zh-CN"); await page.locator("#pricingButton").click(); - await page.locator("#newOverrideModel").fill("codex-auto-review"); + await page.locator("#newOverrideModel").fill("internal-review"); await page.locator("#addOverride").click(); - const card = page.locator('[data-pricing-model="codex-auto-review"]'); + const card = page.locator('[data-pricing-model="internal-review"]'); await card.locator("[data-rate-mode]").selectOption("alias"); await card.locator("[data-alias]").selectOption("gpt-5.4"); await page.locator("#savePricing").click();