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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ upstream release: 1.0.8). Format follows [Keep a Changelog](https://keepachangel
## [2.3.0] — Unreleased

### Fixed
- **AWS Bedrock in-region Claude pricing** — the selectable Bedrock table now
covers Claude Opus 4.5, 4.6, 4.7, 4.8, Sonnet 4.5, Sonnet 4.6, Sonnet 5,
Opus 5, and Haiku 4.5, including separate 5-minute and 1-hour cache-write
rates. Changing the pricing backend invalidates the incremental cost
aggregate so existing logs are recalculated immediately.
- **Codex Today is now the configured calendar day** — the first Codex tab now
pairs its day total with exact hourly API-equivalent cost and a separate
token-composition view. Its additive schema-3 current-day sidecar scans only
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "claude-code-usage",
"displayName": "Claude Code Usage",
"description": "Local Claude Code and Codex usage insights in the VS Code status bar",
"version": "2.1.1",
"version": "2.3.1",
"publisher": "GrowthJack",
"author": "GrowthJack",
"license": "MIT",
Expand Down
28 changes: 27 additions & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { StatusBarManager } from './statusBar';
import { UsageWebviewProvider } from './webview';
import { I18n } from './i18n';
import { resolveTimeZone } from './dateKeys';
import { fetchLatestPricing } from './pricing';
import { fetchLatestPricing, setPricingBackend } from './pricing';
import { ClaudeApiClient } from './claudeApiClient';
import {
buildOptimizerSystemPrompt,
Expand Down Expand Up @@ -150,6 +150,7 @@ export class ClaudeCodeUsageExtension {
private quotaColdRetryDone: boolean = false;
private claudeProfileGeneration: number = 0;
private claudeWeeklyQuotaHistory: WeeklyQuotaObservation[] = [];
private activePricingBackend: ExtensionConfig['pricingBackend'] = 'anthropic';
private codexProvider: CodexProvider;
private readonly codexSalt: string;
private codexView: CodexUsageView | null = null;
Expand Down Expand Up @@ -304,6 +305,7 @@ export class ClaudeCodeUsageExtension {
private async refreshPricing(): Promise<void> {
try {
const result = await fetchLatestPricing();
this.invalidateClaudeUsagePricingCache();
vscode.window.showInformationMessage(`${I18n.t.popup.pricingUpdated} (${result.updated})`);
// Force a full recompute so the new prices take effect.
void this.refreshData(true, 'pricing');
Expand Down Expand Up @@ -756,6 +758,8 @@ export class ClaudeCodeUsageExtension {

private loadConfiguration(): void {
const config = this.getConfiguration();
this.activePricingBackend = config.pricingBackend;
setPricingBackend(config.pricingBackend);
I18n.setLanguage(config.language as any);
I18n.setDecimalPlaces(config.decimalPlaces);
I18n.setTokenDecimalPlaces(config.tokenDecimalPlaces);
Expand Down Expand Up @@ -791,6 +795,7 @@ export class ClaudeCodeUsageExtension {
return {
refreshInterval: s.get<number>('refreshInterval'),
dataDirectory: s.get<string>('dataDirectory'),
pricingBackend: s.get<'anthropic' | 'aws-bedrock-in-region'>('pricingBackend'),
codexEnabled: s.get<boolean>('codex.enabled'),
codexDataDirectory: s.get<string>('codex.dataDirectory'),
codexFileWatchSeconds:
Expand Down Expand Up @@ -1109,6 +1114,12 @@ export class ClaudeCodeUsageExtension {

private onConfigurationChanged(): void {
const config = this.getConfiguration();
const pricingBackendChanged = this.activePricingBackend !== config.pricingBackend;
this.activePricingBackend = config.pricingBackend;
setPricingBackend(config.pricingBackend);
if (pricingBackendChanged) {
this.invalidateClaudeUsagePricingCache();
}
I18n.setLanguage(config.language as any);
I18n.setDecimalPlaces(config.decimalPlaces);
I18n.setTokenDecimalPlaces(config.tokenDecimalPlaces);
Expand Down Expand Up @@ -1136,6 +1147,21 @@ export class ClaudeCodeUsageExtension {
});
}

/**
* Pricing is part of every cached UsageData aggregate. The incremental
* loader normally skips unchanged JSONL files, so changing the pricing
* backend would otherwise update only the displayed rate labels while
* leaving totalCost/modelBreakdown.cost at the old rates.
*/
private invalidateClaudeUsagePricingCache(): void {
this.cache.claudeIndex = createClaudeUsageIndex();
this.cache.records = [];
this.cache.contentAnalysis = null;
this.cache.manifest = null;
this.cache.dataDirectory = null;
this.cache.lastUpdate = new Date(0);
}

/** Keep quota credentials on the same Claude profile as this window's logs.
* A profile switch invalidates every in-memory quota/backoff value because it
* belongs to a different account. */
Expand Down
7 changes: 7 additions & 0 deletions src/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2919,6 +2919,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'Token-Dezimalstellen', help: 'Dezimalstellen für kompakte Token-Anzeige (1.2M / 345.6K). Volle Ganzzahlen bleiben unberührt.' },
'compactNumbers': { label: 'Kompakte Token-Zahlen', help: 'Zeige 1.2M / 345K statt voller Zahlen.' },
'releaseAnnouncements': { label: 'Release-Hinweise', help: 'Nach einem Erweiterungs-Upgrade einmal die Neuerungen anzeigen.' },
'pricingBackend': { label: 'Claude-Preisquelle', help: 'Wähle AWS-Bedrock-In-Region-Preise, wenn Claude Code über Bedrock geroutet wird.' },
'codex.enabled': { label: 'Codex Beta aktivieren', help: 'Datenschutzfreundliche Nutzungsaggregate aus lokalen Codex-Sitzungslogs lesen.' },
'codex.dataDirectory': { label: 'Benutzerdefiniertes Codex-Datenverzeichnis', help: 'Leer = CODEX_HOME, dann ~/.codex. Authentifizierungsdateien werden nie gelesen.' },
'codex.fileWatchSeconds': { label: 'Codex-Live-Aktualisierungsverzögerung', help: 'Ruhe-Debounce nach lokalen Codex-JSONL-Änderungen. Aus deaktiviert die Überwachung.' },
Expand Down Expand Up @@ -2966,6 +2967,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'Token 小數位數', help: '緊湊 token 顯示(1.2M / 345.6K)的小數位數。完整整數值不受影響。' },
'compactNumbers': { label: '簡潔的 Token 計數', help: '顯示 1.2M / 345K 而非完整數字。' },
'releaseAnnouncements': { label: '版本更新通知', help: '擴充套件升級後顯示一次「新功能」通知。' },
'pricingBackend': { label: 'Claude 價格來源', help: '若 Claude Code 透過 Bedrock 路由,請選擇 AWS Bedrock 區域內價格。' },
'codex.enabled': { label: '啟用 Codex Beta', help: '從本機 Codex 工作階段日誌讀取隱私安全的用量彙總。' },
'codex.dataDirectory': { label: '自訂 Codex 資料目錄', help: '留空時使用 CODEX_HOME,再使用 ~/.codex;不會讀取認證檔案。' },
'codex.fileWatchSeconds': { label: 'Codex 即時重新整理延遲', help: '本機 Codex JSONL 變更後的靜默防抖;關閉即停用監看。' },
Expand Down Expand Up @@ -3013,6 +3015,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'Token 小数位数', help: '紧凑 token 显示(1.2M / 345.6K)的小数位数。完整整数值不受影响。' },
'compactNumbers': { label: '简洁的 token 计数', help: '显示 1.2M / 345K 而非完整数字。' },
'releaseAnnouncements': { label: '版本更新通知', help: '扩展升级后显示一次“新功能”通知。' },
'pricingBackend': { label: 'Claude 价格来源', help: '如果 Claude Code 通过 Bedrock 路由,请选择 AWS Bedrock 区域内价格。' },
'codex.enabled': { label: '启用 Codex Beta', help: '从本地 Codex 会话日志读取隐私安全的用量汇总。' },
'codex.dataDirectory': { label: '自定义 Codex 数据目录', help: '留空时使用 CODEX_HOME,再使用 ~/.codex;不会读取认证文件。' },
'codex.fileWatchSeconds': { label: 'Codex 实时刷新延迟', help: '本地 Codex JSONL 变更后的静默防抖;关闭即停用监视。' },
Expand Down Expand Up @@ -3060,6 +3063,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'トークンの小数点以下桁数', help: 'トークンの短縮表示(1.2M / 345.6K)の小数桁数。完全な整数値には影響しません。' },
'compactNumbers': { label: 'トークン数を短縮表記', help: '完全な数値の代わりに 1.2M / 345K と表示します。' },
'releaseAnnouncements': { label: 'リリース通知', help: '拡張機能のアップグレード後に新機能を一度通知します。' },
'pricingBackend': { label: 'Claude の価格ソース', help: 'Claude Code を Bedrock 経由でルーティングする場合は AWS Bedrock のリージョン内価格を選択します。' },
'codex.enabled': { label: 'Codex Beta を有効化', help: 'ローカルの Codex セッションログからプライバシー安全な使用量集計を読み取ります。' },
'codex.dataDirectory': { label: 'カスタム Codex データディレクトリ', help: '空欄の場合は CODEX_HOME、次に ~/.codex。認証ファイルは読みません。' },
'codex.fileWatchSeconds': { label: 'Codex ライブ更新遅延', help: 'ローカル Codex JSONL 変更後の静かなデバウンス。オフで監視を無効化します。' },
Expand Down Expand Up @@ -3107,6 +3111,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: '토큰 소수점 자리수', help: '간략한 토큰 표시(1.2M / 345.6K)의 소수 자리수. 전체 정수 값에는 영향을 주지 않습니다.' },
'compactNumbers': { label: '간략한 토큰 수 표시', help: '전체 숫자 대신 1.2M / 345K로 표시합니다.' },
'releaseAnnouncements': { label: '릴리스 알림', help: '확장 업그레이드 후 새 기능 알림을 한 번 표시합니다.' },
'pricingBackend': { label: 'Claude 가격 출처', help: 'Claude Code가 Bedrock을 통해 라우팅될 때 AWS Bedrock 리전 내 가격을 선택합니다.' },
'codex.enabled': { label: 'Codex Beta 사용', help: '로컬 Codex 세션 로그에서 개인정보 보호형 사용량 집계를 읽습니다.' },
'codex.dataDirectory': { label: '사용자 지정 Codex 데이터 디렉터리', help: '비우면 CODEX_HOME, 그다음 ~/.codex를 사용하며 인증 파일은 읽지 않습니다.' },
'codex.fileWatchSeconds': { label: 'Codex 실시간 새로고침 지연', help: '로컬 Codex JSONL 변경 후 조용한 디바운스입니다. 끄면 감시를 중지합니다.' },
Expand Down Expand Up @@ -3154,6 +3159,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'Casas decimais de tokens', help: 'Casas decimais para a exibição compacta de tokens (1.2M / 345.6K). As contagens inteiras completas não são afetadas.' },
'compactNumbers': { label: 'Contagem de tokens compacta', help: 'Mostra 1.2M / 345K em vez dos números completos.' },
'releaseAnnouncements': { label: 'Avisos de versão', help: 'Mostra uma vez as novidades após atualizar a extensão.' },
'pricingBackend': { label: 'Fonte de preços do Claude', help: 'Selecione os preços regionais da AWS Bedrock quando o Claude Code for roteado pela Bedrock.' },
'codex.enabled': { label: 'Ativar Codex Beta', help: 'Lê agregados de uso com privacidade a partir dos logs locais de sessão do Codex.' },
'codex.dataDirectory': { label: 'Diretório de dados Codex personalizado', help: 'Vazio = CODEX_HOME, depois ~/.codex. Arquivos de autenticação nunca são lidos.' },
'codex.fileWatchSeconds': { label: 'Atraso da atualização ao vivo do Codex', help: 'Debounce silencioso após mudanças locais em JSONL do Codex. Desligado desativa a observação.' },
Expand Down Expand Up @@ -3202,6 +3208,7 @@ const SETTINGS_I18N: Partial<Record<SupportedLanguage, Record<string, { label: s
'tokenDecimalPlaces': { label: 'Angka desimal token', help: 'Angka desimal untuk tampilan token ringkas (1.2M / 345.6K). Jumlah bilangan bulat penuh tidak terpengaruh.' },
'compactNumbers': { label: 'Jumlah token ringkas', help: 'Tampilkan 1.2M / 345K, bukan angka penuh.' },
'releaseAnnouncements': { label: 'Pengumuman rilis', help: 'Tampilkan sekali hal baru setelah ekstensi ditingkatkan.' },
'pricingBackend': { label: 'Sumber harga Claude', help: 'Pilih harga AWS Bedrock dalam-region saat Claude Code dirutekan melalui Bedrock.' },
'codex.enabled': { label: 'Aktifkan Codex Beta', help: 'Baca agregat penggunaan yang aman untuk privasi dari log sesi Codex lokal.' },
'codex.dataDirectory': { label: 'Direktori data Codex kustom', help: 'Kosong = CODEX_HOME, lalu ~/.codex. Berkas autentikasi tidak pernah dibaca.' },
'codex.fileWatchSeconds': { label: 'Jeda penyegaran langsung Codex', help: 'Debounce tenang setelah perubahan JSONL Codex lokal. Mati menonaktifkan pemantauan.' },
Expand Down
116 changes: 116 additions & 0 deletions src/pricing.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,20 @@ import * as https from 'https';

import { ModelPricing } from './types';

export type PricingBackend = 'anthropic' | 'aws-bedrock-in-region';

let pricingBackend: PricingBackend = 'anthropic';

/** Select the price table used for Claude model cost calculations. */
export function setPricingBackend(backend: PricingBackend): void {
pricingBackend = backend === 'aws-bedrock-in-region' ? backend : 'anthropic';
}

/** Return the currently selected Claude pricing backend. */
export function getPricingBackend(): PricingBackend {
return pricingBackend;
}

export interface TokenUsage {
input_tokens: number;
output_tokens: number;
Expand Down Expand Up @@ -126,6 +140,91 @@ const HAIKU_35: ModelPricing = {
cache_read_input_token_cost: 0.08 / MILL,
};

// AWS Bedrock in-region, on-demand pricing (USD per 1M tokens).
//
// The Bedrock table also publishes batch prices. Claude Code's normal
// pay-as-you-go inference is not batch inference, so only the standard input /
// output rates and the cache rates are used here.
const BEDROCK_OPUS_5: ModelPricing = {
input_cost_per_token: 5.5 / MILL,
output_cost_per_token: 27.5 / MILL,
cache_creation_input_token_cost: 6.875 / MILL,
cache_creation_1h_input_token_cost: 11 / MILL,
cache_read_input_token_cost: 0.55 / MILL,
};

// AWS Bedrock Claude Sonnet 4.5 / 4.6 in-region, on-demand pricing.
const BEDROCK_SONNET_45_PLUS: ModelPricing = {
input_cost_per_token: 3.3 / MILL,
output_cost_per_token: 16.5 / MILL,
cache_creation_input_token_cost: 4.125 / MILL,
cache_creation_1h_input_token_cost: 6.6 / MILL,
cache_read_input_token_cost: 0.33 / MILL,
};

const BEDROCK_SONNET_5: ModelPricing = {
input_cost_per_token: 2.2 / MILL,
output_cost_per_token: 11 / MILL,
cache_creation_input_token_cost: 2.75 / MILL,
cache_creation_1h_input_token_cost: 4.4 / MILL,
cache_read_input_token_cost: 0.22 / MILL,
};

const BEDROCK_HAIKU_45: ModelPricing = {
input_cost_per_token: 1.1 / MILL,
output_cost_per_token: 5.5 / MILL,
cache_creation_input_token_cost: 1.375 / MILL,
cache_creation_1h_input_token_cost: 2.2 / MILL,
cache_read_input_token_cost: 0.11 / MILL,
};

/**
* Resolve the user-supplied Claude model label to an AWS Bedrock model family.
*
* Claude Code may log a short model name (`claude-sonnet-5`) even when the
* request was routed through Bedrock. Bedrock itself may expose a regional
* inference-profile prefix such as `us.anthropic.`. In the explicit Bedrock
* mode both forms must use the same in-region table.
*/
function getBedrockPricing(modelName: string): ModelPricing | null {
const name = modelName
.toLowerCase()
.replace(/\[[^\]]*\]\s*$/, '')
.replace(/^(?:(?:global|us|eu|apac|in)\.)?anthropic[./]/, '')
// Some integrations display the version separator as "." or "_", while
// Claude Code's native IDs use "-". Treat those labels as the same
// Bedrock model family so pricing is not silently reduced to Anthropic's
// generic Sonnet fallback.
.replace(/[._]/g, '-')
.replace(/\s+/g, '-');

if (name.includes('claude-opus-5') || name.includes('opus-5')) {
return BEDROCK_OPUS_5;
}
if (
name.includes('claude-opus-4-8') ||
name.includes('claude-opus-4-7') ||
name.includes('claude-opus-4-6') ||
name.includes('claude-opus-4-5') ||
name.includes('opus-4-8') ||
name.includes('opus-4-7') ||
name.includes('opus-4-6') ||
name.includes('opus-4-5')
) {
return BEDROCK_OPUS_5;
}
if (name.includes('claude-sonnet-5') || name.includes('sonnet-5')) {
return BEDROCK_SONNET_5;
}
if (name.includes('claude-sonnet-4-6') || name.includes('claude-sonnet-4-5')) {
return BEDROCK_SONNET_45_PLUS;
}
if (name.includes('claude-haiku-4-5') || name.includes('haiku-4-5') || name.includes('haiku-4.5')) {
return BEDROCK_HAIKU_45;
}
return null;
}

/**
* Build pricing for non-Anthropic providers, which usually only publish an
* input price, an output price, and an optional discounted "cached input" price.
Expand Down Expand Up @@ -373,6 +472,17 @@ export function getModelPricing(modelName: string | undefined): ModelPricing | n
// pricing applies.
modelName = modelName.replace(/\[[^\]]*\]\s*$/, '');

// A Bedrock route cannot be inferred reliably from the model label alone:
// Claude Code commonly records just "claude-sonnet-5". When the user
// explicitly selects Bedrock, its known Claude rates must therefore take
// precedence over both the generic Anthropic table and LiteLLM overrides.
if (pricingBackend === 'aws-bedrock-in-region') {
const bedrockPricing = getBedrockPricing(modelName);
if (bedrockPricing) {
return bedrockPricing;
}
}

// Try different variation matches (similar to ccusage logic)
const variations = [modelName, `anthropic/${modelName}`, `claude-3-5-${modelName}`, `claude-3-${modelName}`, `claude-${modelName}`];

Expand Down Expand Up @@ -416,6 +526,12 @@ export function getExactModelPricing(modelName: string | undefined): ModelPricin
return null;
}
const base = modelName.replace(/\[[^\]]*\]\s*$/, '');
if (pricingBackend === 'aws-bedrock-in-region') {
const bedrockPricing = getBedrockPricing(base);
if (bedrockPricing) {
return bedrockPricing;
}
}
const variations = [
base,
`anthropic/${base}`,
Expand Down
12 changes: 12 additions & 0 deletions src/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,18 @@ export const SETTINGS: SettingDef[] = [
},

// --- Providers ---
{
key: 'pricingBackend',
type: 'enum',
default: 'anthropic',
storage: 'state',
group: 'providers',
label: 'Claude pricing backend',
help: 'Select AWS Bedrock in-region rates when Claude Code usage is routed through Bedrock.',
enumValues: ['anthropic', 'aws-bedrock-in-region'],
enumLabels: ['Anthropic direct API', 'AWS Bedrock (in-region)'],
providers: ['claude'],
},
{
key: 'codex.enabled',
type: 'boolean',
Expand Down
Loading
Loading