From e6984a2b561e032c75475228c058e2824edba813 Mon Sep 17 00:00:00 2001
From: zJay26 <120452926+zJay26@users.noreply.github.com>
Date: Wed, 7 Oct 2026 02:26:17 -0700
Subject: [PATCH] feat: add GPT-6.1 Sol and zero-cost Auto-review pricing for
v2.7.2
---
README.en.md | 11 +++--
README.md | 11 +++--
docs/fast-mode-accounting.md | 4 +-
docs/releases/v2.7.2.md | 31 ++++++++++++++
internal/app/app.go | 2 +-
internal/pricing/auto_review_test.go | 63 ++++++++++++++++++++++++++++
internal/pricing/catalog.go | 24 ++++++++++-
internal/pricing/catalog_test.go | 11 +++--
internal/pricing/estimator.go | 11 +++++
internal/pricing/estimator_test.go | 3 +-
internal/pricing/fast.go | 10 ++++-
internal/pricing/fast_test.go | 1 +
internal/server/server_test.go | 9 ++--
internal/usage/responses_test.go | 4 ++
internal/web/static/app.js | 4 +-
internal/web/static/i18n.js | 10 +++--
scripts/build.ps1 | 2 +-
scripts/build.sh | 2 +-
scripts/demo-api.js | 30 ++++++-------
scripts/mock-dashboard.mjs | 20 +++++----
tests/dashboard.spec.mjs | 27 ++++++++++--
tests/demo.spec.mjs | 27 +++++++++---
22 files changed, 255 insertions(+), 62 deletions(-)
create mode 100644 docs/releases/v2.7.2.md
create mode 100644 internal/pricing/auto_review_test.go
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 = `
${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 = `${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();