Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/pricing.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ packages/shared/src/pricing/
| `currency: 'USD' \| 'CNY'` | 价格币种。Worker 端结算时按 `catalog.fx` 折算到 USD |
| `input_per_million` / `output_per_million` | 基础单价 / 1M tokens |
| `cached_input_per_million` | cache hit 价(Anthropic 叫 cache_read,Kimi 叫缓存命中) |
| `cache_write_per_million` | 不区分缓存时长的通用 cache write 价(如 OpenAI GPT-5.6) |
| `cache_write_5m_per_million` / `cache_write_1h_per_million` | Anthropic 风格 prompt caching write |
| `tiers?: PricingTier[]` | 阶梯定价:按 input token 数命中不同档位(Qwen / Gemini 2.5 Pro / GLM 等) |
| `effective_from` / `effective_to` | 价格生效区间(审计用) |
Expand All @@ -51,6 +52,7 @@ packages/shared/src/pricing/

```ts
'claude-opus-4-7-20260201': 'claude-opus-4-7' // 带日期后缀的版本号
'gpt-5.6': 'gpt-5.6-sol' // 官方系列别名 → Sol
'codex-auto-review': 'gpt-5.4' // 工具内部模型 → 实际推理模型
```

Expand Down Expand Up @@ -82,6 +84,8 @@ tiers: [
]
```

Codex scanner 会先按单次请求命中阶梯并累计 `costUSD`,同时携带 `pricingVersion`,确保同一天混合长短请求时仍按各自档位精确计费。CLI / Worker 仅在版本与当前 catalog 一致时采用该成本;旧版、版本不匹配或不含逐请求成本的聚合 breakdown 会用 `totalInputTokens / eventCount` 估算档位,并将 `costStatus` 标为 `estimated`。

> **限制**:Gemini 2.5 Flash 等模型还按 **output** 长度分档;GLM-4.7 同样有 input × output 双维度。当前实现只按 input 命中,output 分档暂用保守取低档(标了 `notes`)。

## 贡献新模型 / 修订单价
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@aiusage/cli",
"version": "1.7.1",
"version": "1.7.2",
"description": "Track and sync AI tool token usage across devices.",
"license": "MIT",
"private": false,
Expand Down
136 changes: 131 additions & 5 deletions packages/cli/pricing-catalog.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"version": "2026-06-22-codex-tier-v1",
"version": "2026-07-10-gpt-5.6-v1",
"fx": {
"CNY": 7.2
},
Expand All @@ -9,6 +9,7 @@
"claude-opus-4-6-20250301": "claude-opus-4-6",
"claude-haiku-4-5-20251001": "claude-haiku-4-5",
"claude-sonnet-4.6": "claude-sonnet-4-6",
"gpt-5.6": "gpt-5.6-sol",
"codex-auto-review": "gpt-5.4"
},
"providers": {
Expand Down Expand Up @@ -112,23 +113,134 @@
"openai": {
"codex": {
"models": {
"gpt-5.6-sol": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 5,
"cached_input_per_million": 0.5,
"cache_write_per_million": 6.25,
"output_per_million": 30,
"tiers": [
{
"threshold": 272000,
"input_per_million": 5,
"cached_input_per_million": 0.5,
"cache_write_per_million": 6.25,
"output_per_million": 30
},
{
"input_per_million": 10,
"cached_input_per_million": 1,
"cache_write_per_million": 12.5,
"output_per_million": 45
}
]
},
"gpt-5.6-terra": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 2.5,
"cached_input_per_million": 0.25,
"cache_write_per_million": 3.125,
"output_per_million": 15,
"tiers": [
{
"threshold": 272000,
"input_per_million": 2.5,
"cached_input_per_million": 0.25,
"cache_write_per_million": 3.125,
"output_per_million": 15
},
{
"input_per_million": 5,
"cached_input_per_million": 0.5,
"cache_write_per_million": 6.25,
"output_per_million": 22.5
}
]
},
"gpt-5.6-luna": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 1,
"cached_input_per_million": 0.1,
"cache_write_per_million": 1.25,
"output_per_million": 6,
"tiers": [
{
"threshold": 272000,
"input_per_million": 1,
"cached_input_per_million": 0.1,
"cache_write_per_million": 1.25,
"output_per_million": 6
},
{
"input_per_million": 2,
"cached_input_per_million": 0.2,
"cache_write_per_million": 2.5,
"output_per_million": 9
}
]
},
"gpt-5.5": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 5,
"cached_input_per_million": 0.5,
"output_per_million": 30
"output_per_million": 30,
"tiers": [
{
"threshold": 272000,
"input_per_million": 5,
"cached_input_per_million": 0.5,
"output_per_million": 30
},
{
"input_per_million": 10,
"cached_input_per_million": 1,
"output_per_million": 45
}
]
},
"gpt-5.5-pro": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 30,
"cached_input_per_million": null,
"output_per_million": 180
"output_per_million": 180,
"tiers": [
{
"threshold": 272000,
"input_per_million": 30,
"cached_input_per_million": null,
"output_per_million": 180
},
{
"input_per_million": 60,
"cached_input_per_million": null,
"output_per_million": 270
}
]
},
"gpt-5.4": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 2.5,
"cached_input_per_million": 0.25,
"output_per_million": 15
"output_per_million": 15,
"tiers": [
{
"threshold": 272000,
"input_per_million": 2.5,
"cached_input_per_million": 0.25,
"output_per_million": 15
},
{
"input_per_million": 5,
"cached_input_per_million": 0.5,
"output_per_million": 22.5
}
]
},
"gpt-5.4-mini": {
"currency": "USD",
Expand All @@ -144,9 +256,23 @@
},
"gpt-5.4-pro": {
"currency": "USD",
"notes": "prompts over 272K input tokens bill the full request at long-context rates",
"input_per_million": 30,
"cached_input_per_million": null,
"output_per_million": 180
"output_per_million": 180,
"tiers": [
{
"threshold": 272000,
"input_per_million": 30,
"cached_input_per_million": null,
"output_per_million": 180
},
{
"input_per_million": 60,
"cached_input_per_million": null,
"output_per_million": 270
}
]
},
"gpt-5.3-codex": {
"currency": "USD",
Expand Down
54 changes: 52 additions & 2 deletions packages/cli/src/__tests__/report.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,33 @@ describe('buildLocalReport', () => {
});

describe('calculateBreakdownCost', () => {
it('only trusts scanner cost when the selected catalog version matches', async () => {
const { calculateBreakdownCost } = await import('../report.js');
const { catalog } = await import('@aiusage/shared');
const breakdown = {
provider: 'openai' as const,
product: 'codex' as const,
channel: 'cli' as const,
model: 'gpt-5.6-sol',
project: '/tmp/project',
eventCount: 2,
inputTokens: 500_000,
cachedInputTokens: 0,
cacheWriteTokens: 0,
outputTokens: 20_000,
reasoningOutputTokens: 0,
costUSD: 5.25,
pricingVersion: catalog.version,
};

expect(calculateBreakdownCost(breakdown, new Set(), catalog)).toBe(5.25);

const warnings = new Set<string>();
const futureCatalog = { ...catalog, version: 'future-catalog' };
expect(calculateBreakdownCost(breakdown, warnings, futureCatalog)).toBe(3.1);
expect([...warnings]).toEqual(['gpt-5.6-sol 的阶梯价格已按每事件平均输入量估算。']);
});

it('prices local Codex GPT-5.5 usage', async () => {
const { calculateBreakdownCost } = await import('../report.js');
const warnings = new Set<string>();
Expand All @@ -113,7 +140,7 @@ describe('calculateBreakdownCost', () => {
reasoningOutputTokens: 0,
}, warnings);

expect(cost).toBe(20.5);
expect(cost).toBe(33.5);
expect([...warnings]).toEqual([]);
});

Expand All @@ -135,8 +162,31 @@ describe('calculateBreakdownCost', () => {
reasoningOutputTokens: 0,
}, warnings);

expect(cost).toBe(17.75);
expect(cost).toBe(28);
// codex-auto-review 是 catalog 里的显式 alias → gpt-5.4,按 exact 处理,不应有 warning
expect([...warnings]).toEqual([]);
});

it('estimates legacy aggregated GPT-5.6 usage from average per-event input', async () => {
const { calculateBreakdownCost } = await import('../report.js');
const warnings = new Set<string>();

const cost = calculateBreakdownCost({
provider: 'openai',
product: 'codex',
channel: 'cli',
model: 'gpt-5.6-sol',
project: '/tmp/project',
eventCount: 2,
inputTokens: 400_000,
cachedInputTokens: 0,
cacheWriteTokens: 0,
outputTokens: 20_000,
reasoningOutputTokens: 0,
costUSD: 0,
}, warnings);

expect(cost).toBe(2.6);
expect([...warnings]).toEqual(['gpt-5.6-sol 的阶梯价格已按每事件平均输入量估算。']);
});
});
14 changes: 11 additions & 3 deletions packages/cli/src/report.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { readdir, readFile } from 'node:fs/promises';
import { homedir } from 'node:os';
import { basename, join } from 'node:path';
import type { IngestBreakdown } from '@aiusage/shared';
import { calculateCost, type PricingCatalog } from '@aiusage/shared';
import { calculateCost, PRICING_VERSION, type PricingCatalog } from '@aiusage/shared';
import { scanDates } from './scan.js';
import { parseTs, dateKey } from './scanners/utils.js';
import type { PricingInfo } from './pricing.js';
Expand Down Expand Up @@ -578,7 +578,10 @@ export function calculateBreakdownCost(
warnings: Set<string>,
pricingCatalog?: PricingCatalog,
): number {
if (breakdown.costUSD != null && breakdown.costUSD > 0) {
const effectivePricingVersion = pricingCatalog?.version ?? PRICING_VERSION;
const sourceCostMatchesCatalog =
breakdown.pricingVersion == null || breakdown.pricingVersion === effectivePricingVersion;
if (breakdown.costUSD != null && breakdown.costUSD > 0 && sourceCostMatchesCatalog) {
return breakdown.costUSD;
}

Expand All @@ -594,13 +597,18 @@ export function calculateBreakdownCost(
cacheWrite1hTokens: breakdown.cacheWrite1hTokens,
outputTokens: breakdown.outputTokens,
},
pricingCatalog ? { catalog: pricingCatalog } : {},
{
...(pricingCatalog ? { catalog: pricingCatalog } : {}),
requestCount: breakdown.eventCount,
},
);

if (result.costStatus === 'unavailable') {
warnings.add(`${breakdown.provider}/${breakdown.product}/${breakdown.model} 暂无定价配置,已跳过成本估算。`);
} else if (result.costStatus === 'estimated' && result.resolvedModel && result.resolvedModel !== breakdown.model) {
warnings.add(`${breakdown.model} 已按 ${result.resolvedModel} 的公开单价估算。`);
} else if (result.costStatus === 'estimated' && result.matchedTierIndex !== undefined) {
warnings.add(`${breakdown.model} 的阶梯价格已按每事件平均输入量估算。`);
}

return result.estimatedCostUsd;
Expand Down
63 changes: 63 additions & 0 deletions packages/cli/src/scanners/__tests__/codex.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,70 @@ describe('Fix 1: non-cached input cost formula', () => {
});
});

describe('Codex tiered pricing', () => {
it('accumulates mixed short and long requests at their individual tiers', async () => {
const day = '2026-07-10';
const sessionDir = join(tmpDir, 'sessions', '2026', '07', '10');
const lines = [
...tokenCountEvent(
`${day}T10:00:00.000Z`,
{ input: 100_000, cached: 0, output: 10_000 },
{ input: 100_000, cached: 0, output: 10_000 },
'gpt-5.6-sol',
),
...tokenCountEvent(
`${day}T10:05:00.000Z`,
{ input: 400_000, cached: 0, output: 10_000 },
{ input: 500_000, cached: 0, output: 20_000 },
'gpt-5.6-sol',
),
];
await writeSession(sessionDir, 'rollout-test.jsonl', lines);

const [result] = await scanCodex(day, tmpDir);
expect(result.eventCount).toBe(2);
expect(result.inputTokens).toBe(500_000);
expect(result.outputTokens).toBe(20_000);
expect(result.costUSD).toBeCloseTo(5.25, 4);
expect(result.pricingVersion).toMatch(/^2026-07-10/);
});
});

describe('Codex service tier', () => {
it('adds priority suffix for GPT-5.6 Codex usage', async () => {
const day = '2026-07-10';
await writeFile(join(tmpDir, 'config.toml'), 'service_tier = "priority"\n');
const sessionDir = join(tmpDir, 'sessions', '2026', '07', '10');
const events = tokenCountEvent(
`${day}T10:00:00.000Z`,
{ input: 10000, cached: 8000, output: 500 },
{ input: 10000, cached: 8000, output: 500 },
'gpt-5.6-sol',
);
await writeSession(sessionDir, 'rollout-test.jsonl', events);

const results = await scanCodex(day, tmpDir);
expect(results).toHaveLength(1);
expect(results[0].model).toBe('gpt-5.6-sol-priority');
});

it('does not add fast suffix to GPT-5.6 while Codex Fast is unsupported', async () => {
const day = '2026-07-10';
await writeFile(join(tmpDir, 'config.toml'), 'service_tier = "fast"\n');
const sessionDir = join(tmpDir, 'sessions', '2026', '07', '10');
const events = tokenCountEvent(
`${day}T10:00:00.000Z`,
{ input: 10000, cached: 8000, output: 500 },
{ input: 10000, cached: 8000, output: 500 },
'gpt-5.6-sol',
);
await writeSession(sessionDir, 'rollout-test.jsonl', events);

const results = await scanCodex(day, tmpDir);
expect(results).toHaveLength(1);
expect(results[0].model).toBe('gpt-5.6-sol');
});

it('adds priority suffix for supported GPT-5.5 Codex usage', async () => {
const day = '2026-06-22';
await writeFile(join(tmpDir, 'config.toml'), 'service_tier = "priority"\n');
Expand Down
Loading