Skip to content
Draft
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
2 changes: 1 addition & 1 deletion core-version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
7.2.158
7.3.1
57 changes: 57 additions & 0 deletions docs/usage-accounting.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# Usage and cost accounting

The dashboard reports usage events and the known USD amount associated with them. It is not a provider invoice or an HTTP access log. Retries, tools, prewarms, health probes, and unmeasured operations are separate event kinds. Endpoint/transport filters and distinct generation IDs make these differences visible; a WebSocket frame is not a request.

## Measurement and valuation

The collector preserves normalized token breakdown v2, quality, raw provider usage, cache lifetimes, event/attempt/generation IDs, transport, upstream URL, and reported cost. V2 is authoritative. Legacy records use the upstream Keeper normalizer exactly once: recognized executor contracts take precedence, provider identity is a fallback only for OAuth, and unknown producers retain their reported parent fields. Gemini thinking is included in billable output where that contract is known; OpenAI cached/reasoning tokens are subsets. Invalid or incomplete breakdowns are never repaired by inventing tokens. A missing usage object differs from an observed zero.

Each newly ingested event receives a valuation snapshot when its amount is known. The snapshot records the rates and rule version used, so editing prices does not rewrite known historical amounts. Legacy records without a snapshot can still be estimated using available prices, and an unknown event can acquire a later manual tariff. This does not recreate historical rates or lost upstream usage.

- `reported`: provider-supplied USD amount (currently xAI ticks, with the original decimal retained).
- `estimated`: a supported token/modality tariff with its source and captured rates.
- `unknown`: no sufficient usage, inconsistent/partial usage, missing price, or unsupported billing dimensions. Cost is null, not free. The coverage ratio counts only known amounts.

Amounts are evaluated per event before summation, preserving model-specific context thresholds and additivity. Request tier is overridden by an explicit response tier. Unsupported tier/context combinations remain unknown. Claude cache creation distinguishes 5-minute and 1-hour rates; an old aggregate write count cannot reveal its lifetime. Gemini thinking and audio input, OpenAI image/text/audio/cache dimensions, and DeepSeek's documented UTC peak schedule have separate rules. Unknown cache modality splits are not guessed.

xAI video polling retains every event but charges only the increase over a previously recorded cumulative operation cost. General token estimates do not replace a provider's actual cost. Built-in API prices are estimates for OAuth/subscription usage too; they do not establish the subscription's cash charge, taxes, negotiated discounts, reseller margins, or external tool bills. Manual rates can be scoped to provider and upstream base URL.

Model matching uses exact IDs, canonical provider prefixes, and known reasoning suffixes. An unknown media variant cannot inherit its parent text model's price. Manual alias mappings remain explicit user choices. Price sync preserves the last good cache on failure and rejects catalogs older than the bundled rule baseline.

## Sources and coverage

| Source | What is accounted for | Deliberate limit |
| --- | --- | --- |
| Chat/Responses/Claude/Gemini/Interactions executors | Reported tokens, cache, thinking, tiers and available billing dimensions | Aborted streams without final usage cannot establish complete spend |
| Codex Responses HTTP and WebSocket image tools | Separate parent and image-tool events, modality-specific rates | Unpublished/missing modality data remains unknown |
| Live/realtime WebSocket | Upstream terminal response usage, including text/audio/cache | Opaque WebRTC media without usage is an unmeasured operation |
| xAI image/video | Exact reported ticks, cumulative video operation costs | No invented per-image or duration-based charge when the provider omits billing |
| Alpha Search and management model probes | Available core-reported usage | Missing usage or independently priced search tools may remain unknown |
| Desktop health probes | Complete stream consumption, bounded output request, final usage for all four protocols, partial failure records | Network/size/timeout termination can leave only partial measurements |
| Token-count/control calls | Separate unmeasured operations | Predicted tokens are not charged as consumed tokens |
| Hosted tools | Claude search and OpenAI file-search request fees when counts are present | Ambiguous OpenAI search variants, grounding, containers and unsupported tools are not silently priced as token-only work |
| Plugins/custom gateways | Published SDK usage and explicit manual tariffs | Arbitrary external traffic and unpublished plugin charges cannot be inferred |

Billing rules were checked against official sources on 2026-09-12: [OpenAI](https://developers.openai.com/api/docs/pricing), [Anthropic](https://platform.claude.com/docs/en/about-claude/pricing), [Gemini](https://ai.google.dev/gemini-api/docs/pricing), [xAI pricing](https://docs.x.ai/developers/pricing), [xAI cost tracking](https://docs.x.ai/developers/cost-tracking), and [DeepSeek](https://api-docs.deepseek.com/quick_start/pricing/). The catalog is not a complete historical price database. DeepSeek's new schedule is not applied retrospectively before this rule baseline.

## Collection and deployment

With the companion CLIProxyAPI update, the collector reads `/v0/management/usage-journal`, commits a local SQLite inbox before acknowledging event IDs, and deduplicates replay using a unique nonempty `event_id`. A repeated `request_id` remains valid across retries/tools. The UI uses the SQLite row ID. Credentials are fingerprinted before inbox persistence, including old queue messages and direct probes.

Only a 404 enables legacy queue fallback; a journal storage/network/ACK error remains visible and is retried. A lost ACK cannot double-count a committed event. The core journal is a single-consumer local spool whose unacknowledged files do not expire. Operators must monitor storage if the collector is stopped. Disk failure, upstream omission, and events lost before upgrading cannot be fixed by deduplication.

**Release dependency:** `core-version.txt` follows upstream `dev` at 7.3.1. Upstream already supports token breakdown schema v2; this release does not contain the proposed journal/ACK endpoints, delivery identities, and extended billing dimensions from the companion PR. Before shipping the complete feature, publish a core release containing those changes and update the pin using the project's normal release workflow. The desktop changes remain backward compatible, but legacy cores do not provide the new durability and source coverage.

## Regression validation

Native tests cover the original arithmetic reproductions, context boundaries, tier modifiers, Claude TTL, Gemini thinking/audio, image/realtime modalities, reported media cost, repeated polling, replay IDs, endpoint/transport filters, provider-specific manual rates, price snapshots, unknown usage/price dimensions, and credential redaction. Health parser tests cover final stream usage. New regressions were first observed failing, then fixed.

The complete Rust test suite, Bun test suite and TypeScript/Vite build pass locally. On macOS, use a real directory for `TMPDIR`: existing backup tests intentionally reject symlink paths such as `/var`. Their symlink cleanup now uses the correct Unix operation while retaining Windows junction handling.

## Independent review corrections

Schema migration uses an atomic savepoint and repairs partially added accounting columns. The local inbox uses SQLite `synchronous=FULL`: a durable ACK must follow a flushed WAL commit, not merely a NORMAL-mode commit that could be lost on power failure. Overview counters are populated by event kind; logical generation counts deduplicate attempts and exclude prewarms/tools/probes.

Direct health probes pass provider/base URL through IPC, preserve actual response tiers, and require a terminal response instead of treating the first text delta as proof of completeness. Error bodies are captured through the same bounded reader; aborted streams are partial. DeepSeek cache-hit and Gemini tool-input counters retain their provider semantics.

Claude's actual `usage.speed` controls Fast pricing, including standard-speed fallback. Anthropic Priority commitments have no inferred Fast tariff. Eligible OpenAI US/EU processing endpoints receive the documented regional uplift; storage-only regions do not. Unsupported regional combinations remain unknown, and manual tariffs do not receive an extra automatic uplift. Sources: [Claude Fast mode](https://platform.claude.com/docs/en/build-with-claude/fast-mode), [OpenAI data residency support](https://developers.openai.com/api/docs/guides/your-data#api-endpoint-tool-and-model-support), and the pricing sources above.
2 changes: 1 addition & 1 deletion src-tauri/Cargo.lock

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

2 changes: 1 addition & 1 deletion src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
name = "cpa-gui"
# Single source of truth for the desktop app version.
version = "0.2.88"
version = "0.2.91"
description = "CPA desktop management shell"
authors = ["lzt404"]
edition = "2021"
Expand Down
30 changes: 24 additions & 6 deletions src-tauri/resources/model_prices.json
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
{
"schemaVersion": 1,
"updatedAt": "2026-09-07",
"updatedAt": "2026-09-12",
"models": {
"gpt-6-astra": { "inputPer1M": 10.0, "outputPer1M": 50.0, "cacheReadPer1M": 1.0, "cacheCreationPer1M": 12.5 },
"gpt-5.6": { "inputPer1M": 4.0, "outputPer1M": 20.0, "cacheReadPer1M": 0.4, "cacheCreationPer1M": 5.0 },
"gpt-5.6-sol": { "inputPer1M": 4.0, "outputPer1M": 20.0, "cacheReadPer1M": 0.4, "cacheCreationPer1M": 5.0 },
"gpt-5.6-terra": { "inputPer1M": 2.0, "outputPer1M": 12.0, "cacheReadPer1M": 0.2, "cacheCreationPer1M": 2.5 },
"gpt-5.6-luna": { "inputPer1M": 0.2, "outputPer1M": 1.2, "cacheReadPer1M": 0.02, "cacheCreationPer1M": 0.25 },
"gpt-5.5": { "inputPer1M": 5.0, "outputPer1M": 30.0, "cacheReadPer1M": 0.5 },
"gpt-5.5-pro": { "inputPer1M": 30.0, "outputPer1M": 180.0, "cacheReadPer1M": 3.0 },
"gpt-5.5-pro": { "inputPer1M": 30.0, "outputPer1M": 180.0 },
"gpt-5.4": { "inputPer1M": 2.5, "outputPer1M": 15.0, "cacheReadPer1M": 0.25 },
"gpt-5.4-pro": { "inputPer1M": 30.0, "outputPer1M": 180.0, "cacheReadPer1M": 3.0 },
"gpt-5.4-pro": { "inputPer1M": 30.0, "outputPer1M": 180.0 },
"gpt-5.4-mini": { "inputPer1M": 0.75, "outputPer1M": 4.5, "cacheReadPer1M": 0.075 },
"gpt-5.4-nano": { "inputPer1M": 0.2, "outputPer1M": 1.25, "cacheReadPer1M": 0.02 },
"gpt-5.3-chat-latest": { "inputPer1M": 1.75, "outputPer1M": 14.0, "cacheReadPer1M": 0.175 },
Expand Down Expand Up @@ -65,8 +65,8 @@
"gemini-2.5-flash": { "inputPer1M": 0.3, "outputPer1M": 2.5, "cacheReadPer1M": 0.03 },
"gemini-2.5-flash-lite": { "inputPer1M": 0.1, "outputPer1M": 0.4, "cacheReadPer1M": 0.01 },
"deepseek-v4-pro": { "inputPer1M": 1.32, "outputPer1M": 3.96, "cacheReadPer1M": 0.044, "cacheCreationPer1M": 0.0 },
"deepseek-v4-flash": { "inputPer1M": 0.44, "outputPer1M": 1.32, "cacheReadPer1M": 0.014, "cacheCreationPer1M": 0.0 },
"deepseek-v4-flash-vision-exp": { "inputPer1M": 0.44, "outputPer1M": 1.32, "cacheReadPer1M": 0.014, "cacheCreationPer1M": 0.0 },
"deepseek-v4-flash": { "inputPer1M": 0.3, "outputPer1M": 1.2, "cacheReadPer1M": 0.006 },
"deepseek-v4-flash-vision-exp": { "inputPer1M": 0.3, "outputPer1M": 1.2, "cacheReadPer1M": 0.006 },
"deepseek-chat": { "inputPer1M": 0.28, "outputPer1M": 0.42, "cacheReadPer1M": 0.028 },
"deepseek-reasoner": { "inputPer1M": 0.28, "outputPer1M": 0.42, "cacheReadPer1M": 0.028 },
"grok-4.6": { "inputPer1M": 2.0, "outputPer1M": 6.0, "cacheReadPer1M": 0.5 },
Expand All @@ -78,6 +78,24 @@
"grok-4": { "inputPer1M": 1.25, "outputPer1M": 2.5, "cacheReadPer1M": 0.2 },
"grok-4-1-fast": { "inputPer1M": 1.25, "outputPer1M": 2.5, "cacheReadPer1M": 0.2 },
"grok-3": { "inputPer1M": 1.25, "outputPer1M": 2.5, "cacheReadPer1M": 0.2 },
"grok-3-mini": { "inputPer1M": 1.25, "outputPer1M": 2.5, "cacheReadPer1M": 0.2 }
"grok-3-mini": { "inputPer1M": 1.25, "outputPer1M": 2.5, "cacheReadPer1M": 0.2 },
"gpt-4o-2024-05-13": { "inputPer1M": 5, "outputPer1M": 15 },
"deepseek-flash": { "inputPer1M": 0.3, "outputPer1M": 1.2, "cacheReadPer1M": 0.006 },
"gpt-image-2": { "inputPer1M": 5, "outputPer1M": 0, "cacheReadPer1M": 1.25 },
"gpt-image-2.5-sunburst": { "inputPer1M": 5, "outputPer1M": 0, "cacheReadPer1M": 1.25 },
"gpt-image-2.5-flare": { "inputPer1M": 5, "outputPer1M": 0, "cacheReadPer1M": 1.25 },
"gpt-image-1.5": { "inputPer1M": 5, "outputPer1M": 10, "cacheReadPer1M": 1.25 },
"gpt-image-1": { "inputPer1M": 5, "outputPer1M": 0, "cacheReadPer1M": 1.25 },
"gpt-image-1-mini": { "inputPer1M": 2, "outputPer1M": 0, "cacheReadPer1M": 0.2 },
"chatgpt-image-latest": { "inputPer1M": 5, "outputPer1M": 10, "cacheReadPer1M": 1.25 },
"gpt-realtime-2.1": { "inputPer1M": 4, "outputPer1M": 24, "cacheReadPer1M": 0.4 },
"gpt-realtime-2": { "inputPer1M": 4, "outputPer1M": 24, "cacheReadPer1M": 0.4 },
"gpt-realtime-1.5": { "inputPer1M": 4, "outputPer1M": 16, "cacheReadPer1M": 0.4 },
"gpt-realtime": { "inputPer1M": 4, "outputPer1M": 16, "cacheReadPer1M": 0.4 },
"gpt-realtime-2.1-mini": { "inputPer1M": 0.6, "outputPer1M": 2.4, "cacheReadPer1M": 0.06 },
"gpt-realtime-mini": { "inputPer1M": 0.6, "outputPer1M": 2.4, "cacheReadPer1M": 0.06 },
"gpt-audio": { "inputPer1M": 2.5, "outputPer1M": 10 },
"gpt-audio-1.5": { "inputPer1M": 2.5, "outputPer1M": 10 },
"gpt-audio-mini": { "inputPer1M": 0.6, "outputPer1M": 2.4 }
}
}
6 changes: 6 additions & 0 deletions src-tauri/src/agents/backups/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -680,13 +680,19 @@ fn linked_configuration_and_backup_directories_are_rejected() {
fs::read_to_string(outside.0.join("config.toml")).unwrap(),
"custom='outside'"
);
#[cfg(unix)]
fs::remove_file(link).unwrap();
#[cfg(windows)]
fs::remove_dir(link).unwrap();
let data = agent_data_directory(&paths).unwrap();
fs::create_dir_all(&data).unwrap();
link_directory(&outside.0, &data.join("backups"));
assert!(create_backup("codex", &home.0).is_err());
assert!(list_backups("codex", &home.0).is_err());
assert!(delete_backup("codex", &home.0, "1").is_err());
#[cfg(unix)]
fs::remove_file(data.join("backups")).unwrap();
#[cfg(windows)]
fs::remove_dir(data.join("backups")).unwrap();
}

Expand Down
Loading