Skip to content
Open
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
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,19 @@ versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

### Added

- **Model-aware pricing** (`claudeStatus.pricing.useModelPricing`, default `true`) —
cost is now computed per JSONL entry using built-in rates for the entry's
`message.model` (Opus 4.5+ $5/$25, legacy Opus ≤4.1 $15/$75, Sonnet $3/$15,
Haiku $1/$5, Fable/Mythos 5 $10/$50 per 1M tokens, with matching cache rates).
Previously every entry was priced at the flat Sonnet-based `claudeStatus.pricing.*`
rates, overestimating Haiku usage 3× and underestimating Opus usage up to 5×.
Unrecognized models — and all entries when the setting is disabled — fall back
to the manual `claudeStatus.pricing.*` rates.
- **Per-model cost breakdown in dashboard** — the token breakdown section now lists
the 5h-window cost per model (e.g. `sonnet-4-5: $0.42`, `haiku-4-5: $0.03`).

### Performance

- **Skip stale JSONL files by mtime** — the 60-second status bar poll and the
Expand Down
1 change: 1 addition & 0 deletions README.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@ npm run package # → vscode-claude-status-*.vsix
| `claudeStatus.heatmap.days` | `30 \| 60 \| 90` | `90` | 使用ヒートマップに表示する日数 |
| `claudeStatus.credentials.path` | `string \| null` | `null` | カスタム認証情報ファイルパス |
| `claudeStatus.claudeProvider` | `"auto"` \| `"claude-ai"` \| `"aws-bedrock"` \| `"api-key"` | `"auto"` | プロバイダータイプ(自動検出または明示的指定) |
| `claudeStatus.pricing.useModelPricing` | `boolean` | `true` | モデル別の組み込み料金(Opus/Sonnet/Haiku)で正確なコストを計算。下記の手動料金はフォールバック |
| `claudeStatus.pricing.inputPerMillion` | `number` | `3.00` | 入力トークン 1M あたりの USD 単価 |
| `claudeStatus.pricing.outputPerMillion` | `number` | `15.00` | 出力トークン 1M あたりの USD 単価 |
| `claudeStatus.pricing.cacheReadPerMillion` | `number` | `0.30` | キャッシュ読み取りトークン 1M あたりの USD 単価 |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ All settings are under the `claudeStatus` namespace in VS Code Settings.
| `claudeStatus.heatmap.days` | `30 \| 60 \| 90` | `90` | Days shown in usage heatmap |
| `claudeStatus.credentials.path` | `string \| null` | `null` | Custom credentials file path |
| `claudeStatus.claudeProvider` | `"auto"` \| `"claude-ai"` \| `"aws-bedrock"` \| `"api-key"` | `"auto"` | Provider type (auto-detect or explicit) |
| `claudeStatus.pricing.useModelPricing` | `boolean` | `true` | Use built-in per-model rates (Opus/Sonnet/Haiku) for accurate costs; manual rates below are the fallback |
| `claudeStatus.pricing.inputPerMillion` | `number` | `3.00` | USD per 1M input tokens |
| `claudeStatus.pricing.outputPerMillion` | `number` | `15.00` | USD per 1M output tokens |
| `claudeStatus.pricing.cacheReadPerMillion` | `number` | `0.30` | USD per 1M cache-read tokens |
Expand Down
1 change: 1 addition & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,7 @@ npm run package # → vscode-claude-status-*.vsix
| `claudeStatus.heatmap.days` | `30 \| 60 \| 90` | `90` | 使用热力图显示的天数 |
| `claudeStatus.credentials.path` | `string \| null` | `null` | 自定义凭证文件路径 |
| `claudeStatus.claudeProvider` | `"auto"` \| `"claude-ai"` \| `"aws-bedrock"` \| `"api-key"` | `"auto"` | 提供商类型(自动检测或显式指定) |
| `claudeStatus.pricing.useModelPricing` | `boolean` | `true` | 使用内置按模型费率(Opus/Sonnet/Haiku)以获得准确成本;下方手动费率为回退值 |
| `claudeStatus.pricing.inputPerMillion` | `number` | `3.00` | 每百万输入令牌的 USD 单价 |
| `claudeStatus.pricing.outputPerMillion` | `number` | `15.00` | 每百万输出令牌的 USD 单价 |
| `claudeStatus.pricing.cacheReadPerMillion` | `number` | `0.30` | 每百万缓存读取令牌的 USD 单价 |
Expand Down
25 changes: 24 additions & 1 deletion docs/DATA.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,8 +76,31 @@ Always keep a margin on the cutoff — never filter with the exact window start.

### Cost Calculation

#### Model-aware pricing (default)

Each JSONL entry carries `message.model`. When `claudeStatus.pricing.useModelPricing`
is `true` (default), the cost of each entry is computed with the built-in rate for
its model family (`resolveModelPricing()` in `jsonlReader.ts`). Rules are matched
in order — first match wins:

| Match (case-insensitive) | Model family | Input | Output | Cache read | Cache write |
|--------------------------|--------------|-------|--------|------------|-------------|
| `fable`, `mythos` | Fable 5 / Mythos 5 | $10.00 | $50.00 | $1.00 | $12.50 |
| `claude-3-opus`, `opus-4-0`, `opus-4-1`, `opus-4-2025` | Legacy Opus (≤ 4.1) | $15.00 | $75.00 | $1.50 | $18.75 |
| `opus` | Opus 4.5+ | $5.00 | $25.00 | $0.50 | $6.25 |
| `haiku` | Haiku | $1.00 | $5.00 | $0.10 | $1.25 |
| `sonnet` | Sonnet | $3.00 | $15.00 | $0.30 | $3.75 |

Unrecognized models (e.g. `<synthetic>`) fall back to the manual rates below.
Cost aggregation also produces `costByModel5h` (model id → USD in the 5h window),
shown in the dashboard token breakdown.

#### Manual rates (fallback / opt-out)

Default rates are based on Claude Sonnet 4.x pricing.
**All four rates are user-configurable** via `claudeStatus.pricing.*` settings.
**All four rates are user-configurable** via `claudeStatus.pricing.*` settings and
apply to **every** entry when `useModelPricing` is `false`, or to entries whose
model is not recognized.

| Token type | Setting key | Default (USD / 1M) |
|------------|-------------|-------------------|
Expand Down
8 changes: 7 additions & 1 deletion docs/SETTINGS.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,13 @@ What the status bar and dashboard show, depending on your Claude plan and key se
"description": "Claude provider type. Controls whether rate limit percentages or cost is shown in the status bar."
},

// --- Token pricing (user-adjustable when Anthropic changes rates) ---
// --- Token pricing ---
"claudeStatus.pricing.useModelPricing": {
"type": "boolean",
"default": true,
"description": "Use built-in per-model rates (Opus/Sonnet/Haiku) based on each entry's model field for more accurate cost estimates. When disabled, or when a model is not recognized, the manual claudeStatus.pricing.* rates are used for every entry."
},
// --- Manual token pricing (fallback for unknown models; all entries when useModelPricing is false) ---
"claudeStatus.pricing.inputPerMillion": {
"type": "number",
"default": 3.00,
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"Output": "出力",
"Cache read": "キャッシュ読み取り",
"Cache create": "キャッシュ作成",
"By model (5h)": "モデル別(5h)",
"Good! Cache is saving cost.": "良好!キャッシュがコストを節約しています。",
"Low cache reuse.": "キャッシュの再利用が少ないです。",
"No cache reads in this window.": "このウィンドウ内にキャッシュ読み取りはありません。",
Expand Down
1 change: 1 addition & 0 deletions l10n/bundle.l10n.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"Output": "输出",
"Cache read": "缓存读取",
"Cache create": "缓存创建",
"By model (5h)": "按模型(5h)",
"Good! Cache is saving cost.": "很好!缓存正在节省费用。",
"Low cache reuse.": "缓存复用率低。",
"No cache reads in this window.": "此窗口内无缓存读取。",
Expand Down
5 changes: 5 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,11 @@
"default": "auto",
"description": "%config.claudeProvider.desc%"
},
"claudeStatus.pricing.useModelPricing": {
"type": "boolean",
"default": true,
"description": "%config.pricing.useModelPricing.desc%"
},
"claudeStatus.pricing.inputPerMillion": {
"type": "number",
"default": 3.0,
Expand Down
1 change: 1 addition & 0 deletions package.nls.ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"config.claudeProvider.claudeAi": "Claude.ai サブスクリプション — レート制限の利用率を表示します(5h ウィンドウ、利用可能な場合は 7d ウィンドウ)。",
"config.claudeProvider.awsBedrock": "AWS Bedrock — レート制限なし、トークンコストのみ表示。",
"config.claudeProvider.apiKey": "Anthropic API キー — レート制限なし、トークンコストのみ表示。",
"config.pricing.useModelPricing.desc": "各エントリの model フィールドに基づき、モデル別の組み込み料金(Opus/Sonnet/Haiku)を使用してより正確なコストを推定します。無効の場合、またはモデルを認識できない場合は、手動設定の claudeStatus.pricing.* 料金がすべてのエントリに使用されます。",
"config.pricing.inputPerMillion.desc": "入力トークン 1M あたりのコスト(USD)。デフォルト: $3.00(Claude Sonnet 4.x)。Anthropic が料金を変更した場合は更新してください。",
"config.pricing.outputPerMillion.desc": "出力トークン 1M あたりのコスト(USD)。デフォルト: $15.00(Claude Sonnet 4.x)。Anthropic が料金を変更した場合は更新してください。",
"config.pricing.cacheReadPerMillion.desc": "キャッシュ読み取りトークン 1M あたりのコスト(USD)。デフォルト: $0.30(Claude Sonnet 4.x)。Anthropic が料金を変更した場合は更新してください。",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"config.claudeProvider.claudeAi": "Claude.ai subscription — shows rate limit utilization (5h window, 7d window if available).",
"config.claudeProvider.awsBedrock": "AWS Bedrock — no rate limits, shows token cost only.",
"config.claudeProvider.apiKey": "Anthropic API key — no rate limits, shows token cost only.",
"config.pricing.useModelPricing.desc": "Use built-in per-model rates (Opus/Sonnet/Haiku) based on each entry's model field for more accurate cost estimates. When disabled, or when a model is not recognized, the manual claudeStatus.pricing.* rates are used for every entry.",
"config.pricing.inputPerMillion.desc": "Cost per 1M input tokens in USD. Default: $3.00 (Claude Sonnet 4.x). Update this if Anthropic changes pricing.",
"config.pricing.outputPerMillion.desc": "Cost per 1M output tokens in USD. Default: $15.00 (Claude Sonnet 4.x). Update this if Anthropic changes pricing.",
"config.pricing.cacheReadPerMillion.desc": "Cost per 1M cache-read tokens in USD. Default: $0.30 (Claude Sonnet 4.x). Update this if Anthropic changes pricing.",
Expand Down
1 change: 1 addition & 0 deletions package.nls.zh-cn.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"config.claudeProvider.claudeAi": "Claude.ai 订阅 — 显示速率限制利用率(5h 窗口,如可用则显示 7d 窗口)。",
"config.claudeProvider.awsBedrock": "AWS Bedrock — 无速率限制,仅显示令牌费用。",
"config.claudeProvider.apiKey": "Anthropic API 密钥 — 无速率限制,仅显示令牌费用。",
"config.pricing.useModelPricing.desc": "根据每条记录的 model 字段使用内置的按模型费率(Opus/Sonnet/Haiku),以获得更准确的成本估算。禁用时或无法识别模型时,所有记录都将使用手动设置的 claudeStatus.pricing.* 费率。",
"config.pricing.inputPerMillion.desc": "每百万输入令牌的费用(USD)。默认:$3.00(Claude Sonnet 4.x)。Anthropic 调整定价后请更新此值。",
"config.pricing.outputPerMillion.desc": "每百万输出令牌的费用(USD)。默认:$15.00(Claude Sonnet 4.x)。Anthropic 调整定价后请更新此值。",
"config.pricing.cacheReadPerMillion.desc": "每百万缓存读取令牌的费用(USD)。默认:$0.30(Claude Sonnet 4.x)。Anthropic 调整定价后请更新此值。",
Expand Down
13 changes: 12 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as vscode from 'vscode';
import type { ClaudeProvider } from './data/apiClient';
import type { TokenPricing } from './data/jsonlReader';
import type { CostOptions, TokenPricing } from './data/jsonlReader';

export class ExtensionConfig {
private get cfg() {
Expand Down Expand Up @@ -81,6 +81,17 @@ export class ExtensionConfig {
};
}

get useModelPricing(): boolean {
return this.cfg.get('pricing.useModelPricing', true);
}

get costOptions(): CostOptions {
return {
pricing: this.tokenPricing,
useModelPricing: this.useModelPricing,
};
}

async setDisplayMode(mode: 'percent' | 'cost'): Promise<void> {
await this.cfg.update('displayMode', mode, vscode.ConfigurationTarget.Global);
}
Expand Down
8 changes: 5 additions & 3 deletions src/data/dataManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ClaudeUsageData {
tokensOut5h: number
tokensCacheRead5h: number
tokensCacheCreate5h: number
costByModel5h: Record<string, number>

// Rate limit metadata
has7dLimit: boolean // false for plans without a 7d window or non-Claude.ai providers
Expand Down Expand Up @@ -64,7 +65,7 @@ export class DataManager {
}

async getUsageData(forceRefresh = false): Promise<ClaudeUsageData> {
const [localUsage, cache] = await Promise.all([readAllUsage(config.tokenPricing), readCache()]);
const [localUsage, cache] = await Promise.all([readAllUsage(config.costOptions), readCache()]);

// Determine provider type (user config or auto-detection)
const configuredProvider = config.claudeProvider;
Expand Down Expand Up @@ -154,7 +155,7 @@ export class DataManager {

async refreshProjectCosts(): Promise<void> {
try {
this.lastProjectCosts = await getAllProjectCosts(config.tokenPricing);
this.lastProjectCosts = await getAllProjectCosts(config.costOptions);
} catch {
this.lastProjectCosts = [];
}
Expand Down Expand Up @@ -226,6 +227,7 @@ export class DataManager {
this.lastData.cost5h,
this.lastData.costDay,
config.dailyBudget,
config.costOptions,
);
this.lastPrediction = prediction;
return prediction;
Expand All @@ -244,7 +246,7 @@ export class DataManager {
return this.lastHeatmapData;
}
try {
const data = await computeHeatmapData(config.heatmapDays);
const data = await computeHeatmapData(config.heatmapDays, config.costOptions);
this.lastHeatmapData = data;
this.heatmapComputedAt = now;
return data;
Expand Down
57 changes: 55 additions & 2 deletions src/data/jsonlReader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ interface JsonlEntry {
requestId?: string
message?: {
id?: string
model?: string
usage?: TokenUsage
}
}
Expand All @@ -33,6 +34,7 @@ export interface AggregatedUsage {
tokensOut5h: number
tokensCacheRead5h: number
tokensCacheCreate5h: number
costByModel5h: Record<string, number> // model id → cost in the 5h window
}

export interface TokenPricing {
Expand All @@ -49,6 +51,51 @@ export const DEFAULT_PRICING: TokenPricing = {
cacheCreatePerMillion: 3.75,
};

// Built-in per-model rates (USD per 1M tokens), matched against the JSONL
// entry's message.model. Anthropic's published pricing: cache read = 0.1x
// input, 5-minute cache write = 1.25x input. Order matters — first match wins,
// so legacy patterns must precede their generic family pattern.
const MODEL_PRICING_RULES: ReadonlyArray<{ pattern: RegExp; pricing: TokenPricing }> = [
// Fable 5 / Mythos 5: $10 / $50
{ pattern: /fable|mythos/i, pricing: { inputPerMillion: 10.00, outputPerMillion: 50.00, cacheReadPerMillion: 1.00, cacheCreatePerMillion: 12.50 } },
// Legacy Opus (3.x, 4.0, 4.1): $15 / $75
{ pattern: /claude-3-opus|opus-4-0|opus-4-1|opus-4-2025/i, pricing: { inputPerMillion: 15.00, outputPerMillion: 75.00, cacheReadPerMillion: 1.50, cacheCreatePerMillion: 18.75 } },
// Opus 4.5 and later: $5 / $25
{ pattern: /opus/i, pricing: { inputPerMillion: 5.00, outputPerMillion: 25.00, cacheReadPerMillion: 0.50, cacheCreatePerMillion: 6.25 } },
// Haiku (4.5): $1 / $5
{ pattern: /haiku/i, pricing: { inputPerMillion: 1.00, outputPerMillion: 5.00, cacheReadPerMillion: 0.10, cacheCreatePerMillion: 1.25 } },
// Sonnet (3.x–5): $3 / $15
{ pattern: /sonnet/i, pricing: { inputPerMillion: 3.00, outputPerMillion: 15.00, cacheReadPerMillion: 0.30, cacheCreatePerMillion: 3.75 } },
];

// Returns the built-in rate for a recognized model, or the fallback (the
// user-configured claudeStatus.pricing.* rates) when the model is unknown
export function resolveModelPricing(model: string | undefined, fallback: TokenPricing): TokenPricing {
if (model) {
for (const rule of MODEL_PRICING_RULES) {
if (rule.pattern.test(model)) { return rule.pricing; }
}
}
return fallback;
}

export interface CostOptions {
pricing: TokenPricing // manual rates; also the fallback for unknown models
useModelPricing: boolean // when true, recognized models use built-in rates
}

export const DEFAULT_COST_OPTIONS: CostOptions = {
pricing: DEFAULT_PRICING,
useModelPricing: true,
};

export function entryCost(entry: UsageEntry, options: CostOptions = DEFAULT_COST_OPTIONS): number {
const pricing = options.useModelPricing
? resolveModelPricing(entry.model, options.pricing)
: options.pricing;
return calculateCost(entry.usage, pricing);
}

export function calculateCost(usage: TokenUsage, pricing: TokenPricing = DEFAULT_PRICING): number {
return (
((usage.input_tokens || 0) / 1_000_000) * pricing.inputPerMillion +
Expand Down Expand Up @@ -148,6 +195,7 @@ export interface UsageEntry {
timestamp: number // ms since epoch, guaranteed valid
usage: TokenUsage // all four fields normalized to numbers
cwd?: string
model?: string // e.g. "claude-sonnet-4-5-20250929"
}

export async function readUsageEntries(filePath: string): Promise<UsageEntry[]> {
Expand All @@ -167,12 +215,13 @@ export async function readUsageEntries(filePath: string): Promise<UsageEntry[]>
cache_creation_input_tokens: rawUsage.cache_creation_input_tokens || 0,
},
cwd: typeof entry.cwd === 'string' ? entry.cwd : undefined,
model: typeof entry.message?.model === 'string' ? entry.message.model : undefined,
});
}
return result;
}

export async function readAllUsage(pricing: TokenPricing = DEFAULT_PRICING): Promise<AggregatedUsage> {
export async function readAllUsage(options: CostOptions = DEFAULT_COST_OPTIONS): Promise<AggregatedUsage> {
const now = Date.now();
const window5h = 5 * 3600 * 1000;
const window7d = 7 * 24 * 3600 * 1000;
Expand All @@ -187,6 +236,7 @@ export async function readAllUsage(pricing: TokenPricing = DEFAULT_PRICING): Pro
tokensOut5h: 0,
tokensCacheRead5h: 0,
tokensCacheCreate5h: 0,
costByModel5h: {},
};

// Skip files untouched for over 7 days (+1h margin) — they cannot contain
Expand All @@ -197,7 +247,7 @@ export async function readAllUsage(pricing: TokenPricing = DEFAULT_PRICING): Pro
const entries = await readUsageEntries(file);
for (const entry of entries) {
const { timestamp: ts, usage } = entry;
const cost = calculateCost(usage, pricing);
const cost = entryCost(entry, options);

const age = now - ts;
if (age <= window7d) {
Expand All @@ -212,6 +262,9 @@ export async function readAllUsage(pricing: TokenPricing = DEFAULT_PRICING): Pro
result.tokensOut5h += usage.output_tokens || 0;
result.tokensCacheRead5h += usage.cache_read_input_tokens || 0;
result.tokensCacheCreate5h += usage.cache_creation_input_tokens || 0;
if (entry.model) {
result.costByModel5h[entry.model] = (result.costByModel5h[entry.model] || 0) + cost;
}
}
}
}
Expand Down
Loading