"
+ license "MIT"
+
+ depends_on "node"
+
+ def install
+ system "npm", "install", *Language::Node.std_npm_install_args(libexec)
+ bin.install_symlink Dir[libexec/"bin/*"]
+ end
+
+ test do
+ system "#{bin}/codeburn", "--version"
+ end
+end
+```
+
+Update the `url` and `sha256` fields with the new version's values.
+
+### 3. Test Locally
+
+Before pushing, test the formula locally:
+
+```bash
+brew install --build-from-source Formula/codeburn.rb
+codeburn --version
+```
+
+### 4. Commit and Push
+
+Commit the formula change:
+
+```bash
+git add Formula/codeburn.rb
+git commit -m "codeburn: bump to 0.9.8"
+git push origin main
+```
+
+Users can now install with:
+
+```bash
+brew tap getagentseal/codeburn
+brew install codeburn
+```
+
+Or upgrade an existing installation:
+
+```bash
+brew upgrade codeburn
+```
+
+## Replacing Assets on an Existing Release
+
+If a release is published with broken assets (e.g., a menubar zip with a build error), re-run the build and upload the fixed assets without creating a new tag.
+
+Use `gh release upload` with the `--clobber` flag to overwrite existing files:
+
+```bash
+# After re-running mac/Scripts/package-app.sh v0.9.8 to regenerate the zip and sha256
+gh release upload mac-v0.9.8 mac/.build/dist/CodeBurnMenubar-v0.9.8.zip --clobber
+gh release upload mac-v0.9.8 mac/.build/dist/CodeBurnMenubar-v0.9.8.zip.sha256 --clobber
+```
+
+The GitHub Release page will now serve the fixed assets. The menubar installer selects the newest `mac-v*` release with `CodeBurnMenubar-v*.zip` plus its checksum, so users who run `codeburn menubar --force` after the replacement get the fixed version automatically.
+
+## Rollback
+
+If a released version has a critical bug, the fastest path is to fix the bug and cut a new patch release (e.g., 0.9.8 -> 0.9.9). Delete the broken tag locally and on GitHub if it has not yet been widely distributed:
+
+```bash
+git tag -d v0.9.8
+git push origin --delete v0.9.8
+```
+
+npm does not allow republishing to the same version. If you must unpublish from npm, use `npm unpublish codeburn@0.9.8 --force` (requires Owner role), but this is discouraged and all users who installed that version retain it.
+
+For the menubar, tag a new mac-v0.9.9 and let the workflow build and upload it. Users will see the update pill in the menubar settings and upgrade automatically (or manually via `codeburn menubar --force`).
+
+## Summary
+
+The CLI release is manual: bump the version, update `CHANGELOG.md`, commit, run `npm publish`, then tag and create a GitHub Release. The macOS menubar release is automated: pushing a `mac-v*` tag fires `.github/workflows/release-menubar.yml`, which builds, signs, zips, and publishes the bundle. The Homebrew formula at `getagentseal/homebrew-codeburn` is updated by hand after each CLI publish.
diff --git a/SECURITY.md b/SECURITY.md
new file mode 100644
index 0000000..c94d7f9
--- /dev/null
+++ b/SECURITY.md
@@ -0,0 +1,21 @@
+# Security Policy
+
+## Reporting a Vulnerability
+
+Please report security vulnerabilities via [GitHub's private vulnerability reporting](https://github.com/getagentseal/codeburn/security/advisories/new).
+
+Do not open a public issue for security vulnerabilities.
+
+## Scope
+
+Security reports are welcome for:
+
+- The CLI (`src/`)
+- The menubar installer (`src/menubar-installer.ts`)
+- The macOS menubar app (`mac/`)
+- The desktop app (`desktop/`)
+- CI/CD workflows (`.github/workflows/`)
+
+## Release Integrity
+
+Menubar release assets include a `.sha256` checksum file. The installer verifies the checksum before extracting and launching the downloaded bundle.
diff --git a/assets/providers/antigravity.png b/assets/providers/antigravity.png
new file mode 100644
index 0000000..9a0e29f
Binary files /dev/null and b/assets/providers/antigravity.png differ
diff --git a/assets/providers/claude.jpg b/assets/providers/claude.jpg
new file mode 100644
index 0000000..93d3a98
Binary files /dev/null and b/assets/providers/claude.jpg differ
diff --git a/assets/providers/cline.svg b/assets/providers/cline.svg
new file mode 100644
index 0000000..d00094b
--- /dev/null
+++ b/assets/providers/cline.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/assets/providers/codex.png b/assets/providers/codex.png
new file mode 100644
index 0000000..5c8532a
Binary files /dev/null and b/assets/providers/codex.png differ
diff --git a/assets/providers/copilot.jpg b/assets/providers/copilot.jpg
new file mode 100644
index 0000000..3f28afe
Binary files /dev/null and b/assets/providers/copilot.jpg differ
diff --git a/assets/providers/crush.png b/assets/providers/crush.png
new file mode 100644
index 0000000..138a7ab
Binary files /dev/null and b/assets/providers/crush.png differ
diff --git a/assets/providers/cursor-agent.jpg b/assets/providers/cursor-agent.jpg
new file mode 100644
index 0000000..447a9ed
Binary files /dev/null and b/assets/providers/cursor-agent.jpg differ
diff --git a/assets/providers/cursor.jpg b/assets/providers/cursor.jpg
new file mode 100644
index 0000000..447a9ed
Binary files /dev/null and b/assets/providers/cursor.jpg differ
diff --git a/assets/providers/droid.png b/assets/providers/droid.png
new file mode 100644
index 0000000..0aab780
Binary files /dev/null and b/assets/providers/droid.png differ
diff --git a/assets/providers/gemini.png b/assets/providers/gemini.png
new file mode 100644
index 0000000..6c98e13
Binary files /dev/null and b/assets/providers/gemini.png differ
diff --git a/assets/providers/goose.png b/assets/providers/goose.png
new file mode 100644
index 0000000..757649e
Binary files /dev/null and b/assets/providers/goose.png differ
diff --git a/assets/providers/ibm-bob.svg b/assets/providers/ibm-bob.svg
new file mode 100644
index 0000000..ab76047
--- /dev/null
+++ b/assets/providers/ibm-bob.svg
@@ -0,0 +1,6 @@
+
+
+
+
+
+
diff --git a/assets/providers/kilo-code.png b/assets/providers/kilo-code.png
new file mode 100644
index 0000000..2be6018
Binary files /dev/null and b/assets/providers/kilo-code.png differ
diff --git a/assets/providers/kimi.svg b/assets/providers/kimi.svg
new file mode 100644
index 0000000..c09b36f
--- /dev/null
+++ b/assets/providers/kimi.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/assets/providers/kiro.png b/assets/providers/kiro.png
new file mode 100644
index 0000000..f997c66
Binary files /dev/null and b/assets/providers/kiro.png differ
diff --git a/assets/providers/mistral-vibe.svg b/assets/providers/mistral-vibe.svg
new file mode 100644
index 0000000..f70841a
--- /dev/null
+++ b/assets/providers/mistral-vibe.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/providers/omp.svg b/assets/providers/omp.svg
new file mode 100644
index 0000000..f1ccf2a
--- /dev/null
+++ b/assets/providers/omp.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/assets/providers/openclaw.jpg b/assets/providers/openclaw.jpg
new file mode 100644
index 0000000..0394c6f
Binary files /dev/null and b/assets/providers/openclaw.jpg differ
diff --git a/assets/providers/opencode.png b/assets/providers/opencode.png
new file mode 100644
index 0000000..605c441
Binary files /dev/null and b/assets/providers/opencode.png differ
diff --git a/assets/providers/pi.png b/assets/providers/pi.png
new file mode 100644
index 0000000..17bc550
Binary files /dev/null and b/assets/providers/pi.png differ
diff --git a/assets/providers/qwen.png b/assets/providers/qwen.png
new file mode 100644
index 0000000..a834c0f
Binary files /dev/null and b/assets/providers/qwen.png differ
diff --git a/assets/providers/roo-code.png b/assets/providers/roo-code.png
new file mode 100644
index 0000000..776a103
Binary files /dev/null and b/assets/providers/roo-code.png differ
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 0000000..5f31c7c
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,189 @@
+# CodeBurn Architecture
+
+A map of the codebase. Read this once before opening a non-trivial PR.
+
+## Three Surfaces
+
+CodeBurn is one Node.js CLI plus two GUI clients that shell out to it.
+
+```
++----------------------+ +-----------------+
+| mac/ (Swift) | ---> | |
++----------------------+ | src/cli.ts |
+| gnome/ (JavaScript) | ---> | (the CLI) |
++----------------------+ | |
+ | status |
+ | --format |
+ | menubar-json |
+ +-----------------+
+ |
+ v
+ +----------------------------+
+ | session files on disk |
+ | (JSONL, SQLite, protobuf) |
+ +----------------------------+
+```
+
+The macOS menubar (`mac/`) and the GNOME extension (`gnome/`) both invoke `codeburn status --format menubar-json --period ` and parse the JSON. They do not share code with the CLI; they only depend on its output contract.
+
+## CLI (`src/`)
+
+`src/cli.ts` is the Commander.js entry point. The bin field in `package.json` points at `dist/cli.js`. Twelve commands are registered:
+
+| Command | Line | Purpose |
+|---|---|---|
+| `report` | 274 | Default. Interactive Ink TUI dashboard. |
+| `status` | 358 | Compact text status, plus `--format menubar-json` for clients. |
+| `today` | 524 | Today-only view of `report`. |
+| `month` | 542 | Month-only view of `report`. |
+| `export` | 560 | CSV or JSON dump of usage data. |
+| `menubar` | 621 | Downloads and launches the macOS menubar bundle. |
+| `currency` | 636 | Sets display currency. |
+| `model-alias` | 687 | Maps an unknown model name to a known one for pricing. |
+| `plan` | 737 | Configures a subscription plan for overage tracking. |
+| `optimize` | 857 | Runs all 14 waste detectors. |
+| `compare` | 870 | Compares two models side by side. |
+| `yield` | 882 | Tracks which sessions shipped to main vs. were reverted (experimental). |
+
+### Pipeline
+
+```
+provider.discoverSessions()
+ |
+ v
+provider.createSessionParser(source, seenKeys)
+ |
+ v yields ParsedProviderCall (see src/providers/types.ts)
+ |
+ v
+src/parser.ts: parseAllSessions()
+ |
+ v aggregates into ProjectSummary[]
+ |
+ v
+src/daily-cache.ts: aggregate per day, persist
+ |
+ v
+output formatter (Ink TUI, JSON, or menubar-json)
+```
+
+`src/parser.ts` is the central aggregator. Public exports: `parseAllSessions`, `filterProjectsByName`, `extractMcpInventory`. It owns the dedup `Set` (`seenKeys`) that is passed into every provider parser so a turn that surfaces in two providers (Claude logs vs. Cursor mirror, for instance) is counted once.
+
+### Cache Layers
+
+Three caches under `~/.cache/codeburn/` (override with `CODEBURN_CACHE_DIR`):
+
+| File | Owner | Invalidation |
+|---|---|---|
+| `codex-results.json` | `src/codex-cache.ts` | `mtimeMs + sizeBytes` per Codex `.jsonl`. |
+| `cursor-results.json` | `src/cursor-cache.ts` | `mtimeMs + sizeBytes` of the Cursor SQLite db. |
+| `daily-cache.json` | `src/daily-cache.ts` | Tracks `lastComputedDate`; new days are backfilled, old days are reused. |
+
+All three use atomic write (temp file + `rename`) and write with mode `0o600`. All three carry a numeric `version` field; bumping it forces a recompute next run.
+
+### Optimize Detectors
+
+`src/optimize.ts` exports 14 detectors. Each returns a `WasteFinding | null`. They are composed by `runOptimize()` which collects findings, ranks them by impact, and returns them with `WasteAction` objects (paste-to-CLAUDE.md, paste-to-session-opener, prompt-now, edit shell config).
+
+| Detector | Line | What it catches |
+|---|---|---|
+| `detectJunkReads` | 428 | Reads into `node_modules`, `.git`, `dist`, etc. |
+| `detectDuplicateReads` | 477 | Re-reads of the same file in a session. |
+| `detectMcpToolCoverage` | 795 | MCP servers with many tools but low usage. |
+| `detectUnusedMcp` | 855 | MCP servers configured but never invoked. |
+| `detectBloatedClaudeMd` | 944 | `CLAUDE.md` files past a healthy size. |
+| `detectLowReadEditRatio` | 987 | Edit-heavy sessions with too few prior reads. |
+| `detectCacheBloat` | 1048 | High `cache_creation_input_tokens`. |
+| `detectGhostAgents` | 1124 | Defined but never-invoked Claude agents. |
+| `detectGhostSkills` | 1154 | Defined but never-invoked skills. |
+| `detectGhostCommands` | 1184 | Defined but never-invoked slash commands. |
+| `detectBashBloat` | 1228 | Shell output limit set above the recommended 15K chars. |
+| `detectLowWorthSessions` | 1405 | Sessions with cost but no edits or git delivery. |
+| `detectContextBloat` | 1512 | Input:output token ratio above 25:1. |
+| `detectSessionOutliers` | 1558 | Sessions costing more than 2x the project average. |
+
+### Output Formats
+
+| Command | `--format` choices | Default |
+|---|---|---|
+| `report`, `today`, `month` | `tui`, `json` | `tui` |
+| `status` | `terminal`, `menubar-json`, `json` | `terminal` |
+| `export` | `csv`, `json` | `csv` |
+| `plan` | `text`, `json` | `text` |
+
+The macOS menubar and GNOME extension consume `menubar-json`. `src/menubar-json.ts` defines the contract; `tests/menubar-json.test.ts` pins it.
+
+## Providers (`src/providers/`)
+
+Every provider implements the `Provider` interface in `src/providers/types.ts`:
+
+```ts
+type Provider = {
+ name: string
+ displayName: string
+ modelDisplayName(model: string): string
+ toolDisplayName(rawTool: string): string
+ discoverSessions(): Promise
+ createSessionParser(source: SessionSource, seenKeys: Set): SessionParser
+}
+```
+
+`src/providers/index.ts` registers twenty-one providers across two tiers:
+
+- **Eager**: `claude`, `cline`, `codex`, `copilot`, `droid`, `gemini`, `ibm-bob`, `kilo-code`, `kiro`, `kimi`, `openclaw`, `pi`, `omp`, `qwen`, `roo-code`. Imported at module load.
+- **Lazy**: `antigravity`, `goose`, `cursor`, `opencode`, `cursor-agent`, `crush`. Imported via dynamic `import()` so the heavy dependencies (SQLite, protobuf) do not touch users who do not have those tools installed.
+
+Both lists hit the same `getAllProviders()` aggregator. A failed lazy import is silent and excludes that provider from the run.
+
+`src/providers/vscode-cline-parser.ts` is a shared helper consumed by `cline`, `ibm-bob`, `kilo-code`, and `roo-code`. It is not registered as a provider on its own.
+
+For the per-provider data location, storage format, parser quirks, and test coverage, see `docs/providers/`.
+
+## macOS Menubar (`mac/`)
+
+Swift package (`mac/Package.swift`), targets macOS 14, strict concurrency on. Layout under `mac/Sources/CodeBurnMenubar/`:
+
+- `CodeBurnApp.swift` boots the SwiftUI `App` and the `NSStatusItem`.
+- `AppStore.swift` is the single source of truth for UI state.
+- `Data/` holds models, the CLI client, credential stores, and subscription services.
+ - `DataClient.swift` spawns the CLI and decodes `MenubarPayload`. See file-level comment for why we never route through `/bin/zsh -c`.
+ - `MenubarPayload.swift` mirrors the JSON the CLI emits; keep it in sync with `src/menubar-json.ts`.
+- `Security/CodeburnCLI.swift` resolves the CLI binary (env override `CODEBURN_BIN`, fallback `codeburn`), validates each argv entry against an allowlist regex, and augments PATH for Homebrew and npm-global installs. The Process is launched via `/usr/bin/env`, never via a shell.
+- `Theme/` holds color and typography constants and the dark/light state.
+- `Views/` are the SwiftUI components rendered inside `NSPopover`.
+
+Tests live in `mac/Tests/CodeBurnMenubarTests/` (currently `CapacityEstimatorTests.swift`).
+
+The build artifact is a zipped `.app` bundle produced by `mac/Scripts/package-app.sh`. See `RELEASING.md` for how the GitHub Actions workflow uses it.
+
+## GNOME Extension (`gnome/`)
+
+Plain JavaScript, no bundler. Targets GNOME Shell 45-50 (`metadata.json`).
+
+- `extension.js` is the entry point. On `enable()` it constructs a `CodeBurnIndicator` and adds it to the panel.
+- `indicator.js` is the popover. It owns the period selector, the insight tabs, and the provider filter.
+- `dataClient.js` wraps `Gio.Subprocess` to call the CLI. It validates argv against the same allowlist pattern as the macOS client and augments PATH with `~/.local/bin`, `~/.npm-global/bin`, `~/.volta/bin`, `~/.bun/bin`, `~/.cargo/bin`, `~/.asdf/shims`, and a few others. Results are cached for 300 seconds.
+- `prefs.js` is the settings dialog backed by `schemas/org.gnome.shell.extensions.codeburn.gschema.xml`.
+- `install.sh` copies the extension into `~/.local/share/gnome-shell/extensions/`.
+
+## Build (`scripts/`, `tsup.config.ts`)
+
+`npm run build` is two steps:
+
+1. `node scripts/bundle-litellm.mjs` fetches the latest litellm pricing JSON and writes `src/data/litellm-snapshot.json`. The bundle script keeps a manual override for MiniMax variants. Direct (un-prefixed) entries win over prefixed ones. The result is checked in so the build is reproducible.
+2. `tsup` reads `tsup.config.ts` and emits a single ESM bundle at `dist/cli.js` with a Node shebang banner. No source maps in publish builds; sourcemaps on for development.
+
+The `prepublishOnly` hook in `package.json` runs `npm run build` so `npm publish` always ships fresh code.
+
+## Tests
+
+`npm test` runs vitest. Forty-two test files live under `tests/`:
+
+- `tests/` root (27 files) covers CLI, parser, optimize, cache, format, models, plans.
+- `tests/security/` (1 file) covers prototype-pollution guards.
+- `tests/providers/` (15 files) covers per-provider parsing.
+- `tests/fixtures/` holds redacted real-world session data.
+
+Five providers ship without dedicated test files today: `antigravity`, `claude`, `gemini`, `goose`, `qwen`. Closing this gap is a standing good-first-issue.
+
+CI runs Semgrep against `.semgrep/rules/no-bracket-assign-hot-paths.yml` over `src/providers/` and `src/parser.ts` (`.github/workflows/ci.yml`). It does not run vitest in CI today; tests run locally before publish.
diff --git a/docs/providers/README.md b/docs/providers/README.md
new file mode 100644
index 0000000..414289e
--- /dev/null
+++ b/docs/providers/README.md
@@ -0,0 +1,59 @@
+# Provider Docs
+
+One file per provider integration. If you are fixing a bug or adding a feature scoped to a single provider, read the file for that provider first; it tells you which file to edit, where on disk the source data lives, and what edge cases the test suite already covers.
+
+For the architectural picture, see `../architecture.md`.
+
+## Provider Index
+
+### Eager (always loaded)
+
+| Provider | Storage | Source | Test |
+|---|---|---|---|
+| [Claude](claude.md) | JSONL (no parser) | `src/providers/claude.ts` | none (covered indirectly) |
+| [Cline](cline.md) | JSON | `src/providers/cline.ts` | `tests/providers/cline.test.ts` |
+| [Codex](codex.md) | JSONL | `src/providers/codex.ts` | `tests/providers/codex.test.ts` |
+| [Copilot](copilot.md) | JSONL | `src/providers/copilot.ts` | `tests/providers/copilot.test.ts` |
+| [Droid](droid.md) | JSONL | `src/providers/droid.ts` | `tests/providers/droid.test.ts` |
+| [Gemini](gemini.md) | JSON / JSONL | `src/providers/gemini.ts` | none |
+| [IBM Bob](ibm-bob.md) | JSON | `src/providers/ibm-bob.ts` | `tests/providers/ibm-bob.test.ts` |
+| [KiloCode](kilo-code.md) | JSON | `src/providers/kilo-code.ts` | `tests/providers/kilo-code.test.ts` |
+| [Kiro](kiro.md) | JSON | `src/providers/kiro.ts` | `tests/providers/kiro.test.ts` |
+| [Kimi](kimi.md) | JSONL | `src/providers/kimi.ts` | `tests/providers/kimi.test.ts` |
+| [Mistral Vibe](mistral-vibe.md) | JSON / JSONL | `src/providers/mistral-vibe.ts` | `tests/providers/mistral-vibe.test.ts` |
+| [OpenClaw](openclaw.md) | JSONL | `src/providers/openclaw.ts` | `tests/providers/openclaw.test.ts` |
+| [Pi](pi.md) | JSONL | `src/providers/pi.ts` | `tests/providers/pi.test.ts` |
+| [OMP](omp.md) | JSONL | `src/providers/pi.ts` | `tests/providers/omp.test.ts` |
+| [Qwen](qwen.md) | JSONL | `src/providers/qwen.ts` | none |
+| [Roo Code](roo-code.md) | JSON | `src/providers/roo-code.ts` | `tests/providers/roo-code.test.ts` |
+
+### Lazy (loaded on first call)
+
+| Provider | Storage | Source | Test |
+|---|---|---|---|
+| [Antigravity](antigravity.md) | protobuf over RPC | `src/providers/antigravity.ts` | none |
+| [Crush](crush.md) | SQLite (per-project) | `src/providers/crush.ts` | `tests/providers/crush.test.ts` |
+| [Cursor](cursor.md) | SQLite | `src/providers/cursor.ts` | `tests/providers/cursor.test.ts` |
+| [Cursor Agent](cursor-agent.md) | text / JSONL | `src/providers/cursor-agent.ts` | `tests/providers/cursor-agent.test.ts` |
+| [Goose](goose.md) | SQLite | `src/providers/goose.ts` | none |
+| [OpenCode](opencode.md) | SQLite | `src/providers/opencode.ts` | `tests/providers/opencode.test.ts` |
+
+### Shared
+
+| Helper | Used by | Source |
+|---|---|---|
+| [vscode-cline-parser](vscode-cline-parser.md) | `cline`, `ibm-bob`, `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` |
+
+## File Format
+
+Each provider doc has the same structure:
+
+1. **One-line summary** of what the provider integrates.
+2. **Where it reads from** on disk (or over RPC).
+3. **Storage format** and validation rules.
+4. **Caching** (which cache layer, if any).
+5. **Deduplication key** so you understand cross-provider dedup.
+6. **Quirks** that have bitten us before.
+7. **When fixing a bug here** as a checklist.
+
+If you add a new provider, copy `claude.md` as a template and fill in your provider's specifics. Update this index, and prefer adding a real test fixture under `tests/providers/`.
diff --git a/docs/providers/antigravity.md b/docs/providers/antigravity.md
new file mode 100644
index 0000000..ca6d23f
--- /dev/null
+++ b/docs/providers/antigravity.md
@@ -0,0 +1,52 @@
+# Antigravity
+
+Google Antigravity. The only provider that does not read files off disk: it speaks to a local language-server RPC endpoint instead.
+
+- **Source:** `src/providers/antigravity.ts`
+- **Loading:** lazy via `src/providers/index.ts`. Lazy because the protobuf dependency is heavy.
+- **Test:** focused helper coverage in `tests/providers/antigravity.test.ts`.
+
+## Where it reads from
+
+A local HTTPS RPC endpoint exposed by Antigravity's language server. The parser:
+
+1. Locates the running language-server process via `ps` on POSIX or
+ `Get-CimInstance Win32_Process` on Windows.
+2. Reads its port and CSRF token from process metadata.
+3. Calls `GetCascadeTrajectoryGeneratorMetadata` over HTTPS.
+4. Validates the response (capped at 16 MB).
+
+Antigravity exposes slightly different process flags across platforms:
+POSIX builds have used `--https_server_port` and `--csrf_token`; Windows
+builds can expose `--extension_server_port` and
+`--extension_server_csrf_token`. Both space-separated and `--flag=value`
+forms are supported.
+
+If the language server is not running, the parser falls back to the cached results file.
+
+## Storage format
+
+Protobuf. Cascade and response objects map to `ParsedProviderCall` directly.
+
+## Caching
+
+Custom file cache at `$CODEBURN_CACHE_DIR/antigravity-results.json` (defaults to `~/.cache/codeburn/`). The cache is also used as the data source when the RPC endpoint is unavailable, not just as an optimization. Bumping the cache version forces a recompute.
+
+## Deduplication
+
+Per `:`.
+
+## Quirks
+
+- **Antigravity is the only provider that requires a live process.** A user who closes Antigravity loses the most-recent data until next launch (the cache covers older runs).
+- The 16 MB cap on RPC responses is necessary because individual cascades can balloon. Raising it risks OOM on the user's machine.
+- Token types are split across `inputTokens`, `responseOutputTokens`, and `thinkingOutputTokens`. Thinking is billed at output rate.
+
+## When fixing a bug here
+
+1. Reproducing the full provider path requires Antigravity running locally.
+ The unit tests cover process flag parsing and wrapped/unwrapped RPC response
+ extraction, but they do not stand up a live Antigravity RPC endpoint.
+2. Before any change, capture a sample protobuf response (anonymized) so future regressions can be tested against a recording.
+3. If the bug is "no data after Antigravity update", the protobuf schema may have shifted. The parser's response handling is the place to look.
+4. If the bug is "stale data", check whether the RPC is reachable; the cache fallback can mask connectivity issues.
diff --git a/docs/providers/claude.md b/docs/providers/claude.md
new file mode 100644
index 0000000..b5954c1
--- /dev/null
+++ b/docs/providers/claude.md
@@ -0,0 +1,54 @@
+# Claude
+
+Anthropic Claude Code CLI and Claude Desktop's local agent mode.
+
+- **Source:** `src/providers/claude.ts`
+- **Loading:** eager (`src/providers/index.ts:1`)
+- **Test:** none directly. Coverage comes from `tests/parser-claude-cwd.test.ts`, `tests/parser-filter.test.ts`, and `tests/parser-mcp-inventory.test.ts`, which exercise `src/parser.ts` end-to-end against fixture session files.
+
+## Where it reads from
+
+| Source | Path |
+|---|---|
+| Claude Code CLI | `$CLAUDE_CONFIG_DIR` if set, otherwise `~/.claude/projects/` |
+| Claude Desktop (macOS) | `~/Library/Application Support/Claude/local-agent-mode-sessions/` |
+| Claude Desktop (Windows) | `%APPDATA%/Claude/local-agent-mode-sessions/` |
+| Claude Desktop (Linux) | `~/.config/Claude/local-agent-mode-sessions/` |
+
+For Desktop, `findDesktopProjectDirs` walks up to 8 levels deep looking for `projects/` subdirectories, skipping `node_modules` and `.git`.
+
+## Storage format
+
+JSONL, one event per line, per session file. Sessions live under `/.jsonl`.
+
+## Parser
+
+`createSessionParser` returns an empty async generator (`claude.ts:101-105`). Claude is a special case: `src/parser.ts` reads Claude JSONL files directly with full turn grouping, dedup of streaming message IDs, and MCP tool inventory extraction. The provider object exists only so `discoverSessions` can return Claude session sources alongside the others.
+
+## Pricing
+
+Claude Code reports total cache-write tokens in `usage.cache_creation_input_tokens`.
+When available, it also splits those writes by duration in
+`usage.cache_creation.ephemeral_5m_input_tokens` and
+`usage.cache_creation.ephemeral_1h_input_tokens`. CodeBurn keeps the existing
+aggregate cache-write token total for reports, but prices the 1-hour portion at
+2x base input cost (1.6x the 5-minute cache-write rate exposed by LiteLLM).
+If the split fields are missing, the parser falls back to the legacy behavior
+and prices every cache write at the 5-minute rate.
+
+## Caching
+
+None at the provider level. The daily aggregation cache (`src/daily-cache.ts`) reuses prior computed days.
+
+## Quirks
+
+- The parser is in `src/parser.ts`, not in `src/providers/claude.ts`. Anything that changes Claude parsing belongs in `parser.ts`.
+- Streaming responses produce duplicate message IDs across resumed sessions; `parser.ts` strips them via the global `seenMsgIds` Set.
+- Model display names are mapped in `claude.ts:7-20`; add new versions there when Anthropic releases them.
+
+## When fixing a bug here
+
+1. Confirm whether the bug is in **discovery** (sessions not picked up) or **parsing** (sessions found but data wrong).
+2. Discovery bugs live in `claude.ts:78-99`. Verify the directory layout you expect actually matches what Claude writes today.
+3. Parsing bugs live in `src/parser.ts`. Look for `parseSessionFile`, `groupIntoTurns`, and `dedupeStreamingMessageIds`.
+4. Add a fixture under `tests/fixtures/` and a test under `tests/parser-claude-cwd.test.ts` (or a new file). Do not mock the filesystem.
diff --git a/docs/providers/cline.md b/docs/providers/cline.md
new file mode 100644
index 0000000..65f27ea
--- /dev/null
+++ b/docs/providers/cline.md
@@ -0,0 +1,50 @@
+# Cline
+
+Cline VS Code extension and Cline home-data task storage.
+
+- **Source:** `src/providers/cline.ts`
+- **Loading:** eager (`src/providers/index.ts:2`)
+- **Test:** `tests/providers/cline.test.ts`
+
+## Where it reads from
+
+Two task roots are scanned:
+
+1. VS Code extension globalStorage for `saoudrizwan.claude-dev`.
+2. Cline's home-data root at `~/.cline/data`.
+
+Both roots are expected to contain a `tasks/` child directory. Discovery is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`, so a task is only included when it has a `ui_messages.json` file.
+
+## Storage format
+
+Per-task directories with:
+
+```
+tasks//
+ ui_messages.json
+ api_conversation_history.json
+ task_metadata.json
+```
+
+`ui_messages.json` provides the `api_req_started` usage entries. `api_conversation_history.json` is used for model extraction. See [`vscode-cline-parser`](vscode-cline-parser.md) for the full schema description.
+`task_metadata.json` is part of Cline's task layout but is not read by CodeBurn today.
+
+## Caching
+
+None at the provider level; delegates to the shared helper and normal parser/cache layers.
+
+## Deduplication
+
+Discovery deduplicates by task id across the two Cline roots so a migrated task is not scanned twice. If the same task id exists in multiple roots, the one with the newest `ui_messages.json` wins. Parsing still uses the shared per-call key: `::`.
+
+## Quirks
+
+- This provider is intentionally a thin wrapper over the shared Cline-family parser.
+- Cline can keep data in both VS Code globalStorage and `~/.cline/data`, depending on version and workflow.
+- If Cline changes the JSON shape, fix `vscode-cline-parser.ts` only if Roo Code and KiloCode still pass. Branch provider-specific parsing rather than duplicating the whole parser.
+
+## When fixing a bug here
+
+1. Reproduce with a minimal task directory containing `ui_messages.json` and `api_conversation_history.json`.
+2. Run `tests/providers/cline.test.ts`, plus `tests/providers/roo-code.test.ts` and `tests/providers/kilo-code.test.ts` if the shared parser changes.
+3. Keep the provider name `cline`; downstream filters and dedup keys depend on it.
diff --git a/docs/providers/codex.md b/docs/providers/codex.md
new file mode 100644
index 0000000..268fd35
--- /dev/null
+++ b/docs/providers/codex.md
@@ -0,0 +1,50 @@
+# Codex
+
+OpenAI Codex CLI.
+
+- **Source:** `src/providers/codex.ts`
+- **Loading:** eager (`src/providers/index.ts:2`)
+- **Test:** `tests/providers/codex.test.ts` (374 lines)
+
+## Where it reads from
+
+`$CODEX_HOME` if set, otherwise `~/.codex`. Sessions are nested by date:
+
+```
+~/.codex/sessions////rollout-*.jsonl
+```
+
+The discovery walk uses strict regex (`^\d{4}$`, `^\d{2}$`) on each path component.
+
+## Storage format
+
+JSONL. The first line must be a `session_meta` entry with `payload.originator` starting with `codex` (case-insensitive). Files that fail this check are silently skipped.
+
+The first line read is capped at 1 MB (`FIRST_LINE_READ_CAP`). Codex CLI 0.128+ embeds the full system prompt in `session_meta`, which can run 20-27 KB; the cap leaves headroom while bounding memory if a corrupt file has no newline.
+
+## Caching
+
+`src/codex-cache.ts` writes `~/.cache/codeburn/codex-results.json` (or `$CODEBURN_CACHE_DIR/codex-results.json`). Each entry is keyed by absolute file path and validated against `mtimeMs + sizeBytes`. Cached entries are returned wholesale.
+
+A session that yielded zero parseable lines does **not** write to the cache (`codex.ts:419`); this prevents a transient read failure from pinning an empty result against a fingerprint.
+
+## Deduplication
+
+`codex:::` for accounted events, plus `codex:::est` for estimated events that fall back to char-counting.
+
+## Quirks
+
+- Codex CLI emits both `last_token_usage` (per turn) and `total_token_usage` (cumulative). The parser handles three modes:
+ 1. `last_token_usage` present: use it directly.
+ 2. Only cumulative: compute deltas against the prior turn.
+ 3. Neither: estimate from message text length (`CHARS_PER_TOKEN = 4`).
+- `prevCumulativeTotal` is initialized to `null`, not `0`. A session whose first event reports `total = 0` would otherwise be dropped as a "duplicate" of the initial state.
+- `prev*` token counters are advanced on **every** event, including ones that used `last_token_usage`. Earlier code only updated them on the fallback branch, which double-counted any session that mixed modes.
+- OpenAI counts cached tokens **inside** `input_tokens`. The parser subtracts them so the rest of the codebase can assume Anthropic semantics (cached are separate).
+
+## When fixing a bug here
+
+1. Reproduce against a real `rollout-*.jsonl` if you can. Drop a redacted copy under `tests/fixtures/codex/` and reference it from `tests/providers/codex.test.ts`.
+2. If the bug is "zero tokens reported", first check whether the file is being skipped by `isValidCodexSession`.
+3. If the bug is "tokens counted twice", look at `prevCumulativeTotal` and the prev-counter advancement.
+4. If you change the dedup key shape, run `tests/providers/codex.test.ts` and `tests/parser-filter.test.ts` together; cross-provider dedup happens via the global `seenKeys` Set.
diff --git a/docs/providers/copilot.md b/docs/providers/copilot.md
new file mode 100644
index 0000000..a02198e
--- /dev/null
+++ b/docs/providers/copilot.md
@@ -0,0 +1,49 @@
+# Copilot
+
+GitHub Copilot Chat (CLI and VS Code extension transcripts).
+
+- **Source:** `src/providers/copilot.ts`
+- **Loading:** eager (`src/providers/index.ts:3`)
+- **Test:** `tests/providers/copilot.test.ts` (401 lines)
+
+## Where it reads from
+
+Two locations. Both are walked on every run; results merge.
+
+1. **Legacy CLI sessions:** `~/.copilot/session-state/`
+2. **VS Code transcripts:** `~/Library/Application Support/Code/User/workspaceStorage//GitHub.copilot-chat/transcripts/` and equivalents on Windows / Linux
+
+## Storage format
+
+JSONL in both locations, but the schemas differ. The parser switches by detecting which schema the first event uses (`copilot.ts:83-159` for legacy, `copilot.ts:215-293` for transcripts).
+
+## Caching
+
+None at the provider level.
+
+## Deduplication
+
+Per `messageId` in both formats (`copilot.ts:118` for legacy, `copilot.ts:245` for transcripts).
+
+## Model inference
+
+Copilot does not always tag the model on each message. The parser infers it from the tool-call ID prefix:
+
+| Prefix | Inferred model family |
+|---|---|
+| `toolu_bdrk_`, `toolu_vrtx_`, `tooluse_`, `toolu_` | Anthropic |
+| `call_` | OpenAI |
+
+See `copilot.ts:176-213`.
+
+## Quirks
+
+- `toolRequests` can be missing or non-array on older sessions; the parser guards against that (`copilot.ts:126`, `:260`).
+- When `outputTokens` is missing the parser falls back to char-counting (`CHARS_PER_TOKEN = 4`, `copilot.ts:252-254`).
+- A single chat may be mirrored across both legacy and transcript paths if the user upgraded; the dedup `messageId` collision handles this.
+
+## When fixing a bug here
+
+1. Determine which schema reproduces the bug. The two parsers share little code on purpose; do not unify them unless you understand both formats.
+2. If the model is misidentified, look at the tool-call ID prefix list and consider whether a new prefix should be added.
+3. New fixtures go under `tests/fixtures/copilot/` and are referenced from `tests/providers/copilot.test.ts`.
diff --git a/docs/providers/crush.md b/docs/providers/crush.md
new file mode 100644
index 0000000..b293002
--- /dev/null
+++ b/docs/providers/crush.md
@@ -0,0 +1,87 @@
+# Crush
+
+Charmbracelet's Crush TUI coding agent.
+
+- **Source:** `src/providers/crush.ts`
+- **Loading:** lazy (`src/providers/index.ts`). Lazy because Crush ships per-project SQLite databases and we use `node:sqlite` to read them.
+- **Test:** `tests/providers/crush.test.ts` (10 tests, fixture-based)
+
+## Where it reads from
+
+Crush keeps a global registry that lists every project it has touched, and a separate SQLite database **per project**.
+
+| File | Path |
+|---|---|
+| Registry (project list) | `$CRUSH_GLOBAL_DATA/projects.json`, otherwise `$XDG_DATA_HOME/crush/projects.json`, otherwise `~/.local/share/crush/projects.json` (Linux/macOS) or `%LOCALAPPDATA%/crush/projects.json` (Windows). |
+| Per-project db | `//crush.db` where `data_dir` defaults to `.crush`. |
+
+The registry shape is an object keyed by project id (modern Crush) or an array (older builds and tokscale's sample fixtures). The parser accepts both.
+
+## Storage format
+
+SQLite. Schema verified against `charmbracelet/crush` v0.66.1 (`internal/db/migrations/20250424200609_initial.sql` plus subsequent additive migrations).
+
+Two tables matter for codeburn:
+
+```sql
+CREATE TABLE sessions (
+ id TEXT PRIMARY KEY,
+ parent_session_id TEXT,
+ title TEXT NOT NULL,
+ message_count INTEGER NOT NULL DEFAULT 0,
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
+ cost REAL NOT NULL DEFAULT 0.0,
+ updated_at INTEGER NOT NULL,
+ created_at INTEGER NOT NULL,
+ ...
+);
+
+CREATE TABLE messages (
+ id TEXT PRIMARY KEY,
+ session_id TEXT NOT NULL,
+ role TEXT NOT NULL,
+ parts TEXT NOT NULL DEFAULT '[]',
+ model TEXT,
+ created_at INTEGER NOT NULL,
+ updated_at INTEGER NOT NULL,
+ ...
+);
+```
+
+## Caching
+
+None at the provider level.
+
+## Deduplication
+
+Per `crush:` (`crush.ts`).
+
+## What we extract
+
+| codeburn field | Crush source |
+|---|---|
+| `inputTokens` | `sessions.prompt_tokens` |
+| `outputTokens` | `sessions.completion_tokens` |
+| `costUSD` | `sessions.cost` (already in dollars) |
+| `model` | dominant value of `messages.model` for the session, picked by `GROUP BY model ORDER BY COUNT(*) DESC LIMIT 1`. Falls back to `unknown`. |
+| `timestamp` | `sessions.updated_at` if set, otherwise `created_at` |
+
+Cache tokens, reasoning tokens, web-search counts, tools, and bash commands are all left as zero / empty. Crush does not record per-message token data, so per-turn attribution is not available.
+
+## Quirks worth knowing
+
+- **Timestamps are seconds, not milliseconds.** The Crush schema *comments* in the upstream migration claim millisecond timestamps, but every actual `INSERT`/`UPDATE` in `internal/db/sql/{sessions,messages}.sql` uses `strftime('%s', 'now')`, which returns Unix seconds. The parser multiplies by 1000 before constructing a `Date`. **Tokscale's parser (junhoyeo/tokscale#346) gets this wrong and is off by 1000x.** Confirmed against Crush v0.66.1.
+- **Cost is stored in dollars as a `REAL`.** No conversion needed.
+- **Child sessions are skipped.** Only rows with `parent_session_id IS NULL` are surfaced. Crush sub-agents inherit cost into the parent.
+- **Zero-spend rows are filtered.** Discovery skips sessions with `cost = 0 AND prompt_tokens = 0 AND completion_tokens = 0`.
+- **Optimize detectors that depend on tools (`detectJunkReads`, `detectDuplicateReads`, `detectLowReadEditRatio`) will not flag Crush sessions.** That is correct: Crush does not log per-tool calls in a way we can read today.
+- **`detectLowWorthSessions` may flag Crush sessions** because it looks for cost without edits. That is a known false positive; if it becomes noisy, we can branch the detector on provider.
+
+## When fixing a bug here
+
+1. Confirm the issue against a real Crush install (`brew install charmbracelet/tap/crush`) before assuming the schema has changed. Migrations in the last six months have only added columns to `sessions`/`messages`, never removed any of the ones we read.
+2. If the bug is "Crush sessions show timestamps from 1970-something", check whether someone "fixed" the seconds-vs-milliseconds handling by removing the `* 1000`. The schema comment is wrong; the data is in seconds.
+3. If the bug is "Crush model column shows `unknown`", the session has no messages with a non-null `model`. Some early Crush builds did not record provider on every message; add `LIKE` matching against `provider` if you want a stronger fallback.
+4. If the bug is "no sessions discovered", the registry path probably has not been verified for the user's setup. Print `getRegistryPath()` and have them confirm the file exists at that location.
+5. New fixtures go under the inline schema in `tests/providers/crush.test.ts`; keep the `CREATE TABLE` literal and synchronized with the upstream migration.
diff --git a/docs/providers/cursor-agent.md b/docs/providers/cursor-agent.md
new file mode 100644
index 0000000..d77775b
--- /dev/null
+++ b/docs/providers/cursor-agent.md
@@ -0,0 +1,41 @@
+# Cursor Agent
+
+Cursor's background agent transcripts (separate from the regular chat).
+
+- **Source:** `src/providers/cursor-agent.ts`
+- **Loading:** lazy (`src/providers/index.ts:62-87`)
+- **Test:** `tests/providers/cursor-agent.test.ts` (243 lines)
+
+## Where it reads from
+
+`~/.cursor/projects//agent-transcripts/`. Inside each project, two layouts coexist:
+
+1. **Legacy:** `*.txt` flat files.
+2. **Composer 2:** UUID-named subdirectories, each containing JSONL.
+
+Subagents (delegated runs) live in `subagents/` subdirectories under the parent (`cursor-agent.ts:479-490`). They are picked up too.
+
+## Storage format
+
+- Legacy: free-form text transcripts. The parser does line-based heuristic parsing (`cursor-agent.ts:219-314`).
+- Composer 2: JSONL (`cursor-agent.ts:167-217`).
+
+## Caching
+
+None at the provider level. Conversation metadata is read from the same Cursor SQLite db (`state.vscdb`), specifically the `conversation_summaries` table (`cursor-agent.ts:46-50`). If the summary is missing, file mtime is used as the timestamp.
+
+## Deduplication
+
+Per `::` (`cursor-agent.ts:379`).
+
+## Quirks
+
+- A file with a UUID-shaped name is treated as the conversation ID directly (`cursor-agent.ts:142-143`); other names are derived from the parent directory.
+- Token counts are estimated from char count (`CHARS_PER_TOKEN = 4`, `cursor-agent.ts:35`, `:81-84`). The legacy text format never reports real tokens.
+- The text parser is regex-driven and brittle. It is easier to fix a Composer 2 (JSONL) bug than a legacy (text) bug.
+
+## When fixing a bug here
+
+1. Check which format the failing transcript uses before opening a fix.
+2. For text-format bugs, copy the redacted transcript verbatim into `tests/fixtures/cursor-agent/` so the regex change can be regression-tested.
+3. If the bug is "wrong project", look at `cursor-agent.ts:46-50` and whether a `conversation_summaries` row exists for the conversation.
diff --git a/docs/providers/cursor.md b/docs/providers/cursor.md
new file mode 100644
index 0000000..8ccf6c4
--- /dev/null
+++ b/docs/providers/cursor.md
@@ -0,0 +1,50 @@
+# Cursor
+
+Cursor IDE chat history.
+
+- **Source:** `src/providers/cursor.ts`
+- **Loading:** lazy (`src/providers/index.ts:44-57`). The `node:sqlite` import is the heavy dependency that justifies lazy loading.
+- **Test:** `tests/providers/cursor.test.ts` (77 lines), `tests/providers/cursor-bubble-dedup.test.ts` (176 lines)
+
+## Where it reads from
+
+A single SQLite database per platform:
+
+| Platform | Path |
+|---|---|
+| macOS | `~/Library/Application Support/Cursor/User/globalStorage/state.vscdb` |
+| Windows | `%APPDATA%/Cursor/User/globalStorage/state.vscdb` |
+| Linux | `~/.config/Cursor/User/globalStorage/state.vscdb` |
+
+## Storage format
+
+SQLite. Two parallel sources within the same db:
+
+1. **Bubbles** (`cursor.ts:201-331`): per-message rows. The richer source.
+2. **agentKv** (`cursor.ts:350-460`): per-conversation key-value blobs. The fallback for older sessions.
+
+The parser tries both and dedupes via `seenKeys`.
+
+## Caching
+
+`src/cursor-cache.ts` writes `~/.cache/codeburn/cursor-results.json` (override with `$CODEBURN_CACHE_DIR`). The fingerprint is `dbMtimeMs + dbSizeBytes` of `state.vscdb`. Atomic write via temp + rename.
+
+## Deduplication
+
+- Bubbles: per `bubbleId` (`cursor.ts:282`).
+- agentKv: per `requestId` (`cursor.ts:429`).
+
+## Quirks
+
+- **180-day lookback.** The bubbles query bounds itself to the trailing 180 days (`cursor.ts:205`). Older history is ignored. If a user reports "Cursor data missing", confirm the date range first.
+- **250 000 bubble cap.** Power users with massive history are capped to prevent unbounded memory. If you need to raise this, also raise the cache size budget.
+- **Per-conversation user-message queue.** The parser caches the user-message stream per conversation to avoid an O(n) shift on every turn (`cursor.ts:171-191`).
+- **agentKv has no per-message timestamp.** The DB file's mtime is used as the timestamp for every agentKv-derived call (`cursor.ts:358-363`). This is wrong but consistent.
+- **Cursor v3 reports zero token counts.** The parser falls back to char-counting (`CHARS_PER_TOKEN = 4`) for those rows (`cursor.ts:265-272`).
+
+## When fixing a bug here
+
+1. **Always reproduce against a fixture, not a real db.** SQLite over the live db is racy; the user might be using Cursor while you read.
+2. If the bug is "tokens are zero", check whether the row is a v3 zero-token bubble, in which case the char-fallback should kick in.
+3. If the bug is "duplicate counts", check both `bubbleId` dedup and the cross-provider `seenKeys` dedup.
+4. Cache poisoning is the most common failure mode after a Cursor schema change. Bump `CURSOR_CACHE_VERSION` in `src/cursor-cache.ts` so old caches are invalidated.
diff --git a/docs/providers/droid.md b/docs/providers/droid.md
new file mode 100644
index 0000000..b8288e5
--- /dev/null
+++ b/docs/providers/droid.md
@@ -0,0 +1,36 @@
+# Droid
+
+Factory's Droid CLI.
+
+- **Source:** `src/providers/droid.ts`
+- **Loading:** eager (`src/providers/index.ts:4`)
+- **Test:** `tests/providers/droid.test.ts` (148 lines)
+
+## Where it reads from
+
+`$FACTORY_DIR` if set, otherwise `~/.factory/sessions//*.jsonl`.
+
+The parser ignores the `.factory/` directory itself (`droid.ts:293-296`); some installs nest it accidentally.
+
+## Storage format
+
+JSONL.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `messageId` within a session (`droid.ts:253`).
+
+## Quirks
+
+- **Token totals are session-level only.** Droid does not report per-message tokens. The parser reads `settings.tokenUsage` once per session and **splits it evenly** across all assistant calls, with the remainder added to the last call (`droid.ts:223-251`). This is approximate but consistent.
+- Project name is derived from the session's `cwd`. If the cwd contains `projects/`, that name is preferred over the basename (`droid.ts:299-319`).
+
+## When fixing a bug here
+
+1. If the bug is "tokens unevenly attributed", that is by design. The session-level total is the only signal Droid emits.
+2. If the bug is "no sessions found", confirm the user does not have `$FACTORY_DIR` pointing somewhere unexpected.
+3. New fixtures go under `tests/fixtures/droid/`.
diff --git a/docs/providers/gemini.md b/docs/providers/gemini.md
new file mode 100644
index 0000000..b411d23
--- /dev/null
+++ b/docs/providers/gemini.md
@@ -0,0 +1,35 @@
+# Gemini
+
+Google Gemini CLI.
+
+- **Source:** `src/providers/gemini.ts`
+- **Loading:** eager (`src/providers/index.ts:5`)
+- **Test:** none. Adding a fixture-based test is a known good first issue.
+
+## Where it reads from
+
+`~/.gemini/tmp//chats/session-*.json` and `session-*.jsonl` (`gemini.ts:218-252`).
+
+## Storage format
+
+Either a single JSON document per session or JSONL, depending on Gemini CLI version. The parser sniffs the first non-whitespace character to decide (`gemini.ts:197-206`).
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `sessionId` (`gemini.ts:72`). Gemini sessions are aggregated to a single call per session.
+
+## Quirks
+
+- **Cached tokens are a subset of input.** Gemini reports cached tokens included inside `promptTokenCount`. The parser subtracts them so callers see Anthropic semantics (cached are separate).
+- **Thoughts are billed at output rate** (`gemini.ts:125`).
+- Each session collapses to one `ParsedProviderCall`. If you need per-turn data, the upstream format does not support it without re-parsing the prompt history.
+
+## When fixing a bug here
+
+1. The lack of a test file is a hazard. **Add a fixture and a test before changing parsing logic** so future regressions are caught.
+2. If the bug involves a new Gemini version's schema, sniff with the same first-character heuristic; do not call `JSON.parse` on the whole file.
+3. If the bug is "Gemini sessions report less than expected", check whether the cached-token subtraction is over-correcting.
diff --git a/docs/providers/goose.md b/docs/providers/goose.md
new file mode 100644
index 0000000..d203d55
--- /dev/null
+++ b/docs/providers/goose.md
@@ -0,0 +1,42 @@
+# Goose
+
+Block's Goose CLI.
+
+- **Source:** `src/providers/goose.ts`
+- **Loading:** lazy (`src/providers/index.ts:29-42`)
+- **Test:** none. Adding a fixture-based test is a known good first issue.
+
+## Where it reads from
+
+A SQLite database. Path resolution honors `XDG_DATA_HOME` and a `GOOSE_PATH_ROOT` override:
+
+| Platform | Default path |
+|---|---|
+| macOS / Linux | `~/.local/share/goose/sessions/sessions.db` |
+| Windows | `%APPDATA%/Block/goose/sessions/sessions.db` |
+
+See `goose.ts:52-62`.
+
+## Storage format
+
+SQLite.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `sessionId` (`goose.ts:174`).
+
+## Quirks
+
+- Source paths are encoded as `:` so a single db can yield many session sources. The discovery code splits on the last colon (`goose.ts:148-150`).
+- Tool inventory comes from the `messages` table queried with `LIKE '%toolRequest%'` (`goose.ts:90`). This will miss tools whose payloads are encoded differently in a future Goose version.
+- Tokens are read directly from `accumulated_input_tokens` and `accumulated_output_tokens`. No estimation.
+
+## When fixing a bug here
+
+1. Add a fixture-based test before changing logic. `tests/providers/goose.test.ts` does not exist yet; create it and use a small SQLite file under `tests/fixtures/goose/`.
+2. If the bug is "no sessions", check `XDG_DATA_HOME` and `GOOSE_PATH_ROOT` first; users on non-default Linux setups will not match the default path.
+3. The `LIKE '%toolRequest%'` query is fragile. If Goose changes the message envelope, this is where it will break.
diff --git a/docs/providers/ibm-bob.md b/docs/providers/ibm-bob.md
new file mode 100644
index 0000000..c9d4373
--- /dev/null
+++ b/docs/providers/ibm-bob.md
@@ -0,0 +1,55 @@
+# IBM Bob
+
+IBM Bob IDE task history.
+
+- **Source:** `src/providers/ibm-bob.ts`
+- **Loading:** eager (`src/providers/index.ts`)
+- **Test:** `tests/providers/ibm-bob.test.ts`
+
+## Where It Reads From
+
+IBM Bob stores IDE task history below `User/globalStorage/ibm.bob-code/tasks/` in the application data directory.
+
+Default paths checked:
+
+| Platform | Paths |
+|---|---|
+| macOS | `~/Library/Application Support/IBM Bob/User/globalStorage/ibm.bob-code/`, `~/Library/Application Support/Bob-IDE/User/globalStorage/ibm.bob-code/` |
+| Windows | `%APPDATA%/IBM Bob/User/globalStorage/ibm.bob-code/`, `%APPDATA%/Bob-IDE/User/globalStorage/ibm.bob-code/` |
+| Linux | `$XDG_CONFIG_HOME/IBM Bob/User/globalStorage/ibm.bob-code/`, `$XDG_CONFIG_HOME/Bob-IDE/User/globalStorage/ibm.bob-code/` with `~/.config` fallback |
+
+The `Bob-IDE` paths cover the preview-era app name that some installs used before the GA `IBM Bob` directory.
+
+## Storage Format
+
+Each task is a directory under `tasks//` and must contain `ui_messages.json`.
+
+CodeBurn parses the same Cline-family UI event format used by Roo Code and KiloCode:
+
+- `ui_messages.json` entries with `type: "say"` and `say: "api_req_started"` contain serialized token/cost metrics.
+- `ui_messages.json` user text entries seed the turn's first user message.
+- `api_conversation_history.json` is optional and is used to extract the selected model from `... ` environment details when present.
+- `task_metadata.json` may exist upstream, but CodeBurn does not need it for usage math today.
+
+If no model tag is present, the parser uses `ibm-bob-auto`, which is priced through the same conservative Sonnet fallback used for Cline-family auto modes.
+
+## Caching
+
+None at the provider level.
+
+## Deduplication
+
+Per `::` via `vscode-cline-parser.ts`.
+
+## Quirks
+
+- IBM Bob has shipped under both `IBM Bob` and `Bob-IDE` application data folder names.
+- This provider intentionally covers the IDE task-history format. Bob Shell's `~/.bob` checkpoint data is a separate storage surface and is not parsed until we have a stable usage schema fixture.
+- The shared Cline parser does not currently extract individual tool names from UI messages, so tool breakdowns are empty for IBM Bob just like Roo Code and KiloCode.
+
+## When Fixing A Bug Here
+
+1. Check whether the install uses `IBM Bob` or `Bob-IDE` as the application data directory.
+2. Confirm the task folder still contains `ui_messages.json` and `api_conversation_history.json`.
+3. If the UI message schema changed, add a focused fixture to `tests/providers/ibm-bob.test.ts`.
+4. If the change also affects Roo Code or KiloCode, update `src/providers/vscode-cline-parser.ts` and run all three provider test files.
diff --git a/docs/providers/kilo-code.md b/docs/providers/kilo-code.md
new file mode 100644
index 0000000..51527ef
--- /dev/null
+++ b/docs/providers/kilo-code.md
@@ -0,0 +1,34 @@
+# KiloCode
+
+KiloCode VS Code extension.
+
+- **Source:** `src/providers/kilo-code.ts`
+- **Loading:** eager (`src/providers/index.ts:6`)
+- **Test:** `tests/providers/kilo-code.test.ts` (62 lines)
+
+## Where it reads from
+
+VS Code extension globalStorage for `kilocode.kilo-code` (extension ID set at `kilo-code.ts:4`). The actual walk is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`.
+
+## Storage format
+
+Per-task directories with `ui_messages.json` and `api_conversation_history.json`. See [`vscode-cline-parser`](vscode-cline-parser.md) for the full schema description.
+
+## Caching
+
+None at the provider level; delegates to the shared helper.
+
+## Deduplication
+
+Delegated. Per `::` (handled in `vscode-cline-parser.ts:109`).
+
+## Quirks
+
+- This file is a thin wrapper. Almost every bug for KiloCode actually lives in `vscode-cline-parser.ts`.
+- The VS Code extension wrappers using the Cline-family parser differ **only** by extension ID.
+
+## When fixing a bug here
+
+1. If the bug is "Cline, KiloCode, and Roo Code all broken in the same way", fix it in `vscode-cline-parser.ts`.
+2. If the bug is "KiloCode broken, Roo Code fine", the difference is upstream (KiloCode's emitted JSON differs slightly). Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
+3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
diff --git a/docs/providers/kimi.md b/docs/providers/kimi.md
new file mode 100644
index 0000000..19d6876
--- /dev/null
+++ b/docs/providers/kimi.md
@@ -0,0 +1,62 @@
+# Kimi
+
+Kimi Code CLI session parser.
+
+- **Source:** `src/providers/kimi.ts`
+- **Loading:** eager (`src/providers/index.ts`)
+- **Test:** `tests/providers/kimi.test.ts`
+
+## Where it reads from
+
+`$KIMI_SHARE_DIR/sessions/` if set, otherwise `~/.kimi/sessions/`.
+
+Kimi stores sessions by work-directory hash:
+
+```text
+~/.kimi/
+ kimi.json
+ config.toml
+ sessions/
+ /
+ /
+ context.jsonl
+ wire.jsonl
+ state.json
+ subagents/
+ /
+ context.jsonl
+ wire.jsonl
+```
+
+`kimi.json` maps each work-directory hash back to the original working path. CodeBurn uses that to display the project basename; if the metadata file is missing, the hash directory name is used.
+
+## Storage Format
+
+CodeBurn reads `wire.jsonl`. Each data line is a persisted wire record:
+
+```json
+{"timestamp":1776162403,"message":{"type":"StatusUpdate","payload":{"message_id":"msg-1","token_usage":{"input_other":100,"input_cache_read":25,"input_cache_creation":10,"output":40}}}}
+```
+
+`TurnBegin` / `SteerInput` provide the user prompt, `ToolCall` / `ToolCallRequest` provide tool names and shell commands, and `StatusUpdate.token_usage` provides the billable token counts.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `kimi::`, falling back to the status-update line index if the message id is absent.
+
+## Quirks
+
+- Kimi's official `TokenUsage` separates `input_other`, `input_cache_read`, `input_cache_creation`, and `output`. CodeBurn maps those directly into input, cache read, cache write, and output.
+- The current Kimi wire schema does not persist the model on every usage update. CodeBurn uses `KIMI_MODEL_NAME` when set, then the active `~/.kimi/config.toml` default model, then `kimi-auto`.
+- `kimi-auto`, `kimi-code`, and `kimi-for-coding` are priced as `kimi-k2-thinking` so managed Kimi Code sessions do not show as `$0` when the exact backend model is hidden.
+- Subagent sessions are discovered from `subagents//wire.jsonl` and parsed as separate Kimi sessions under the same project.
+
+## When Fixing A Bug Here
+
+1. Reproduce with a tiny `wire.jsonl` fixture in `tests/providers/kimi.test.ts`.
+2. If token totals look wrong, inspect `StatusUpdate.token_usage` first; `context.jsonl` only stores context checkpoints and cumulative counts, not per-step billing detail.
+3. If tools are missing, check whether Kimi emitted `ToolCall`, `ToolCallRequest`, or nested `SubagentEvent`; CodeBurn intentionally counts subagent wire files separately to avoid double-counting parent mirrors.
diff --git a/docs/providers/kiro.md b/docs/providers/kiro.md
new file mode 100644
index 0000000..0c450fb
--- /dev/null
+++ b/docs/providers/kiro.md
@@ -0,0 +1,44 @@
+# Kiro
+
+Kiro IDE chat history.
+
+- **Source:** `src/providers/kiro.ts`
+- **Loading:** eager (`src/providers/index.ts:7`)
+- **Test:** `tests/providers/kiro.test.ts` (328 lines)
+
+## Where it reads from
+
+VS Code-style globalStorage at `kiro.kiroagent`:
+
+| Platform | Path |
+|---|---|
+| macOS | `~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent` |
+| Windows | `%APPDATA%/Kiro/User/globalStorage/kiro.kiroagent` |
+| Linux | `~/.config/Kiro/User/globalStorage/kiro.kiroagent` |
+
+Sessions are `.chat` files under hash-named subdirectories. Discovery is in `kiro.ts:215-247`; the path-resolution helpers it uses start at `kiro.ts:164`.
+
+## Storage format
+
+JSON `.chat` files (`kiro.ts:153`).
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `executionId` (`kiro.ts:104`).
+
+## Quirks
+
+- **Workspace hash resolution** is non-trivial. The parser tries `workspace.json` first; if that fails, it base64-decodes the directory name to recover the workspace path (`kiro.ts:198-213`).
+- **Model ID normalization.** Kiro stores models like `claude-1.2`; the parser rewrites the dot to a hyphen so they match `claude-1-2` in the pricing snapshot (`kiro.ts:65-67`). Add new versions here when Kiro ships them.
+- **Tool name extraction is regex-driven.** Kiro embeds tool calls inside the message text as `... ` (`kiro.ts:69-78`). Brittle but unavoidable until Kiro emits structured tool data.
+- Token counts are estimated via char count (`CHARS_PER_TOKEN = 4`, `kiro.ts:9`, `:108-109`).
+
+## When fixing a bug here
+
+1. If the bug is "wrong workspace", check the base64 fallback path. Some users name their workspaces with characters that are not valid base64.
+2. If the bug is "missing model in pricing", add the model to the normalization map at `kiro.ts:65-67` and verify against `tests/providers/kiro.test.ts`.
+3. If the bug is "tools missing", look at the regex at `kiro.ts:69-78`. Kiro changes its envelope occasionally.
diff --git a/docs/providers/mistral-vibe.md b/docs/providers/mistral-vibe.md
new file mode 100644
index 0000000..c7005f7
--- /dev/null
+++ b/docs/providers/mistral-vibe.md
@@ -0,0 +1,41 @@
+# Mistral Vibe
+
+Mistral Vibe CLI.
+
+- **Source:** `src/providers/mistral-vibe.ts`
+- **Loading:** eager (`src/providers/index.ts`)
+- **Test:** `tests/providers/mistral-vibe.test.ts`
+
+## Where it reads from
+
+`$VIBE_HOME/logs/session/` when `VIBE_HOME` is set, otherwise `~/.vibe/logs/session/`.
+
+## Storage format
+
+Vibe 2.x stores each session as a directory:
+
+- `meta.json` contains session metadata, cumulative token totals, active model config, model prices, timestamps, working directory, and available tools.
+- `messages.jsonl` contains non-system messages and assistant `tool_calls`.
+
+Subagent traces are stored under a parent session's `agents/` folder with the same `meta.json` / `messages.jsonl` shape, so CodeBurn scans those one level down as separate sessions.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `mistral-vibe:`.
+
+## Quirks
+
+- **Usage is cumulative per session.** Vibe does not write per-assistant-message token usage into `messages.jsonl`; token counts come from `meta.json.stats.session_prompt_tokens` and `session_completion_tokens`. CodeBurn emits one usage record per Vibe session.
+- **Cost prefers Vibe's own model prices.** `meta.json.stats.input_price_per_million` and `output_price_per_million` are used first, with the active model config as a fallback. LiteLLM pricing is only used when Vibe provides no price data.
+- **Project names come from metadata.** Discovery uses `meta.json.environment.working_directory` and falls back to the session directory name if that field is missing.
+- **Tool calls come from messages.** Assistant `tool_calls[*].function.name` is normalized to the standard CodeBurn names (`bash` to `Bash`, `search_replace` to `Edit`, etc.). Bash commands are extracted from `function.arguments.command`.
+
+## When fixing a bug here
+
+1. Reproduce with a fixture that has both `meta.json` and `messages.jsonl`; both files are required for current Vibe sessions.
+2. If the bug is "wrong total", check `meta.json.stats` first. `messages.jsonl` is only for prompts and tool calls.
+3. If a future Vibe release adds per-turn usage, add tests before changing the one-record-per-session behavior so historical sessions continue to parse correctly.
diff --git a/docs/providers/omp.md b/docs/providers/omp.md
new file mode 100644
index 0000000..4546a2f
--- /dev/null
+++ b/docs/providers/omp.md
@@ -0,0 +1,34 @@
+# OMP
+
+OMP CLI. Same parser as Pi, different data directory.
+
+- **Source:** `src/providers/pi.ts` (the `omp` export)
+- **Loading:** eager (`src/providers/index.ts:9`)
+- **Test:** `tests/providers/omp.test.ts` (225 lines)
+
+## Where it reads from
+
+`~/.omp/agent/sessions/` (`pi.ts:59-61`).
+
+## Storage format
+
+JSONL, identical schema to Pi.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Identical to Pi: `::` with timestamp / line-index fallbacks (`pi.ts:164`).
+
+## Quirks
+
+- OMP and Pi share the **same** `createParser` function. The provider object differs only in name, displayName, and the discovery directory.
+- If OMP and Pi diverge in a future release, do **not** copy-paste the parser. Add a discriminator to `createParser` and branch.
+
+## When fixing a bug here
+
+1. Check if the bug also reproduces against Pi. If yes, fix both with one change; the parser is shared.
+2. If the bug is OMP-specific, the right fix is usually to pass an option into `createParser` rather than to fork the file.
+3. Read [`pi.md`](pi.md) for the parser-level details.
diff --git a/docs/providers/openclaw.md b/docs/providers/openclaw.md
new file mode 100644
index 0000000..255b736
--- /dev/null
+++ b/docs/providers/openclaw.md
@@ -0,0 +1,41 @@
+# OpenClaw
+
+OpenClaw, plus the older Clawdbot / Moltbot / Moldbot lineage.
+
+- **Source:** `src/providers/openclaw.ts`
+- **Loading:** eager (`src/providers/index.ts:8`)
+- **Test:** `tests/providers/openclaw.test.ts` (192 lines)
+
+## Where it reads from
+
+Four directories, all checked on every run (`openclaw.ts:62-70`):
+
+- `~/.openclaw/agents`
+- `~/.clawdbot/agents`
+- `~/.moltbot/agents`
+- `~/.moldbot/agents`
+
+The legacy directories are kept for users who upgraded from older builds.
+
+## Storage format
+
+JSONL (`openclaw.ts:242`). Each agents directory has a `sessions.json` index file plus per-session `.jsonl` files. The parser reads the index when present and falls back to a directory scan if it is missing or stale (`openclaw.ts:220-247`).
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `:` (`openclaw.ts:169`).
+
+## Quirks
+
+- **Cost is preferred from the provider when reported.** OpenClaw emits `costUSD` in `message.usage`; the parser uses it directly when present (`openclaw.ts:174-177`) and only computes from tokens when it is missing.
+- Tokens are reported across `input`, `output`, `cacheRead`, and `cacheWrite`. Anthropic semantics throughout, no normalization needed.
+
+## When fixing a bug here
+
+1. If the bug is "session not found", check the four legacy dirs. A user might have a stray `~/.moltbot/` that the parser is reading instead of the real `~/.openclaw/`.
+2. If the bug is "wrong cost", confirm whether `costUSD` is present in the source data; the parser trusts it over its own calculation.
+3. The `sessions.json` index can drift when the user crashes mid-session. Make sure the directory-scan fallback triggers in those cases.
diff --git a/docs/providers/opencode.md b/docs/providers/opencode.md
new file mode 100644
index 0000000..0148cc9
--- /dev/null
+++ b/docs/providers/opencode.md
@@ -0,0 +1,40 @@
+# OpenCode
+
+OpenCode (sst/opencode).
+
+- **Source:** `src/providers/opencode.ts`
+- **Loading:** lazy (`src/providers/index.ts:59-75`)
+- **Test:** `tests/providers/opencode.test.ts` (676 lines, the largest provider test)
+
+## Where it reads from
+
+Default `~/.local/share/opencode/` or `$XDG_DATA_HOME/opencode/`. The discovery walk picks up `opencode*.db` files (`opencode.ts:71-88`).
+
+## Storage format
+
+SQLite.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `:`.
+
+## Quirks
+
+- **Schema validation is loud.** When a required table is missing, the parser logs an actionable warning telling the user which table is gone and what version of OpenCode it expects. This is the right behavior; do not silently swallow these.
+- Source paths are encoded as `:`.
+- Each message's `parts` are indexed; preserving the order matters for reasoning-token correctness.
+- Tokens are reported across `input`, `output`, `reasoning`, `cache.read`, and `cache.write`. Anthropic semantics.
+- External MCP tools are stored as `_` names (for example
+ `clickup_clickup_get_task`). The provider normalizes those to CodeBurn's
+ canonical `mcp____` names before aggregation so shared MCP
+ panels and `optimize` findings count OpenCode usage.
+
+## When fixing a bug here
+
+1. The 558-line test suite catches a lot. Run `npm test -- tests/providers/opencode.test.ts` before and after any change.
+2. If the bug is "missing table" warning, do not catch and silence it. Either upgrade the version expectation in the parser or document the breaking schema change.
+3. If the bug is "reasoning tokens off by one", check the parts index ordering.
diff --git a/docs/providers/pi.md b/docs/providers/pi.md
new file mode 100644
index 0000000..9427226
--- /dev/null
+++ b/docs/providers/pi.md
@@ -0,0 +1,35 @@
+# Pi
+
+Pi agent CLI.
+
+- **Source:** `src/providers/pi.ts`
+- **Loading:** eager (`src/providers/index.ts:9`)
+- **Test:** `tests/providers/pi.test.ts` (336 lines)
+
+## Where it reads from
+
+`~/.pi/agent/sessions/` (`pi.ts:55-57`).
+
+## Storage format
+
+JSONL (`pi.ts:98`).
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `::` when a response ID is present, falling back to the entry timestamp, and finally to a line index (`pi.ts:164`).
+
+## Quirks
+
+- Undefined token fields in `message.usage` are coerced to `0` (`pi.ts:156-159`); never `undefined`.
+- The provider name is taken from `source.provider` (`pi.ts:182`), not hard-coded. This matters because `pi.ts` is the parser for **both** Pi and OMP; see [`omp.md`](omp.md).
+- Tool-call content type is extracted from the message envelope (`pi.ts:169-176`).
+
+## When fixing a bug here
+
+1. If you change parsing logic, also run `tests/providers/omp.test.ts` because OMP shares this code.
+2. If the bug is "tokens are NaN", look at the coercion at `pi.ts:156-159`. A regression on this is silent and easy to miss.
+3. If the bug is specific to the dedup behavior, decide which of the three fallback keys was used by adding a temporary log; the keys collide differently for old vs. new Pi versions.
diff --git a/docs/providers/qwen.md b/docs/providers/qwen.md
new file mode 100644
index 0000000..1970328
--- /dev/null
+++ b/docs/providers/qwen.md
@@ -0,0 +1,36 @@
+# Qwen
+
+Qwen Code CLI.
+
+- **Source:** `src/providers/qwen.ts`
+- **Loading:** eager (`src/providers/index.ts:10`)
+- **Test:** none. Adding a fixture-based test is a known good first issue.
+
+## Where it reads from
+
+`$QWEN_DATA_DIR` if set, otherwise `~/.qwen/projects//chats/*.jsonl` (`qwen.ts:52-54`).
+
+## Storage format
+
+JSONL.
+
+## Caching
+
+None.
+
+## Deduplication
+
+Per `:` (`qwen.ts:110`).
+
+## Quirks
+
+- **Project name comes from the last path component** (`qwen.ts:56-59`), not from any in-file field. If a user puts the same project under two different paths, they will appear as two projects.
+- **Thought parts are filtered out** before token accounting (`qwen.ts:97`). Qwen reports `thoughtsTokenCount` separately from `candidatesTokenCount`; this parser counts both as output but does not double-count thoughts in the main message.
+- **Tool calls** are extracted from a fixed envelope shape (`qwen.ts:61-76`). If Qwen restructures its tool-call format in a future release, this is where it will break first.
+- Tokens come from `usageMetadata`: `promptTokenCount`, `candidatesTokenCount`, `thoughtsTokenCount`, `cachedContentTokenCount`.
+
+## When fixing a bug here
+
+1. Add a fixture and a test before changing logic. The lack of `tests/providers/qwen.test.ts` makes regressions invisible.
+2. If the bug is "tools missing", look at the function-call extraction loop at `qwen.ts:61-76`.
+3. If the bug is "duplicate counts", confirm `:` actually uniquely identifies a turn in your reproducer; some Qwen builds repeat UUIDs across resumed sessions.
diff --git a/docs/providers/roo-code.md b/docs/providers/roo-code.md
new file mode 100644
index 0000000..e829064
--- /dev/null
+++ b/docs/providers/roo-code.md
@@ -0,0 +1,34 @@
+# Roo Code
+
+Roo Code VS Code extension.
+
+- **Source:** `src/providers/roo-code.ts`
+- **Loading:** eager (`src/providers/index.ts:11`)
+- **Test:** `tests/providers/roo-code.test.ts` (247 lines)
+
+## Where it reads from
+
+VS Code extension globalStorage for `rooveterinaryinc.roo-cline` (extension ID set at `roo-code.ts:4`). The actual walk is delegated to `discoverClineTasks` in `src/providers/vscode-cline-parser.ts`.
+
+## Storage format
+
+Per-task directories with `ui_messages.json` and `api_conversation_history.json`. See [`vscode-cline-parser`](vscode-cline-parser.md) for the schema.
+
+## Caching
+
+None at the provider level; delegates to the shared helper.
+
+## Deduplication
+
+Delegated. Per `::` (in `vscode-cline-parser.ts:109`).
+
+## Quirks
+
+- Thin wrapper. Almost every Roo Code bug actually lives in `vscode-cline-parser.ts`.
+- The VS Code extension wrappers using the Cline-family parser differ **only** by extension ID.
+
+## When fixing a bug here
+
+1. If the bug also reproduces against Cline or KiloCode, fix it in `vscode-cline-parser.ts`.
+2. If the bug is Roo Code-specific, the difference is upstream JSON shape. Reproduce with a fixture and consider whether the cline parser needs to branch on extension ID.
+3. Read [`vscode-cline-parser.md`](vscode-cline-parser.md) before editing.
diff --git a/docs/providers/vscode-cline-parser.md b/docs/providers/vscode-cline-parser.md
new file mode 100644
index 0000000..3535e63
--- /dev/null
+++ b/docs/providers/vscode-cline-parser.md
@@ -0,0 +1,50 @@
+# vscode-cline-parser (Shared Helper)
+
+Shared discovery and parsing for Cline and VS Code extensions descended from Cline.
+
+- **Source:** `src/providers/vscode-cline-parser.ts`
+- **Loading:** not a provider; imported by `cline.ts`, `ibm-bob.ts`, `kilo-code.ts`, and `roo-code.ts`.
+- **Test:** none directly. Coverage comes from `tests/providers/cline.test.ts`, `tests/providers/ibm-bob.test.ts`, `tests/providers/kilo-code.test.ts`, and `tests/providers/roo-code.test.ts`.
+
+## What it does
+
+Two responsibilities:
+
+1. `discoverClineTasks(extensionId)` walks a base directory's `tasks/` child and returns one source per task that has a `ui_messages.json` file (`vscode-cline-parser.ts:25-50`). Without an override directory it uses VS Code's `globalStorage//` path.
+2. `discoverClineTasksInBaseDirs(baseDirs)` does the same for non-VS Code apps with compatible task storage, such as IBM Bob.
+3. `createClineParser` reads each task's `ui_messages.json` and `api_conversation_history.json`, extracts model, tools, and token counts, and yields `ParsedProviderCall` objects.
+
+## Storage layout
+
+Per task directory:
+
+```
+/tasks//
+ ui_messages.json # event stream
+ api_conversation_history.json # full prompt history with model tags
+```
+
+## Model resolution
+
+The model is extracted from `api_conversation_history.json` by searching user message content blocks for a `... ` tag. Falls back to the provider-supplied auto model (`cline-auto` by default) if no tag is found.
+
+## Token extraction
+
+From `api_req_started` entries inside `ui_messages.json`. Each such entry's `text` field is JSON-parsed; the parsed object holds `tokensIn`, `tokensOut`, `cacheReads`, `cacheWrites`, and (optionally) `cost`.
+
+If `cost` is present, it is used directly. If not, `calculateCost` from `src/models.ts` computes it from tokens.
+
+## Deduplication
+
+Per `::` where `index` is the position of the `api_req_started` entry within `ui_messages.json`.
+
+## Quirks
+
+- Only the **first** user message is emitted as `userMessage` in the `ParsedProviderCall`. Subsequent user turns are accounted but not surfaced.
+- The model regex looks inside content blocks, not at top-level fields. Some Cline-derivative extensions emit the model elsewhere; if you add support for one, branch on extension ID rather than rewriting the regex.
+
+## When fixing a bug here
+
+1. A change here ripples to Cline, IBM Bob, KiloCode, and Roo Code. Run all four provider test files before opening a PR.
+2. If you find that one of the extensions emits a different shape, branch on the extension ID parameter that the discovery function already takes; do not duplicate the parser.
+3. If you add support for another Cline-family task store, register it as a thin wrapper file in the same shape as `cline.ts`, `ibm-bob.ts`, `kilo-code.ts`, and `roo-code.ts`.
diff --git a/docs/superpowers/plans/2026-04-19-model-comparison.md b/docs/superpowers/plans/2026-04-19-model-comparison.md
deleted file mode 100644
index 43d4ce3..0000000
--- a/docs/superpowers/plans/2026-04-19-model-comparison.md
+++ /dev/null
@@ -1,1169 +0,0 @@
-# Model Comparison Implementation Plan
-
-> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
-
-**Goal:** Let users pick any two AI models and see a fair, normalized side-by-side comparison of cost efficiency, edit reliability, and self-correction rates.
-
-**Architecture:** Pure data module (`compare-stats.ts`) handles aggregation and comparison logic. Ink TUI module (`compare.tsx`) handles model selection and results display. Accessible via `codeburn compare` standalone command and `c` shortcut in the dashboard.
-
-**Tech Stack:** TypeScript, React 19, Ink 7, vitest
-
----
-
-## File Structure
-
-```
-src/compare-stats.ts -- ModelStats type, aggregateModelStats(), computeComparison(),
- self-correction JSONL scanner. Pure data, no UI.
-src/compare.tsx -- ModelSelector, ComparisonResults, CompareView components.
- Exported renderCompare() for standalone command.
-tests/compare-stats.test.ts -- Unit tests for aggregation, comparison, edge cases.
-src/cli.ts -- Add `compare` command (modify ~line 650).
-src/dashboard.tsx -- Add 'compare' to View type, 'c' keybinding, CompareView
- render branch, StatusBar hint (modify ~5 locations).
-```
-
----
-
-### Task 1: ModelStats type and aggregateModelStats()
-
-**Files:**
-- Create: `src/compare-stats.ts`
-- Test: `tests/compare-stats.test.ts`
-
-- [ ] **Step 1: Write the failing test for aggregateModelStats**
-
-Create `tests/compare-stats.test.ts`:
-
-```ts
-import { describe, it, expect } from 'vitest'
-import { aggregateModelStats, type ModelStats } from '../src/compare-stats.js'
-import type { ProjectSummary, SessionSummary, ClassifiedTurn } from '../src/types.js'
-
-function makeTurn(model: string, cost: number, opts: { hasEdits?: boolean; retries?: number; outputTokens?: number; inputTokens?: number; cacheRead?: number; cacheWrite?: number; timestamp?: string } = {}): ClassifiedTurn {
- return {
- timestamp: opts.timestamp ?? '2026-04-15T10:00:00Z',
- category: 'coding',
- retries: opts.retries ?? 0,
- hasEdits: opts.hasEdits ?? false,
- userMessage: '',
- assistantCalls: [{
- provider: 'claude',
- model,
- usage: {
- inputTokens: opts.inputTokens ?? 100,
- outputTokens: opts.outputTokens ?? 200,
- cacheCreationInputTokens: opts.cacheWrite ?? 500,
- cacheReadInputTokens: opts.cacheRead ?? 5000,
- cachedInputTokens: 0,
- reasoningTokens: 0,
- webSearchRequests: 0,
- },
- costUSD: cost,
- tools: opts.hasEdits ? ['Edit'] : ['Read'],
- mcpTools: [],
- hasAgentSpawn: false,
- hasPlanMode: false,
- speed: 'standard' as const,
- timestamp: opts.timestamp ?? '2026-04-15T10:00:00Z',
- bashCommands: [],
- deduplicationKey: `key-${Math.random()}`,
- }],
- }
-}
-
-function makeProject(turns: ClassifiedTurn[]): ProjectSummary {
- const session: SessionSummary = {
- sessionId: 'test-session',
- project: 'test-project',
- firstTimestamp: turns[0]?.timestamp ?? '',
- lastTimestamp: turns[turns.length - 1]?.timestamp ?? '',
- totalCostUSD: turns.reduce((s, t) => s + t.assistantCalls.reduce((s2, c) => s2 + c.costUSD, 0), 0),
- totalInputTokens: 0,
- totalOutputTokens: 0,
- totalCacheReadTokens: 0,
- totalCacheWriteTokens: 0,
- apiCalls: turns.reduce((s, t) => s + t.assistantCalls.length, 0),
- turns,
- modelBreakdown: {},
- toolBreakdown: {},
- mcpBreakdown: {},
- bashBreakdown: {},
- categoryBreakdown: {} as SessionSummary['categoryBreakdown'],
- }
- return {
- project: 'test-project',
- projectPath: '/test',
- sessions: [session],
- totalCostUSD: session.totalCostUSD,
- totalApiCalls: session.apiCalls,
- }
-}
-
-describe('aggregateModelStats', () => {
- it('aggregates calls, cost, and tokens per model', () => {
- const project = makeProject([
- makeTurn('opus-4-6', 0.10, { outputTokens: 200, inputTokens: 50, cacheRead: 5000, cacheWrite: 500 }),
- makeTurn('opus-4-6', 0.15, { outputTokens: 300, inputTokens: 80, cacheRead: 6000, cacheWrite: 600 }),
- makeTurn('opus-4-7', 0.25, { outputTokens: 800, inputTokens: 100, cacheRead: 7000, cacheWrite: 700 }),
- ])
- const stats = aggregateModelStats([project])
- const m6 = stats.find(s => s.model === 'opus-4-6')!
- const m7 = stats.find(s => s.model === 'opus-4-7')!
-
- expect(m6.calls).toBe(2)
- expect(m6.cost).toBeCloseTo(0.25)
- expect(m6.outputTokens).toBe(500)
- expect(m7.calls).toBe(1)
- expect(m7.cost).toBeCloseTo(0.25)
- expect(m7.outputTokens).toBe(800)
- })
-
- it('attributes turn-level metrics to the primary model', () => {
- const project = makeProject([
- makeTurn('opus-4-6', 0.10, { hasEdits: true, retries: 0 }),
- makeTurn('opus-4-6', 0.10, { hasEdits: true, retries: 2 }),
- makeTurn('opus-4-7', 0.20, { hasEdits: true, retries: 0 }),
- makeTurn('opus-4-7', 0.20, { hasEdits: false }),
- ])
- const stats = aggregateModelStats([project])
- const m6 = stats.find(s => s.model === 'opus-4-6')!
- const m7 = stats.find(s => s.model === 'opus-4-7')!
-
- expect(m6.editTurns).toBe(2)
- expect(m6.oneShotTurns).toBe(1)
- expect(m6.retries).toBe(2)
- expect(m7.editTurns).toBe(1)
- expect(m7.oneShotTurns).toBe(1)
- expect(m7.totalTurns).toBe(2)
- })
-
- it('tracks firstSeen and lastSeen timestamps', () => {
- const project = makeProject([
- makeTurn('opus-4-6', 0.10, { timestamp: '2026-04-10T08:00:00Z' }),
- makeTurn('opus-4-6', 0.10, { timestamp: '2026-04-15T20:00:00Z' }),
- ])
- const stats = aggregateModelStats([project])
- const m = stats.find(s => s.model === 'opus-4-6')!
- expect(m.firstSeen).toBe('2026-04-10T08:00:00Z')
- expect(m.lastSeen).toBe('2026-04-15T20:00:00Z')
- })
-
- it('filters out model entries', () => {
- const project = makeProject([
- makeTurn('', 0, {}),
- makeTurn('opus-4-6', 0.10, {}),
- ])
- const stats = aggregateModelStats([project])
- expect(stats.find(s => s.model === '')).toBeUndefined()
- expect(stats).toHaveLength(1)
- })
-
- it('returns empty array for no projects', () => {
- expect(aggregateModelStats([])).toEqual([])
- })
-
- it('sorts by cost descending', () => {
- const project = makeProject([
- makeTurn('cheap-model', 0.01),
- makeTurn('expensive-model', 5.00),
- ])
- const stats = aggregateModelStats([project])
- expect(stats[0].model).toBe('expensive-model')
- expect(stats[1].model).toBe('cheap-model')
- })
-})
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: FAIL with "Cannot find module '../src/compare-stats.js'"
-
-- [ ] **Step 3: Write minimal implementation**
-
-Create `src/compare-stats.ts`:
-
-```ts
-import type { ProjectSummary } from './types.js'
-
-export type ModelStats = {
- model: string
- calls: number
- cost: number
- outputTokens: number
- inputTokens: number
- cacheReadTokens: number
- cacheWriteTokens: number
- totalTurns: number
- editTurns: number
- oneShotTurns: number
- retries: number
- selfCorrections: number
- firstSeen: string
- lastSeen: string
-}
-
-export function aggregateModelStats(projects: ProjectSummary[]): ModelStats[] {
- const byModel = new Map()
-
- const ensure = (model: string): ModelStats => {
- let s = byModel.get(model)
- if (!s) {
- s = { model, calls: 0, cost: 0, outputTokens: 0, inputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0, totalTurns: 0, editTurns: 0, oneShotTurns: 0, retries: 0, selfCorrections: 0, firstSeen: '', lastSeen: '' }
- byModel.set(model, s)
- }
- return s
- }
-
- for (const project of projects) {
- for (const session of project.sessions) {
- for (const turn of session.turns) {
- if (turn.assistantCalls.length === 0) continue
- const primaryModel = turn.assistantCalls[0].model
- if (primaryModel === '') continue
-
- const ms = ensure(primaryModel)
- ms.totalTurns++
- if (turn.hasEdits) ms.editTurns++
- if (turn.hasEdits && turn.retries === 0) ms.oneShotTurns++
- ms.retries += turn.retries
-
- for (const call of turn.assistantCalls) {
- if (call.model === '') continue
- const cs = call.model === primaryModel ? ms : ensure(call.model)
- cs.calls++
- cs.cost += call.costUSD
- cs.outputTokens += call.usage.outputTokens
- cs.inputTokens += call.usage.inputTokens
- cs.cacheReadTokens += call.usage.cacheReadInputTokens
- cs.cacheWriteTokens += call.usage.cacheCreationInputTokens
-
- if (!cs.firstSeen || call.timestamp < cs.firstSeen) cs.firstSeen = call.timestamp
- if (!cs.lastSeen || call.timestamp > cs.lastSeen) cs.lastSeen = call.timestamp
- }
- }
- }
- }
-
- return [...byModel.values()].sort((a, b) => b.cost - a.cost)
-}
-```
-
-- [ ] **Step 4: Run test to verify it passes**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: All 6 tests PASS
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/compare-stats.ts tests/compare-stats.test.ts
-git commit --author="iamtoruk " -m "feat(compare): add ModelStats type and aggregateModelStats"
-```
-
----
-
-### Task 2: computeComparison()
-
-**Files:**
-- Modify: `src/compare-stats.ts`
-- Modify: `tests/compare-stats.test.ts`
-
-- [ ] **Step 1: Write the failing test for computeComparison**
-
-Add to `tests/compare-stats.test.ts`:
-
-```ts
-import { computeComparison, type ComparisonRow } from '../src/compare-stats.js'
-
-function makeStats(model: string, overrides: Partial = {}): ModelStats {
- return {
- model,
- calls: 1000,
- cost: 100,
- outputTokens: 200000,
- inputTokens: 10000,
- cacheReadTokens: 500000,
- cacheWriteTokens: 50000,
- totalTurns: 500,
- editTurns: 100,
- oneShotTurns: 80,
- retries: 30,
- selfCorrections: 5,
- firstSeen: '2026-04-01T00:00:00Z',
- lastSeen: '2026-04-15T00:00:00Z',
- ...overrides,
- }
-}
-
-describe('computeComparison', () => {
- it('computes normalized metrics and picks winners', () => {
- const a = makeStats('opus-4-6', { calls: 1000, cost: 100, outputTokens: 200000 })
- const b = makeStats('opus-4-7', { calls: 500, cost: 100, outputTokens: 400000 })
- const rows = computeComparison(a, b)
-
- const costRow = rows.find(r => r.label === 'Cost / call')!
- expect(costRow.valueA).toBeCloseTo(0.10)
- expect(costRow.valueB).toBeCloseTo(0.20)
- expect(costRow.winner).toBe('a')
-
- const outputRow = rows.find(r => r.label === 'Output tok / call')!
- expect(outputRow.valueA).toBe(200)
- expect(outputRow.valueB).toBe(800)
- expect(outputRow.winner).toBe('a')
- })
-
- it('handles zero edit turns gracefully', () => {
- const a = makeStats('opus-4-6', { editTurns: 0, oneShotTurns: 0, retries: 0 })
- const b = makeStats('opus-4-7', { editTurns: 50, oneShotTurns: 40, retries: 15 })
- const rows = computeComparison(a, b)
-
- const osRow = rows.find(r => r.label === 'One-shot rate')!
- expect(osRow.valueA).toBeNull()
- expect(osRow.valueB).not.toBeNull()
- expect(osRow.winner).toBe('none')
- })
-
- it('returns tie when values are equal', () => {
- const a = makeStats('opus-4-6')
- const b = makeStats('opus-4-7')
- const rows = computeComparison(a, b)
- for (const row of rows) {
- expect(row.winner).toBe('tie')
- }
- })
-
- it('higher-is-better metrics pick the higher value', () => {
- const a = makeStats('opus-4-6', { cacheReadTokens: 900000, inputTokens: 10000, cacheWriteTokens: 90000 })
- const b = makeStats('opus-4-7', { cacheReadTokens: 500000, inputTokens: 10000, cacheWriteTokens: 490000 })
- const rows = computeComparison(a, b)
- const cacheRow = rows.find(r => r.label === 'Cache hit rate')!
- expect(cacheRow.winner).toBe('a')
- })
-})
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: FAIL with "computeComparison is not a function"
-
-- [ ] **Step 3: Write minimal implementation**
-
-Add to `src/compare-stats.ts`:
-
-```ts
-export type ComparisonRow = {
- label: string
- valueA: number | null
- valueB: number | null
- formatFn: 'cost' | 'number' | 'percent' | 'decimal'
- winner: 'a' | 'b' | 'tie' | 'none'
-}
-
-type MetricDef = {
- label: string
- extract: (s: ModelStats) => number | null
- format: ComparisonRow['formatFn']
- higherIsBetter: boolean
-}
-
-const METRICS: MetricDef[] = [
- {
- label: 'Cost / call',
- extract: s => s.calls > 0 ? s.cost / s.calls : null,
- format: 'cost',
- higherIsBetter: false,
- },
- {
- label: 'Output tok / call',
- extract: s => s.calls > 0 ? Math.round(s.outputTokens / s.calls) : null,
- format: 'number',
- higherIsBetter: false,
- },
- {
- label: 'Cache hit rate',
- extract: s => {
- const total = s.inputTokens + s.cacheReadTokens + s.cacheWriteTokens
- return total > 0 ? (s.cacheReadTokens / total) * 100 : null
- },
- format: 'percent',
- higherIsBetter: true,
- },
- {
- label: 'One-shot rate',
- extract: s => s.editTurns > 0 ? (s.oneShotTurns / s.editTurns) * 100 : null,
- format: 'percent',
- higherIsBetter: true,
- },
- {
- label: 'Retry rate',
- extract: s => s.editTurns > 0 ? s.retries / s.editTurns : null,
- format: 'decimal',
- higherIsBetter: false,
- },
- {
- label: 'Self-correction',
- extract: s => s.totalTurns > 0 ? (s.selfCorrections / s.totalTurns) * 100 : null,
- format: 'percent',
- higherIsBetter: false,
- },
-]
-
-function pickWinner(a: number | null, b: number | null, higherIsBetter: boolean): ComparisonRow['winner'] {
- if (a === null || b === null) return 'none'
- if (a === b) return 'tie'
- if (higherIsBetter) return a > b ? 'a' : 'b'
- return a < b ? 'a' : 'b'
-}
-
-export function computeComparison(a: ModelStats, b: ModelStats): ComparisonRow[] {
- return METRICS.map(m => {
- const valueA = m.extract(a)
- const valueB = m.extract(b)
- return {
- label: m.label,
- valueA,
- valueB,
- formatFn: m.format,
- winner: pickWinner(valueA, valueB, m.higherIsBetter),
- }
- })
-}
-```
-
-- [ ] **Step 4: Run test to verify it passes**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: All tests PASS
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/compare-stats.ts tests/compare-stats.test.ts
-git commit --author="iamtoruk " -m "feat(compare): add computeComparison with normalized metrics"
-```
-
----
-
-### Task 3: Self-correction JSONL scanner
-
-**Files:**
-- Modify: `src/compare-stats.ts`
-- Modify: `tests/compare-stats.test.ts`
-
-- [ ] **Step 1: Write the failing test**
-
-Add to `tests/compare-stats.test.ts`:
-
-```ts
-import { scanSelfCorrections } from '../src/compare-stats.js'
-import { writeFile, mkdir, rm } from 'fs/promises'
-import { tmpdir } from 'os'
-import { join } from 'path'
-import { afterEach, beforeEach } from 'vitest'
-
-const TMP_DIR = join(tmpdir(), `codeburn-compare-test-${Date.now()}`)
-
-function jsonlLine(type: string, model: string, text: string, timestamp = '2026-04-15T10:00:00Z'): string {
- if (type === 'assistant') {
- return JSON.stringify({
- type: 'assistant',
- timestamp,
- message: { model, content: [{ type: 'text', text }], id: `msg-${Math.random()}`, usage: { input_tokens: 0, output_tokens: 0 } },
- })
- }
- return JSON.stringify({ type: 'user', timestamp, message: { role: 'user', content: text } })
-}
-
-describe('scanSelfCorrections', () => {
- beforeEach(async () => {
- await mkdir(TMP_DIR, { recursive: true })
- })
-
- afterEach(async () => {
- await rm(TMP_DIR, { recursive: true, force: true })
- })
-
- it('counts apology patterns per model', async () => {
- const lines = [
- jsonlLine('user', '', 'fix this'),
- jsonlLine('assistant', 'opus-4-6', 'Sure, let me fix that.'),
- jsonlLine('assistant', 'opus-4-6', "I'm sorry, I made a mistake in the previous edit."),
- jsonlLine('assistant', 'opus-4-7', 'My bad, that was incorrect.'),
- jsonlLine('assistant', 'opus-4-7', 'Here is the correct version.'),
- ]
- await writeFile(join(TMP_DIR, 'session1.jsonl'), lines.join('\n'), 'utf-8')
-
- const counts = await scanSelfCorrections([TMP_DIR])
- expect(counts.get('opus-4-6')).toBe(1)
- expect(counts.get('opus-4-7')).toBe(1)
- })
-
- it('does not count non-apology text', async () => {
- const lines = [
- jsonlLine('assistant', 'opus-4-6', 'Everything looks good. The tests pass.'),
- jsonlLine('assistant', 'opus-4-6', 'I have fixed the bug successfully.'),
- ]
- await writeFile(join(TMP_DIR, 'session1.jsonl'), lines.join('\n'), 'utf-8')
-
- const counts = await scanSelfCorrections([TMP_DIR])
- expect(counts.get('opus-4-6') ?? 0).toBe(0)
- })
-
- it('handles missing or empty directories', async () => {
- const counts = await scanSelfCorrections(['/nonexistent/path'])
- expect(counts.size).toBe(0)
- })
-
- it('scans subagent directories', async () => {
- const subDir = join(TMP_DIR, 'abc123', 'subagents')
- await mkdir(subDir, { recursive: true })
- const lines = [
- jsonlLine('assistant', 'opus-4-7', "I apologize for the confusion."),
- ]
- await writeFile(join(subDir, 'sub1.jsonl'), lines.join('\n'), 'utf-8')
-
- const counts = await scanSelfCorrections([TMP_DIR])
- expect(counts.get('opus-4-7')).toBe(1)
- })
-})
-```
-
-- [ ] **Step 2: Run test to verify it fails**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: FAIL with "scanSelfCorrections is not exported"
-
-- [ ] **Step 3: Write the implementation**
-
-Add to `src/compare-stats.ts`:
-
-```ts
-import { readdir, readFile } from 'fs/promises'
-import { join } from 'path'
-
-const SELF_CORRECTION_PATTERNS = [
- /\bI('m| am) sorry\b/i,
- /\bmy mistake\b/i,
- /\bmy apolog/i,
- /\bI made (a |an )?(error|mistake)\b/i,
- /\bI was wrong\b/i,
- /\bmy bad\b/i,
- /\bI apologize\b/i,
- /\bsorry about that\b/i,
- /\bsorry for (the|that|this)\b/i,
- /\bI should have\b/i,
- /\bI shouldn't have\b/i,
- /\bI incorrectly\b/i,
- /\bI mistakenly\b/i,
-]
-
-function hasSelfCorrection(text: string): boolean {
- return SELF_CORRECTION_PATTERNS.some(p => p.test(text))
-}
-
-function extractAssistantText(entry: { message?: { content?: unknown } }): string {
- const content = entry.message?.content
- if (typeof content === 'string') return content
- if (Array.isArray(content)) {
- return content
- .filter((b: { type?: string; text?: string }) => b.type === 'text' && typeof b.text === 'string')
- .map((b: { text: string }) => b.text)
- .join(' ')
- }
- return ''
-}
-
-async function collectJsonlPaths(dirPath: string): Promise {
- const paths: string[] = []
- const files = await readdir(dirPath).catch(() => [])
- for (const f of files) {
- if (f.endsWith('.jsonl')) {
- paths.push(join(dirPath, f))
- } else {
- const subagents = join(dirPath, f, 'subagents')
- const subs = await readdir(subagents).catch(() => [])
- for (const sf of subs) {
- if (sf.endsWith('.jsonl')) paths.push(join(subagents, sf))
- }
- }
- }
- return paths
-}
-
-export async function scanSelfCorrections(sessionDirs: string[]): Promise> {
- const counts = new Map()
-
- for (const dir of sessionDirs) {
- const jsonlPaths = await collectJsonlPaths(dir)
- for (const filePath of jsonlPaths) {
- const content = await readFile(filePath, 'utf-8').catch(() => null)
- if (!content) continue
- for (const line of content.split('\n')) {
- if (!line.trim()) continue
- try {
- const entry = JSON.parse(line)
- if (entry.type !== 'assistant') continue
- const model = entry.message?.model
- if (!model || model === '') continue
- const text = extractAssistantText(entry)
- if (text && hasSelfCorrection(text)) {
- counts.set(model, (counts.get(model) ?? 0) + 1)
- }
- } catch {}
- }
- }
- }
-
- return counts
-}
-```
-
-- [ ] **Step 4: Run test to verify it passes**
-
-Run: `npx vitest run tests/compare-stats.test.ts`
-Expected: All tests PASS
-
-- [ ] **Step 5: Commit**
-
-```bash
-git add src/compare-stats.ts tests/compare-stats.test.ts
-git commit --author="iamtoruk " -m "feat(compare): add self-correction JSONL scanner"
-```
-
----
-
-### Task 4: ModelSelector Ink component
-
-**Files:**
-- Create: `src/compare.tsx`
-
-- [ ] **Step 1: Create the ModelSelector component**
-
-Create `src/compare.tsx`:
-
-```tsx
-import React, { useState } from 'react'
-import { Box, Text, useInput } from 'ink'
-
-import type { ModelStats, ComparisonRow } from './compare-stats.js'
-import { formatCost } from './format.js'
-
-const ORANGE = '#FF8C42'
-const GREEN = '#5BF5A0'
-const DIM = '#555555'
-const GOLD = '#FFD700'
-
-const LOW_DATA_THRESHOLD = 20
-
-type ModelSelectorProps = {
- models: ModelStats[]
- onSelect: (a: ModelStats, b: ModelStats) => void
- onBack: () => void
-}
-
-export function ModelSelector({ models, onSelect, onBack }: ModelSelectorProps) {
- const [cursor, setCursor] = useState(0)
- const [selected, setSelected] = useState>(new Set())
-
- useInput((input, key) => {
- if (input === 'q') { process.exit(0) }
- if (key.escape) { onBack(); return }
-
- if (key.upArrow) {
- setCursor(c => (c - 1 + models.length) % models.length)
- } else if (key.downArrow) {
- setCursor(c => (c + 1) % models.length)
- } else if (input === ' ') {
- setSelected(prev => {
- const next = new Set(prev)
- const model = models[cursor].model
- if (next.has(model)) {
- next.delete(model)
- } else if (next.size < 2) {
- next.add(model)
- }
- return next
- })
- } else if (key.return && selected.size === 2) {
- const picks = models.filter(m => selected.has(m.model))
- onSelect(picks[0], picks[1])
- }
- })
-
- return (
-
- Model Comparison
- {''}
- Select two models to compare:
- {''}
- {models.map((m, i) => {
- const isCursor = i === cursor
- const isSelected = selected.has(m.model)
- const isLowData = m.calls < LOW_DATA_THRESHOLD
- const prefix = isCursor ? '> ' : ' '
- const marker = isSelected ? ' [selected]' : ''
- const lowLabel = isLowData ? ' low data' : ''
- return (
-
-
- {prefix}{m.model.padEnd(28)}
-
- {String(m.calls.toLocaleString()).padStart(10)} calls
- {formatCost(m.cost).padStart(10)}
- {marker}
- {lowLabel}
-
- )
- })}
- {''}
-
- [space] select {selected.size === 2 ? [enter] compare : [enter] compare } [esc] back [q] quit
-
-
- )
-}
-```
-
-- [ ] **Step 2: Verify it compiles**
-
-Run: `npx tsx --eval "import './src/compare.js'" 2>&1 | head -5`
-Expected: No import errors (may warn about unused exports, that's fine)
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/compare.tsx
-git commit --author="iamtoruk " -m "feat(compare): add ModelSelector component"
-```
-
----
-
-### Task 5: ComparisonResults Ink component
-
-**Files:**
-- Modify: `src/compare.tsx`
-
-- [ ] **Step 1: Add the ComparisonResults component**
-
-Add to `src/compare.tsx`:
-
-```tsx
-type ComparisonResultsProps = {
- modelA: ModelStats
- modelB: ModelStats
- rows: ComparisonRow[]
- onBack: () => void
-}
-
-function formatValue(value: number | null, fmt: ComparisonRow['formatFn']): string {
- if (value === null) return '-'
- switch (fmt) {
- case 'cost': return '$' + value.toFixed(4)
- case 'number': return value.toLocaleString()
- case 'percent': return value.toFixed(1) + '%'
- case 'decimal': return value.toFixed(2)
- }
-}
-
-function shortName(model: string): string {
- return model.replace(/^claude-/, '')
-}
-
-function daysOfData(first: string, last: string): number {
- if (!first || !last) return 0
- const ms = new Date(last).getTime() - new Date(first).getTime()
- return Math.max(1, Math.ceil(ms / 86400000))
-}
-
-const LABEL_WIDTH = 20
-const VALUE_WIDTH = 14
-const WINNER_WIDTH = 12
-
-export function ComparisonResults({ modelA, modelB, rows, onBack }: ComparisonResultsProps) {
- const nameA = shortName(modelA.model)
- const nameB = shortName(modelB.model)
-
- useInput((input, key) => {
- if (input === 'q') process.exit(0)
- if (key.escape) onBack()
- })
-
- return (
-
- {modelA.model} vs {modelB.model}
- {''}
-
-
- {''.padEnd(LABEL_WIDTH)}{nameA.padStart(VALUE_WIDTH)}{nameB.padStart(VALUE_WIDTH)}
-
-
- {rows.map(row => (
-
- {' ' + row.label.padEnd(LABEL_WIDTH - 2)}
-
- {formatValue(row.valueA, row.formatFn).padStart(VALUE_WIDTH)}
-
-
- {formatValue(row.valueB, row.formatFn).padStart(VALUE_WIDTH)}
-
-
- {(row.winner === 'a' ? `${nameA} wins` : row.winner === 'b' ? `${nameB} wins` : row.winner === 'tie' ? 'tie' : '').padStart(WINNER_WIDTH)}
-
-
- ))}
-
- {''}
- {' ' + '\u2500'.repeat(LABEL_WIDTH + VALUE_WIDTH * 2 + WINNER_WIDTH - 4) + ' Context'}
-
-
- {' ' + 'Calls'.padEnd(LABEL_WIDTH - 2)}
- {modelA.calls.toLocaleString().padStart(VALUE_WIDTH)}
- {modelB.calls.toLocaleString().padStart(VALUE_WIDTH)}
-
-
- {' ' + 'Cost'.padEnd(LABEL_WIDTH - 2)}
- {formatCost(modelA.cost).padStart(VALUE_WIDTH)}
- {formatCost(modelB.cost).padStart(VALUE_WIDTH)}
-
-
- {' ' + 'Days of data'.padEnd(LABEL_WIDTH - 2)}
- {String(daysOfData(modelA.firstSeen, modelA.lastSeen)).padStart(VALUE_WIDTH)}
- {String(daysOfData(modelB.firstSeen, modelB.lastSeen)).padStart(VALUE_WIDTH)}
-
-
- {' ' + 'Edit turns'.padEnd(LABEL_WIDTH - 2)}
- {modelA.editTurns.toLocaleString().padStart(VALUE_WIDTH)}
- {modelB.editTurns.toLocaleString().padStart(VALUE_WIDTH)}
-
-
- {(modelA.calls < LOW_DATA_THRESHOLD || modelB.calls < LOW_DATA_THRESHOLD) && (
- <>
- {''}
- Note: {modelA.calls < LOW_DATA_THRESHOLD ? nameA : nameB} has limited data ({Math.min(modelA.calls, modelB.calls)} calls). Results may not be representative.
- >
- )}
-
- {''}
- [esc] back [q] quit
-
- )
-}
-```
-
-- [ ] **Step 2: Verify it compiles**
-
-Run: `npx tsx --eval "import './src/compare.js'" 2>&1 | head -5`
-Expected: No import errors
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/compare.tsx
-git commit --author="iamtoruk " -m "feat(compare): add ComparisonResults component"
-```
-
----
-
-### Task 6: CompareView orchestrator and renderCompare()
-
-**Files:**
-- Modify: `src/compare.tsx`
-
-- [ ] **Step 1: Add CompareView and renderCompare**
-
-Add to `src/compare.tsx`:
-
-```tsx
-import { render } from 'ink'
-
-import { aggregateModelStats, computeComparison, scanSelfCorrections } from './compare-stats.js'
-import { parseAllSessions } from './parser.js'
-import { getAllProviders } from './providers/index.js'
-import type { ProjectSummary, DateRange } from './types.js'
-
-type ComparePhase = 'select' | 'loading' | 'results'
-
-type CompareViewProps = {
- projects: ProjectSummary[]
- onBack: () => void
-}
-
-export function CompareView({ projects, onBack }: CompareViewProps) {
- const [phase, setPhase] = useState('select')
- const [models] = useState(() => aggregateModelStats(projects))
- const [pickedA, setPickedA] = useState(null)
- const [pickedB, setPickedB] = useState(null)
- const [rows, setRows] = useState([])
-
- if (models.length < 2) {
- return (
-
- Model Comparison
- {''}
- Need at least 2 models to compare. Found: {models.map(m => m.model).join(', ') || 'none'}
- {''}
- [esc] back [q] quit
-
- )
- }
-
- const handleSelect = async (a: ModelStats, b: ModelStats) => {
- setPickedA(a)
- setPickedB(b)
- setPhase('loading')
-
- const providers = await getAllProviders()
- const dirs: string[] = []
- for (const p of providers) {
- const sources = await p.discoverSessions()
- for (const s of sources) dirs.push(s.path)
- }
- const corrections = await scanSelfCorrections(dirs)
- a.selfCorrections = corrections.get(a.model) ?? 0
- b.selfCorrections = corrections.get(b.model) ?? 0
-
- setRows(computeComparison(a, b))
- setPhase('results')
- }
-
- if (phase === 'select') {
- return
- }
-
- if (phase === 'loading') {
- return (
-
- Comparing {pickedA?.model} vs {pickedB?.model}...
-
- )
- }
-
- return (
- setPhase('select')}
- />
- )
-}
-
-export async function renderCompare(
- range: DateRange,
- provider: string,
-): Promise {
- const projects = await parseAllSessions(range, provider)
- if (projects.length === 0) {
- console.log('\n No usage data found.\n')
- return
- }
-
- const isTTY = process.stdin.isTTY && process.stdout.isTTY
- if (!isTTY) {
- console.log('\n Model comparison requires an interactive terminal.\n')
- return
- }
-
- const { waitUntilExit } = render(
- process.exit(0)} />
- )
- await waitUntilExit()
-}
-```
-
-- [ ] **Step 2: Verify it compiles**
-
-Run: `npx tsx --eval "import './src/compare.js'" 2>&1 | head -5`
-Expected: No import errors
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/compare.tsx
-git commit --author="iamtoruk " -m "feat(compare): add CompareView orchestrator and renderCompare"
-```
-
----
-
-### Task 7: CLI compare command
-
-**Files:**
-- Modify: `src/cli.ts` (add command at ~line 650, before `program.parse()`)
-
-- [ ] **Step 1: Add the compare command**
-
-Add before the `program.parse()` line in `src/cli.ts`:
-
-```ts
-import { renderCompare } from './compare.js'
-```
-
-Add at the top with other imports. Then add the command before `program.parse()`:
-
-```ts
-program
- .command('compare')
- .description('Compare two AI models side-by-side')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', 'all')
- .option('--provider ', 'Filter by provider: all, claude, codex, cursor', 'all')
- .action(async (opts) => {
- await loadPricing()
- const { range } = getDateRange(opts.period)
- await renderCompare(range, opts.provider)
- })
-```
-
-- [ ] **Step 2: Test the standalone command**
-
-Run: `npx tsx src/cli.ts compare`
-Expected: Model selection screen appears with arrow-key navigation. Press `q` to quit.
-
-- [ ] **Step 3: Commit**
-
-```bash
-git add src/cli.ts
-git commit --author="iamtoruk " -m "feat(compare): add codeburn compare command"
-```
-
----
-
-### Task 8: Dashboard integration
-
-**Files:**
-- Modify: `src/dashboard.tsx` (~5 changes)
-
-- [ ] **Step 1: Add 'compare' to View type**
-
-Change line 16 in `src/dashboard.tsx`:
-
-```ts
-// Before:
-type View = 'dashboard' | 'optimize'
-
-// After:
-type View = 'dashboard' | 'optimize' | 'compare'
-```
-
-- [ ] **Step 2: Add import**
-
-Add to imports at the top of `src/dashboard.tsx`:
-
-```ts
-import { CompareView } from './compare.js'
-```
-
-- [ ] **Step 3: Add modelCount state and 'c' keybinding**
-
-In the `InteractiveDashboard` component, add state tracking after `optimizeAvailable`:
-
-```ts
-const modelCount = new Set(
- projects.flatMap(p => p.sessions.flatMap(s => Object.keys(s.modelBreakdown)))
-).size
-const compareAvailable = modelCount >= 2
-```
-
-In the `useInput` handler, add after the optimize toggle:
-
-```ts
-if (input === 'c' && compareAvailable && view === 'dashboard') { setView('compare'); return }
-if (key.escape && view === 'compare') { setView('dashboard'); return }
-```
-
-Update the existing escape handler for optimize to also check compare:
-
-```ts
-// Before:
-if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); return }
-
-// After:
-if ((input === 'b' || key.escape) && (view === 'optimize' || view === 'compare')) { setView('dashboard'); return }
-```
-
-- [ ] **Step 4: Add CompareView to render**
-
-In the return JSX, extend the conditional render (around line 704):
-
-```tsx
-// Before:
-{view === 'optimize' && optimizeResult
- ?
- : }
-
-// After:
-{view === 'compare'
- ? setView('dashboard')} />
- : view === 'optimize' && optimizeResult
- ?
- : }
-```
-
-- [ ] **Step 5: Update StatusBar**
-
-Add `compareAvailable` prop to StatusBar and render the hint. In the StatusBar component, add after the optimize hint:
-
-```tsx
-{!isOptimize && view !== 'compare' && compareAvailable && (
- <> c compare >
-)}
-```
-
-Update StatusBar props:
-
-```ts
-function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable, compareAvailable }: {
- width: number; showProvider?: boolean; view?: View; findingCount?: number; optimizeAvailable?: boolean; compareAvailable?: boolean
-})
-```
-
-Pass `compareAvailable` at both StatusBar call sites.
-
-- [ ] **Step 6: Test the dashboard integration**
-
-Run: `npx tsx src/cli.ts report`
-Expected: Status bar shows `c compare`. Press `c` to open model selection. Press `Esc` to go back. Press `q` to quit.
-
-- [ ] **Step 7: Commit**
-
-```bash
-git add src/dashboard.tsx
-git commit --author="iamtoruk " -m "feat(compare): integrate into dashboard with c shortcut"
-```
-
----
-
-### Task 9: End-to-end verification
-
-**Files:** None (testing only)
-
-- [ ] **Step 1: Run the full test suite**
-
-Run: `npx vitest run`
-Expected: All tests pass (including new compare-stats tests)
-
-- [ ] **Step 2: Test standalone compare**
-
-Run: `npx tsx src/cli.ts compare`
-Expected: Model selection screen. Select two models with spacebar. Press Enter. See comparison table with color-coded winners. Press Esc to go back. Press q to quit.
-
-- [ ] **Step 3: Test dashboard integration**
-
-Run: `npx tsx src/cli.ts report`
-Expected: Press `c` to open compare. Select models. See results. Press Esc twice to return to dashboard. Verify `o` for optimize still works.
-
-- [ ] **Step 4: Verify edge cases**
-
-Run: `npx tsx src/cli.ts compare --provider codex`
-Expected: If Codex has < 2 models, shows "Need at least 2 models" message.
-
-- [ ] **Step 5: Final commit on branch**
-
-```bash
-git add -A
-git status # verify no unrelated files
-# Only if there are unstaged fixes:
-git commit --author="iamtoruk " -m "fix(compare): polish from end-to-end testing"
-```
diff --git a/docs/superpowers/specs/2026-04-19-model-comparison-design.md b/docs/superpowers/specs/2026-04-19-model-comparison-design.md
deleted file mode 100644
index 1dc5cc2..0000000
--- a/docs/superpowers/specs/2026-04-19-model-comparison-design.md
+++ /dev/null
@@ -1,266 +0,0 @@
-# Model Comparison Design
-
-Compare two AI models side-by-side using normalized metrics derived from real
-usage data. Answers "is Opus 4.7 actually better than 4.6 for my workflow?"
-with hard numbers instead of vibes.
-
-## Goals
-
-1. Let users pick any two models and see a fair, normalized comparison
-2. Surface efficiency metrics that raw cost/token dashboards don't show
- (one-shot rate, retry rate, self-correction rate)
-3. Accessible from both the dashboard (press `c`) and standalone (`codeburn compare`)
-4. Screenshot-friendly terminal output
-
-## Non-Goals
-
-- Multi-model comparison (3+) -- v2
-- Time-frame filtering (`--period`) -- v2
-- Charts/graphs in the comparison view -- v2
-- Exporting comparison results to JSON/CSV -- v2
-- Statistical significance testing (show sample sizes, let the user judge)
-
----
-
-## 1. Entry Points
-
-### Standalone command
-
-```
-codeburn compare [--provider ] [--period ]
-```
-
-Period defaults to `all` (6 months). Provider defaults to `all`. Both flags
-are accepted but optional. Launches the full-screen Ink TUI directly into the
-model selection screen.
-
-### Dashboard shortcut
-
-Press `c` in the dashboard to switch to the compare view. Same component, same
-flow. `Escape` returns to the dashboard (mirrors how `o` toggles optimize).
-
-The status bar gains a `[c]ompare` hint next to the existing `[o]ptimize`.
-
----
-
-## 2. Data Pipeline
-
-### Aggregation
-
-Reuse `parseAllSessions` to get `ProjectSummary[]` for the selected
-period/provider. Then build per-model stats by iterating turns and calls:
-
-```ts
-type ModelStats = {
- model: string
- calls: number
- cost: number
- outputTokens: number
- inputTokens: number
- cacheReadTokens: number
- cacheWriteTokens: number
- totalTurns: number
- editTurns: number
- oneShotTurns: number // edit turns with 0 retries
- retries: number // total retry count
- selfCorrections: number // turns matching apology/mistake patterns
- firstSeen: string // earliest timestamp (ISO)
- lastSeen: string // latest timestamp (ISO)
-}
-```
-
-Turn-level metrics are attributed to the primary model (first call in the
-turn). This matches how the dashboard already attributes turns.
-
-### Self-correction detection
-
-Scan assistant message text for patterns that indicate the model acknowledged
-an error. These patterns were validated against real session data:
-
-```ts
-const SELF_CORRECTION_PATTERNS = [
- /\bI('m| am) sorry\b/i,
- /\bmy mistake\b/i,
- /\bmy apolog/i,
- /\bI made (a |an )?(error|mistake)\b/i,
- /\bI was wrong\b/i,
- /\bmy bad\b/i,
- /\bI apologize\b/i,
- /\bsorry about that\b/i,
- /\bsorry for (the|that|this)\b/i,
- /\bI should have\b/i,
- /\bI shouldn't have\b/i,
- /\bI incorrectly\b/i,
- /\bI mistakenly\b/i,
-]
-```
-
-This requires reading assistant message content from session JSONL files,
-which the current parser does not expose. The aggregation function will need
-to read raw JSONL entries for sessions that contain the selected models.
-
-### Normalization
-
-All comparison metrics are rates or per-call averages. Raw totals (cost, calls,
-days) are shown as context, never as comparison metrics.
-
----
-
-## 3. Comparison Metrics
-
-| Metric | Formula | Better |
-|---|---|---|
-| Cost / call | `cost / calls` | Lower |
-| Output tokens / call | `outputTokens / calls` | Lower |
-| Cache hit rate | `cacheRead / (input + cacheRead + cacheWrite) * 100` | Higher |
-| One-shot rate | `oneShotTurns / editTurns * 100` | Higher |
-| Retry rate | `retries / editTurns` | Lower |
-| Self-correction rate | `selfCorrections / totalTurns * 100` | Lower |
-
-### Context row (not compared)
-
-Displayed below the table to give sample-size context:
-
-- Total calls
-- Total cost
-- Days of data (lastSeen - firstSeen)
-- Edit turns (denominator for one-shot/retry metrics)
-
----
-
-## 4. UI Screens
-
-### Model Selection Screen
-
-```
- Model Comparison
-
- Select two models to compare:
-
- claude-opus-4-6 56,031 calls $5,272
- > claude-opus-4-7 3,592 calls $664 [selected]
- claude-sonnet-4-6 1,142 calls $25
- claude-haiku-4-5 323 calls $4
- gpt-5 113 calls $3 low data
-
- [space] select [enter] compare [esc] back [q] quit
-```
-
-- Arrow keys navigate, spacebar toggles selection (max 2)
-- Models sorted by cost descending (most-used first)
-- Models with < 20 calls show "low data" dim label
-- Enter is disabled until exactly 2 models are selected
-- Filter out `` model entries
-
-### Loading Screen
-
-```
- Comparing claude-opus-4-6 vs claude-opus-4-7...
-```
-
-Simple spinner while aggregation runs. Should be fast (< 2 seconds) since
-session data is already parsed.
-
-### Comparison Results Screen
-
-```
- claude-opus-4-6 vs claude-opus-4-7
-
- 4.6 4.7
- Cost / call $0.094 $0.185 4.6 wins
- Output tok / call 227 800 4.6 wins
- Cache hit rate 98.4% 98.8% 4.7 wins
- One-shot rate 88.8% 74.5% 4.6 wins
- Retry rate 0.18 0.46 4.6 wins
- Self-correction 0.18% 0.25% 4.6 wins
-
- ── Context ──────────────────────────────────
- Calls 56,031 3,592
- Cost $5,272.13 $664.32
- Days of data 60 3
- Edit turns 1,577 102
-
- [esc] back [q] quit
-```
-
-- Winner column uses green text for the better model on each metric
-- Model names in the header are shortened for display (drop `claude-` prefix)
-- Context section is dimmed to visually separate it from the comparison
-- If a metric can't be computed (e.g., 0 edit turns), show `-` instead
-
----
-
-## 5. File Structure
-
-```
-src/compare.tsx -- Ink components: ModelSelector, ComparisonResults,
- CompareView (top-level), loading state
-src/compare-stats.ts -- aggregateModelStats(), computeComparison(),
- self-correction pattern matching, ModelStats type
-src/cli.ts -- new `compare` command registration
-src/dashboard.tsx -- add 'c' keybinding, CompareView integration
-```
-
-### compare-stats.ts
-
-Pure data module, no UI. Exports:
-
-```ts
-function aggregateModelStats(projects: ProjectSummary[]): ModelStats[]
-function computeComparison(a: ModelStats, b: ModelStats): ComparisonRow[]
-```
-
-The self-correction scanner needs raw assistant message text from JSONL files.
-Two options: (a) extend the parser to expose message text on turns during
-initial parse, or (b) have `compare-stats.ts` re-read JSONL files via provider
-session discovery (same mechanism the parser uses). Option (a) is cleaner but
-increases memory for all commands; option (b) is isolated to compare. The
-implementation plan should decide.
-
-### compare.tsx
-
-Three Ink components:
-
-- `ModelSelector` -- arrow navigation, spacebar toggle, enter to confirm
-- `ComparisonResults` -- the formatted table with color-coded winners
-- `CompareView` -- orchestrates the flow (selection -> loading -> results)
-
-Exported `renderCompare()` function for the standalone command, and
-`CompareView` component for embedding in the dashboard.
-
----
-
-## 6. Dashboard Integration
-
-### Status bar
-
-Add `[c]ompare` to the status bar, after `[o]ptimize`:
-
-```
- 1-5 period arrows switch p provider o optimize c compare q quit
-```
-
-### View state
-
-Extend the existing `View` type:
-
-```ts
-type View = 'dashboard' | 'optimize' | 'compare'
-```
-
-Press `c` sets view to `'compare'`. Escape from compare returns to
-`'dashboard'`. The compare view receives the already-parsed `projects`
-from the dashboard state -- no re-parsing needed.
-
----
-
-## 7. Edge Cases
-
-- **Only one model in data**: Show message "Need at least 2 models to compare.
- Only found: claude-opus-4-6"
-- **Model with 0 edit turns**: Show `-` for one-shot rate and retry rate
-- **Model with < 20 calls**: Show "low data" warning on selection screen;
- allow selection but display a note on the results screen
-- **Self-correction scanner fails to read JSONL**: Gracefully degrade --
- show `-` for self-correction rate, don't block the rest of the comparison
-- **Both models have identical metrics**: Show "tie" instead of a winner
diff --git a/gnome/README.md b/gnome/README.md
new file mode 100644
index 0000000..3b76632
--- /dev/null
+++ b/gnome/README.md
@@ -0,0 +1,70 @@
+# CodeBurn GNOME Extension
+
+Monitor AI coding assistant token usage and costs from your GNOME desktop panel.
+
+## Requirements
+
+- GNOME Shell 45 or later
+- CodeBurn CLI installed (`npm i -g codeburn`)
+- `glib-compile-schemas` (usually part of `glib2-devel` or `libglib2.0-dev`)
+
+## Install
+
+```bash
+cd gnome
+chmod +x install.sh
+./install.sh
+```
+
+Then restart GNOME Shell:
+- **Wayland:** Log out and back in
+- **X11:** Press `Alt+F2`, type `r`, press Enter
+
+Enable the extension:
+
+```bash
+gnome-extensions enable codeburn@codeburn.dev
+```
+
+## Configure
+
+Open preferences:
+
+```bash
+gnome-extensions prefs codeburn@codeburn.dev
+```
+
+Or use the GNOME Extensions app.
+
+### Settings
+
+| Setting | Default | Description |
+|---------|---------|-------------|
+| Refresh Interval | 30s | How often to poll CodeBurn CLI |
+| Default Period | Today | Period shown on open |
+| Compact Mode | Off | Hide cost label, show icon only |
+| Budget Threshold | $0 | Daily budget alert (0 = disabled) |
+| Budget Alerts | Off | Show warning when budget exceeded |
+| CLI Path | (auto) | Custom path to `codeburn` binary |
+
+## Uninstall
+
+```bash
+gnome-extensions disable codeburn@codeburn.dev
+rm -r ~/.local/share/gnome-shell/extensions/codeburn@codeburn.dev
+```
+
+## Development
+
+Test changes without installing:
+
+```bash
+# Compile schemas locally
+glib-compile-schemas schemas/
+
+# Symlink for development
+ln -sf "$(pwd)" ~/.local/share/gnome-shell/extensions/codeburn@codeburn.dev
+
+# Watch logs
+journalctl -f -o cat /usr/bin/gnome-shell
+```
diff --git a/gnome/dataClient.js b/gnome/dataClient.js
new file mode 100644
index 0000000..4d0056b
--- /dev/null
+++ b/gnome/dataClient.js
@@ -0,0 +1,161 @@
+import GLib from 'gi://GLib';
+import Gio from 'gi://Gio';
+
+const TIMEOUT_SECONDS = 15;
+const SAFE_ARG_RE = /^[A-Za-z0-9 ._/\-]+$/;
+
+function buildAdditionalPaths() {
+ const home = GLib.get_home_dir();
+ return [
+ '/usr/local/bin',
+ `${home}/.local/bin`,
+ `${home}/.npm-global/bin`,
+ `${home}/.volta/bin`,
+ `${home}/.bun/bin`,
+ `${home}/.cargo/bin`,
+ `${home}/.asdf/shims`,
+ `${home}/.local/share/fnm/aliases/default/bin`,
+ `${home}/.local/share/pnpm`,
+ ];
+}
+
+export class DataClient {
+ _cache = new Map();
+ _inFlight = null;
+ _codeburnPath;
+ _augmentedPath;
+
+ constructor(codeburnPath) {
+ this._codeburnPath = codeburnPath || '';
+ this._augmentedPath = this._buildAugmentedPath();
+ }
+
+ setCodeburnPath(path) {
+ this._codeburnPath = path || '';
+ }
+
+ cancelInFlight() {
+ if (this._inFlight) {
+ this._inFlight.cancellable.cancel();
+ this._inFlight = null;
+ }
+ }
+
+ getCached(period, provider) {
+ const key = `${period}:${provider}`;
+ return this._cache.get(key) ?? null;
+ }
+
+ async fetch(period, provider) {
+ this.cancelInFlight();
+
+ const cancellable = new Gio.Cancellable();
+ this._inFlight = { cancellable };
+
+ try {
+ const payload = await this._spawn(period, provider, cancellable);
+ const key = `${period}:${provider}`;
+ this._cache.set(key, payload);
+ return payload;
+ } finally {
+ if (this._inFlight?.cancellable === cancellable)
+ this._inFlight = null;
+ }
+ }
+
+ _buildArgv(period, provider) {
+ let base;
+ if (this._codeburnPath && SAFE_ARG_RE.test(this._codeburnPath)) {
+ base = this._codeburnPath.split(' ').filter(s => s.length > 0);
+ } else {
+ base = ['codeburn'];
+ }
+
+ const args = [
+ ...base,
+ 'status',
+ '--format', 'menubar-json',
+ '--period', period,
+ '--no-optimize',
+ ];
+
+ if (provider && provider !== 'all')
+ args.push('--provider', provider);
+
+ return args;
+ }
+
+ _buildAugmentedPath() {
+ const currentPath = GLib.getenv('PATH') || '/usr/bin:/bin';
+ const parts = currentPath.split(':');
+ for (const extra of buildAdditionalPaths()) {
+ if (!parts.includes(extra))
+ parts.push(extra);
+ }
+ return parts.join(':');
+ }
+
+ _spawn(period, provider, cancellable) {
+ return new Promise((resolve, reject) => {
+ const argv = this._buildArgv(period, provider);
+ let settled = false;
+
+ const settle = (fn, value) => {
+ if (settled) return;
+ settled = true;
+ fn(value);
+ };
+
+ let proc;
+ try {
+ const launcher = Gio.SubprocessLauncher.new(
+ Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
+ );
+ launcher.setenv('PATH', this._augmentedPath, true);
+ proc = launcher.spawnv(argv);
+ } catch (e) {
+ settle(reject, new Error(`CLI not found: ${e.message}`));
+ return;
+ }
+
+ let timeoutId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, TIMEOUT_SECONDS, () => {
+ timeoutId = 0;
+ proc.force_exit();
+ settle(reject, new Error('CLI timeout'));
+ return GLib.SOURCE_REMOVE;
+ });
+
+ proc.communicate_utf8_async(null, cancellable, (_proc, res) => {
+ if (timeoutId) {
+ GLib.Source.remove(timeoutId);
+ timeoutId = 0;
+ }
+
+ try {
+ const [, stdout, stderr] = _proc.communicate_utf8_finish(res);
+
+ if (!_proc.get_successful()) {
+ const msg = stderr?.trim() || 'CLI exited with error';
+ settle(reject, new Error(msg));
+ return;
+ }
+
+ if (!stdout || stdout.trim().length === 0) {
+ settle(reject, new Error('CLI returned empty output'));
+ return;
+ }
+
+ const payload = JSON.parse(stdout);
+ settle(resolve, payload);
+ } catch (e) {
+ settle(reject, e);
+ }
+ });
+ });
+ }
+
+ destroy() {
+ this.cancelInFlight();
+ this._cache.clear();
+ }
+}
diff --git a/gnome/extension.js b/gnome/extension.js
new file mode 100644
index 0000000..fba94fd
--- /dev/null
+++ b/gnome/extension.js
@@ -0,0 +1,17 @@
+import { Extension } from 'resource:///org/gnome/shell/extensions/extension.js';
+import * as Main from 'resource:///org/gnome/shell/ui/main.js';
+import { CodeBurnIndicator } from './indicator.js';
+
+export default class CodeBurnExtension extends Extension {
+ _indicator = null;
+
+ enable() {
+ this._indicator = new CodeBurnIndicator(this);
+ Main.panel.addToStatusArea('codeburn-indicator', this._indicator);
+ }
+
+ disable() {
+ this._indicator?.destroy();
+ this._indicator = null;
+ }
+}
diff --git a/gnome/icons/codeburn-symbolic.svg b/gnome/icons/codeburn-symbolic.svg
new file mode 100644
index 0000000..3a4ee85
--- /dev/null
+++ b/gnome/icons/codeburn-symbolic.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/gnome/indicator.js b/gnome/indicator.js
new file mode 100644
index 0000000..533f644
--- /dev/null
+++ b/gnome/indicator.js
@@ -0,0 +1,1004 @@
+import GObject from 'gi://GObject';
+import St from 'gi://St';
+import Gio from 'gi://Gio';
+import GLib from 'gi://GLib';
+import Clutter from 'gi://Clutter';
+import Soup from 'gi://Soup?version=3.0';
+import * as PanelMenu from 'resource:///org/gnome/shell/ui/panelMenu.js';
+import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
+import { DataClient } from './dataClient.js';
+
+const CACHE_TTL_MS = 300_000;
+const TOP_ACTIVITIES = 10;
+const CHART_HEIGHT = 52;
+const BAR_TRACK_WIDTH = 240;
+
+const PERIODS = [
+ { id: 'today', label: 'Today' },
+ { id: 'week', label: '7 Days' },
+ { id: '30days', label: '30 Days' },
+ { id: 'month', label: 'Month' },
+ { id: 'all', label: '6 Months' },
+];
+
+const INSIGHTS = [
+ { id: 'activity', label: 'Activity' },
+ { id: 'trend', label: 'Trend' },
+ { id: 'forecast', label: 'Forecast' },
+ { id: 'pulse', label: 'Pulse' },
+ { id: 'stats', label: 'Stats' },
+];
+
+const PROVIDERS = [
+ { id: 'all', label: 'All' },
+ { id: 'claude', label: 'Claude' },
+ { id: 'codex', label: 'Codex' },
+ { id: 'cursor', label: 'Cursor' },
+ { id: 'copilot', label: 'Copilot' },
+ { id: 'opencode', label: 'OpenCode' },
+ { id: 'pi', label: 'Pi' },
+ { id: 'droid', label: 'Droid' },
+ { id: 'gemini', label: 'Gemini' },
+ { id: 'kilo-code', label: 'Kilo Code' },
+ { id: 'kiro', label: 'Kiro' },
+ { id: 'kimi', label: 'Kimi' },
+ { id: 'roo-code', label: 'Roo Code' },
+];
+
+const CURRENCIES = [
+ { code: 'USD', symbol: '$' },
+ { code: 'EUR', symbol: '€' },
+ { code: 'GBP', symbol: '£' },
+ { code: 'CAD', symbol: 'C$' },
+ { code: 'AUD', symbol: 'A$' },
+ { code: 'JPY', symbol: '¥' },
+ { code: 'INR', symbol: '₹' },
+ { code: 'BRL', symbol: 'R$' },
+ { code: 'CHF', symbol: 'CHF ' },
+ { code: 'SEK', symbol: 'kr ' },
+ { code: 'SGD', symbol: 'S$' },
+ { code: 'HKD', symbol: 'HK$' },
+ { code: 'KRW', symbol: '₩' },
+ { code: 'MXN', symbol: 'MX$' },
+ { code: 'ZAR', symbol: 'R ' },
+ { code: 'DKK', symbol: 'kr ' },
+ { code: 'CNY', symbol: '¥' },
+];
+
+const PROVIDER_PATHS = {
+ claude: '.claude/projects',
+ codex: '.codex/sessions',
+ cursor: '.config/Cursor/User/globalStorage/state.vscdb',
+ copilot: '.copilot/session-state',
+ kimi: '.kimi/sessions',
+ pi: '.pi/agent/sessions',
+};
+
+function formatCost(value, currency, rate = 1, exact = false) {
+ const n = (Number(value) || 0) * (Number(rate) || 1);
+ const abs = Math.abs(n);
+ const symbol = currency?.symbol || '$';
+ if (!exact && abs >= 1000) return `${symbol}${(n / 1000).toFixed(abs >= 10000 ? 0 : 1)}k`;
+ const parts = n.toFixed(2).split('.');
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+ return `${symbol}${parts.join('.')}`;
+}
+
+function formatTokensCompact(n) {
+ const v = Number(n) || 0;
+ if (v >= 1_000_000_000) return `${(v / 1_000_000_000).toFixed(1)}B`;
+ if (v >= 1_000_000) return `${(v / 1_000_000).toFixed(1)}M`;
+ if (v >= 1000) return `${(v / 1000).toFixed(1)}k`;
+ return String(v);
+}
+
+function formatTime(date) {
+ if (!date || Number.isNaN(date.getTime())) return '';
+ const now = new Date();
+ const diffSec = Math.floor((now.getTime() - date.getTime()) / 1000);
+ if (diffSec < 60) return 'just now';
+ if (diffSec < 3600) return `${Math.floor(diffSec / 60)}m ago`;
+ if (diffSec < 86400) return `${Math.floor(diffSec / 3600)}h ago`;
+ return date.toLocaleDateString();
+}
+
+export const CodeBurnIndicator = GObject.registerClass(
+class CodeBurnIndicator extends PanelMenu.Button {
+ _init(extension) {
+ super._init(0.0, 'CodeBurn');
+
+ this._extension = extension;
+ this._settings = extension.getSettings();
+ this._dataClient = new DataClient(this._settings.get_string('codeburn-path'));
+ this._settingsChangedIds = [];
+
+ this._period = this._settings.get_string('default-period') || 'today';
+ this._insight = 'activity';
+ this._availableProviders = this._detectProviders();
+ this._provider = this._availableProviders.length === 1 ? this._availableProviders[0] : 'all';
+
+ this._currency = this._loadCurrency();
+ this._exactCosts = this._settings.get_boolean('show-exact-costs');
+ this._fxRate = 1;
+ this._fxCache = { USD: 1 };
+ this._soupSession = new Soup.Session();
+ this._payload = null;
+ this._payloadCache = new Map();
+ this._inFlightKeys = new Set();
+ this._refreshGen = 0;
+ this._refreshSourceId = 0;
+ this._chartSummaryText = '';
+ this._destroyed = false;
+
+ this._themeSettings = new Gio.Settings({ schema_id: 'org.gnome.desktop.interface' });
+ this._themeSignal = this._themeSettings.connect('changed::color-scheme', () => this._applyThemeClass());
+ this._applyThemeClass();
+ this._updateFxRate();
+
+ this._buildPanelButton();
+ this._buildPopup();
+ this._connectSettings();
+ this._startRefreshLoop();
+ this._refresh();
+ }
+
+ // -- Panel button --
+
+ _buildPanelButton() {
+ const box = new St.BoxLayout({ style_class: 'panel-status-menu-box codeburn-panel' });
+ this._panelIcon = new St.Label({
+ text: '🔥',
+ y_align: Clutter.ActorAlign.CENTER,
+ style_class: 'codeburn-flame',
+ });
+ this._panelLabel = new St.Label({
+ text: '...',
+ y_align: Clutter.ActorAlign.CENTER,
+ style_class: 'codeburn-label',
+ });
+ box.add_child(this._panelIcon);
+ box.add_child(this._panelLabel);
+ this._panelLabel.visible = !this._settings.get_boolean('compact-mode');
+ this.add_child(box);
+ }
+
+ // -- Popup --
+
+ _buildPopup() {
+ try {
+ this.menu.box.add_style_class_name('codeburn-menu');
+ this._popupHost = new PopupMenu.PopupBaseMenuItem({ reactive: false, can_focus: false });
+ this._popupHost.add_style_class_name('codeburn-host');
+ this.menu.addMenuItem(this._popupHost);
+
+ this._root = new St.BoxLayout({ vertical: true, style_class: 'codeburn-root', x_expand: true });
+ this._popupHost.add_child(this._root);
+
+ this._buildBrandHeader();
+
+ this._scrollView = new St.ScrollView({
+ style_class: 'codeburn-scroll',
+ hscrollbar_policy: St.PolicyType.NEVER,
+ vscrollbar_policy: St.PolicyType.AUTOMATIC,
+ y_expand: true,
+ });
+ this._scrollContent = new St.BoxLayout({ vertical: true, x_expand: true });
+ this._scrollView.set_child(this._scrollContent);
+ this._root.add_child(this._scrollView);
+
+ this._buildAgentTabs();
+ this._buildHero();
+ this._buildPeriodTabs();
+ this._buildInsightPills();
+ this._buildTokenChart();
+ this._buildLoadingIndicator();
+ this._buildContentArea();
+ this._buildBudgetAlert();
+ this._buildFindingsSection();
+ this._buildFooter();
+ } catch (e) {
+ log(`CodeBurn: popup build error: ${e.message}\n${e.stack}`);
+ }
+ }
+
+ _buildBrandHeader() {
+ const header = new St.BoxLayout({ vertical: true, style_class: 'codeburn-brand-header' });
+ const title = new St.BoxLayout({ style_class: 'codeburn-brand-row' });
+ title.add_child(new St.Label({ text: 'Code', style_class: 'codeburn-brand-primary' }));
+ title.add_child(new St.Label({ text: 'Burn', style_class: 'codeburn-brand-accent' }));
+ header.add_child(title);
+ header.add_child(new St.Label({ text: 'AI Coding Cost Tracker', style_class: 'codeburn-brand-subhead' }));
+ this._root.add_child(header);
+ }
+
+ _buildAgentTabs() {
+ const detected = this._availableProviders;
+ this._agentTabs = new Map();
+ this._agentTabRow = null;
+ if (detected.length === 0) return;
+
+ const disabled = this._getDisabledProviders();
+ const tabs = detected.length === 1
+ ? PROVIDERS.filter(p => p.id === detected[0])
+ : [PROVIDERS[0], ...PROVIDERS.slice(1).filter(p => detected.includes(p.id) && !disabled.has(p.id))];
+
+ if (tabs.length === 1) {
+ const badge = new St.Label({ text: tabs[0].label, style_class: 'codeburn-agent-badge' });
+ const row = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
+ row.add_child(badge);
+ this._scrollContent.add_child(row);
+ return;
+ }
+
+ const useScroll = tabs.length > 5;
+ this._agentTabRow = new St.BoxLayout({ style_class: 'codeburn-tab-row' });
+ for (const p of tabs) {
+ const btn = new St.Button({ label: p.label, style_class: 'codeburn-tab', can_focus: true, x_expand: !useScroll });
+ btn.connect('clicked', () => {
+ this._provider = p.id;
+ this._updateAgentTabStyle();
+ this._refresh();
+ });
+ this._agentTabRow.add_child(btn);
+ this._agentTabs.set(p.id, btn);
+ }
+ if (useScroll) {
+ const agentScroll = new St.ScrollView({
+ style_class: 'codeburn-agent-scroll',
+ hscrollbar_policy: St.PolicyType.AUTOMATIC,
+ vscrollbar_policy: St.PolicyType.NEVER,
+ });
+ agentScroll.set_child(this._agentTabRow);
+ this._scrollContent.add_child(agentScroll);
+ } else {
+ this._scrollContent.add_child(this._agentTabRow);
+ }
+ this._updateAgentTabStyle();
+ }
+
+ _updateAgentTabStyle() {
+ for (const [id, btn] of this._agentTabs) {
+ if (id === this._provider) btn.add_style_class_name('codeburn-tab-active');
+ else btn.remove_style_class_name('codeburn-tab-active');
+ }
+ }
+
+ _buildHero() {
+ const hero = new St.BoxLayout({ vertical: true, style_class: 'codeburn-hero' });
+ const topLine = new St.BoxLayout({ style_class: 'codeburn-hero-top' });
+ this._heroDot = new St.Widget({ style_class: 'codeburn-hero-dot' });
+ this._heroLabel = new St.Label({ text: 'Loading...', style_class: 'codeburn-hero-label' });
+ topLine.add_child(this._heroDot);
+ topLine.add_child(this._heroLabel);
+ this._heroAmount = new St.Label({ text: '$0.00', style_class: 'codeburn-hero-amount' });
+ this._heroMeta = new St.Label({ text: '', style_class: 'codeburn-hero-meta' });
+ hero.add_child(topLine);
+ hero.add_child(this._heroAmount);
+ hero.add_child(this._heroMeta);
+ this._scrollContent.add_child(hero);
+ }
+
+ _buildPeriodTabs() {
+ const row = new St.BoxLayout({ style_class: 'codeburn-tab-row codeburn-period-row' });
+ this._periodTabs = new Map();
+ for (const p of PERIODS) {
+ const btn = new St.Button({ label: p.label, style_class: 'codeburn-period', can_focus: true, x_expand: true });
+ btn.connect('clicked', () => {
+ this._period = p.id;
+ this._updatePeriodTabStyle();
+ this._refresh();
+ });
+ row.add_child(btn);
+ this._periodTabs.set(p.id, btn);
+ }
+ this._scrollContent.add_child(row);
+ this._updatePeriodTabStyle();
+ }
+
+ _updatePeriodTabStyle() {
+ for (const [id, btn] of this._periodTabs) {
+ if (id === this._period) btn.add_style_class_name('codeburn-period-active');
+ else btn.remove_style_class_name('codeburn-period-active');
+ }
+ }
+
+ _buildInsightPills() {
+ const row = new St.BoxLayout({ style_class: 'codeburn-insight-row' });
+ this._insightPills = new Map();
+ for (const i of INSIGHTS) {
+ const btn = new St.Button({ label: i.label, style_class: 'codeburn-insight-pill', can_focus: true, x_expand: true });
+ btn.connect('clicked', () => {
+ this._insight = i.id;
+ this._updateInsightPillStyle();
+ this._renderContent();
+ });
+ row.add_child(btn);
+ this._insightPills.set(i.id, btn);
+ }
+ this._scrollContent.add_child(row);
+ this._updateInsightPillStyle();
+ }
+
+ _updateInsightPillStyle() {
+ for (const [id, btn] of this._insightPills) {
+ if (id === this._insight) btn.add_style_class_name('codeburn-insight-pill-active');
+ else btn.remove_style_class_name('codeburn-insight-pill-active');
+ }
+ }
+
+ _buildTokenChart() {
+ this._chartContainer = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart' });
+ const header = new St.BoxLayout({ style_class: 'codeburn-chart-header' });
+ this._chartLabel = new St.Label({ text: 'Tokens', style_class: 'codeburn-chart-label', x_expand: true });
+ this._chartTotal = new St.Label({ text: '', style_class: 'codeburn-chart-total' });
+ header.add_child(this._chartLabel);
+ header.add_child(this._chartTotal);
+ this._chartContainer.add_child(header);
+ this._chartBars = new St.BoxLayout({ style_class: 'codeburn-chart-bars' });
+ this._chartContainer.add_child(this._chartBars);
+ this._scrollContent.add_child(this._chartContainer);
+ }
+
+ _buildContentArea() {
+ this._scrollContent.add_child(new St.Widget({ style_class: 'codeburn-divider' }));
+ this._contentArea = new St.BoxLayout({ vertical: true, style_class: 'codeburn-content' });
+ this._scrollContent.add_child(this._contentArea);
+ }
+
+ _buildBudgetAlert() {
+ this._budgetLabel = new St.Label({ text: '', style_class: 'codeburn-budget-warning', visible: false });
+ this._scrollContent.add_child(this._budgetLabel);
+ }
+
+ _buildFindingsSection() {
+ this._findingsBtn = new St.Button({ style_class: 'codeburn-findings', visible: false });
+ const box = new St.BoxLayout({ style_class: 'codeburn-findings-inner' });
+ this._findingsCount = new St.Label({ text: '', style_class: 'codeburn-findings-count' });
+ this._findingsSavings = new St.Label({ text: '', style_class: 'codeburn-findings-savings' });
+ box.add_child(this._findingsCount);
+ box.add_child(this._findingsSavings);
+ this._findingsBtn.set_child(box);
+ this._findingsBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'optimize']));
+ this._scrollContent.add_child(this._findingsBtn);
+ }
+
+ _buildLoadingIndicator() {
+ this._loadingBox = new St.BoxLayout({ vertical: true, style_class: 'codeburn-loading', visible: false, x_expand: true });
+ const widths = [0.85, 0.6, 0.92, 0.5, 0.75, 0.45];
+ for (const w of widths) {
+ const bar = new St.Widget({ style_class: 'codeburn-skeleton-bar', x_expand: false });
+ bar.set_width(Math.round(308 * w));
+ bar.set_height(10);
+ this._loadingBox.add_child(bar);
+ }
+ this._scrollContent.add_child(this._loadingBox);
+ }
+
+ _showLoading() {
+ if (!this._loadingBox) return;
+ this._loadingBox.visible = true;
+ this._loadingBox.get_children().forEach((bar, i) => {
+ bar.opacity = 255;
+ bar.ease({
+ opacity: 60,
+ duration: 900,
+ delay: i * 120,
+ mode: Clutter.AnimationMode.EASE_IN_OUT_SINE,
+ repeatCount: -1,
+ autoReverse: true,
+ });
+ });
+ }
+
+ _hideLoading() {
+ if (!this._loadingBox) return;
+ this._loadingBox.visible = false;
+ this._loadingBox.get_children().forEach(bar => {
+ bar.remove_all_transitions();
+ bar.opacity = 255;
+ });
+ }
+
+ _buildFooter() {
+ this._currencyPicker = new St.ScrollView({
+ style_class: 'codeburn-currency-picker',
+ visible: false,
+ hscrollbar_policy: St.PolicyType.NEVER,
+ vscrollbar_policy: St.PolicyType.AUTOMATIC,
+ });
+ const pickerList = new St.BoxLayout({ vertical: true, style_class: 'codeburn-currency-list' });
+ for (const c of CURRENCIES) {
+ const item = new St.Button({ label: `${c.symbol} ${c.code}`, style_class: 'codeburn-currency-item', can_focus: true });
+ if (c.code === this._currency.code) item.add_style_class_name('codeburn-currency-item-active');
+ item.connect('clicked', () => {
+ this._setCurrency(c.code);
+ this._currencyPicker.hide();
+ pickerList.get_children().forEach(ch => ch.remove_style_class_name('codeburn-currency-item-active'));
+ item.add_style_class_name('codeburn-currency-item-active');
+ });
+ pickerList.add_child(item);
+ }
+ this._currencyPicker.set_child(pickerList);
+ this._root.add_child(this._currencyPicker);
+
+ const footer = new St.BoxLayout({ style_class: 'codeburn-footer' });
+
+ this._currencyBtn = new St.Button({
+ label: `${this._currency.code} ⌄`,
+ style_class: 'codeburn-footer-btn codeburn-currency-btn',
+ can_focus: true,
+ });
+ this._currencyBtn.connect('clicked', () => this._toggleCurrencyPicker());
+ footer.add_child(this._currencyBtn);
+
+ const refreshBtn = new St.Button({ label: 'Refresh', style_class: 'codeburn-footer-btn', can_focus: true, x_expand: true });
+ refreshBtn.connect('clicked', () => this._refresh(true));
+ footer.add_child(refreshBtn);
+
+ const reportBtn = new St.Button({ label: 'Full Report', style_class: 'codeburn-footer-btn codeburn-footer-cta', can_focus: true, x_expand: true });
+ reportBtn.connect('clicked', () => this._spawnTerminal(['codeburn', 'report', '--period', this._period, '--provider', this._provider]));
+ footer.add_child(reportBtn);
+
+ const prefsBtn = new St.Button({ label: '⚙', style_class: 'codeburn-footer-btn codeburn-prefs-btn', can_focus: true });
+ prefsBtn.connect('clicked', () => {
+ this._extension.openPreferences();
+ this.menu.close();
+ });
+ footer.add_child(prefsBtn);
+
+ this._root.add_child(footer);
+ this._updatedLabel = new St.Label({ text: '', style_class: 'codeburn-updated' });
+ this._root.add_child(this._updatedLabel);
+ }
+
+ // -- Settings --
+
+ _connectSettings() {
+ const watch = (key, cb) => {
+ const id = this._settings.connect(`changed::${key}`, cb);
+ this._settingsChangedIds.push(id);
+ };
+ watch('refresh-interval', () => this._restartRefreshLoop());
+ watch('compact-mode', () => { this._panelLabel.visible = !this._settings.get_boolean('compact-mode'); });
+ watch('codeburn-path', () => {
+ this._dataClient.setCodeburnPath(this._settings.get_string('codeburn-path'));
+ this._refresh(true);
+ });
+ watch('default-period', () => {
+ this._period = this._settings.get_string('default-period');
+ this._updatePeriodTabStyle();
+ this._refresh();
+ });
+ watch('budget-threshold', () => this._updateBudget());
+ watch('budget-alert-enabled', () => this._updateBudget());
+ watch('force-dark-mode', () => this._applyThemeClass());
+ watch('show-exact-costs', () => {
+ this._exactCosts = this._settings.get_boolean('show-exact-costs');
+ if (this._payload) this._render(this._payload);
+ });
+ watch('disabled-providers', () => {
+ if (this._payload) this._render(this._payload);
+ });
+ }
+
+ _getDisabledProviders() {
+ return new Set(this._settings.get_strv('disabled-providers'));
+ }
+
+ // -- Provider detection --
+
+ _detectProviders() {
+ const home = GLib.get_home_dir();
+ const xdgData = GLib.getenv('XDG_DATA_HOME') || `${home}/.local/share`;
+ const checks = Object.fromEntries(
+ Object.entries(PROVIDER_PATHS).map(([id, rel]) => [id, `${home}/${rel}`])
+ );
+ checks.opencode = `${xdgData}/opencode`;
+ const out = [];
+ for (const [id, path] of Object.entries(checks)) {
+ if (Gio.File.new_for_path(path).query_exists(null)) out.push(id);
+ }
+ return out;
+ }
+
+ // -- Refresh loop --
+
+ _startRefreshLoop() {
+ const interval = this._settings.get_uint('refresh-interval') || 30;
+ this._refreshSourceId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, interval, () => {
+ this._refresh();
+ return GLib.SOURCE_CONTINUE;
+ });
+ }
+
+ _restartRefreshLoop() {
+ if (this._refreshSourceId) {
+ GLib.Source.remove(this._refreshSourceId);
+ this._refreshSourceId = 0;
+ }
+ this._startRefreshLoop();
+ }
+
+ // -- Data fetching with cache --
+
+ _cacheKey() {
+ return `${this._period}|${this._provider}`;
+ }
+
+ async _refresh(force = false) {
+ const key = this._cacheKey();
+ const cached = this._payloadCache.get(key);
+ const cacheAge = cached ? Date.now() - cached.fetchedAt : Infinity;
+
+ if (!force && cached && cacheAge < CACHE_TTL_MS) {
+ this._payload = cached.payload;
+ this._render(this._payload);
+ return;
+ }
+
+ if (this._inFlightKeys.has(key)) return;
+ this._inFlightKeys.add(key);
+ const gen = ++this._refreshGen;
+
+ if (cached) {
+ this._payload = cached.payload;
+ this._render(this._payload);
+ } else {
+ this._showLoading();
+ if (this._contentArea) this._contentArea.opacity = 120;
+ }
+
+ try {
+ const payload = await this._dataClient.fetch(this._period, this._provider);
+ this._inFlightKeys.delete(key);
+ if (this._destroyed || gen !== this._refreshGen) return;
+ this._payloadCache.set(key, { payload, fetchedAt: Date.now() });
+ if (this._cacheKey() === key) {
+ this._payload = payload;
+ this._hideLoading();
+ if (this._contentArea) this._contentArea.opacity = 255;
+ this._render(this._payload);
+ }
+ } catch (e) {
+ this._inFlightKeys.delete(key);
+ if (this._destroyed) return;
+ this._hideLoading();
+ if (this._contentArea) this._contentArea.opacity = 255;
+ if (gen !== this._refreshGen) return;
+ if (e.message?.includes('cancelled')) return;
+ log(`CodeBurn: refresh error: ${e.message}`);
+ if (!this._payload) this._renderError(e.message);
+ }
+ }
+
+ // -- Rendering --
+
+ _render(payload) {
+ const current = payload?.current ?? {};
+ const cost = Number(current.cost ?? 0);
+
+ this._panelLabel.set_text(this._fmt(cost));
+ this._heroLabel.set_text(current.label || '');
+ this._heroAmount.set_text(this._fmt(cost));
+
+ const calls = Number(current.calls ?? 0);
+ const sessions = Number(current.sessions ?? 0);
+ this._heroMeta.set_text(`${calls.toLocaleString()} calls ${sessions} sessions`);
+
+ this._renderChart(payload?.history?.daily ?? []);
+ this._renderContent();
+ this._renderFindings(payload?.optimize ?? {});
+ this._updateBudget();
+
+ const updated = payload?.generated ? formatTime(new Date(payload.generated)) : '';
+ this._updatedLabel.set_text(updated ? `Updated ${updated}` : '');
+ }
+
+ _renderChart(daily) {
+ this._chartBars.destroy_all_children();
+ const days = Array.isArray(daily) ? daily.slice(-19) : [];
+ if (days.length === 0) {
+ this._chartContainer.visible = false;
+ return;
+ }
+ const inTotals = days.map(d => Number(d?.inputTokens) || 0);
+ const outTotals = days.map(d => Number(d?.outputTokens) || 0);
+ const totals = inTotals.map((v, i) => v + outTotals[i]);
+ let maxTotal = 1;
+ let totalIn = 0;
+ let totalOut = 0;
+ let hasAnyTokens = false;
+ for (let i = 0; i < days.length; i++) {
+ if (totals[i] > maxTotal) maxTotal = totals[i];
+ if (totals[i] > 0) hasAnyTokens = true;
+ totalIn += inTotals[i];
+ totalOut += outTotals[i];
+ }
+ if (!hasAnyTokens) {
+ this._chartContainer.visible = false;
+ return;
+ }
+ this._chartContainer.visible = true;
+ const summaryText = `In: ${formatTokensCompact(totalIn)} Out: ${formatTokensCompact(totalOut)}`;
+ this._chartTotal.set_text(summaryText);
+ this._chartSummaryText = summaryText;
+
+ const chartWidth = 308;
+ const gap = 2;
+ const barW = Math.max(4, Math.floor((chartWidth - gap * (days.length - 1)) / days.length));
+
+ for (let i = 0; i < days.length; i++) {
+ const h = Math.max(2, Math.round((totals[i] / maxTotal) * CHART_HEIGHT));
+ const col = new St.BoxLayout({ vertical: true, style_class: 'codeburn-chart-col', reactive: true });
+ col.set_width(barW);
+ col.set_height(CHART_HEIGHT);
+ const spacer = new St.Widget({ style_class: 'codeburn-chart-spacer' });
+ spacer.set_height(CHART_HEIGHT - h);
+ const bar = new St.Widget({ style_class: 'codeburn-chart-bar' });
+ bar.set_width(barW);
+ bar.set_height(h);
+ col.add_child(spacer);
+ col.add_child(bar);
+
+ const date = days[i]?.date || '';
+ const inTok = formatTokensCompact(inTotals[i]);
+ const outTok = formatTokensCompact(outTotals[i]);
+ const cost = days[i]?.cost != null ? this._fmt(days[i].cost) : '';
+ col.connect('enter-event', () => {
+ this._chartTotal.set_text(`${date} ${inTok}/${outTok} ${cost}`);
+ this._chartTotal.add_style_class_name('codeburn-chart-total-hover');
+ bar.add_style_class_name('codeburn-chart-bar-hover');
+ return Clutter.EVENT_PROPAGATE;
+ });
+ col.connect('leave-event', () => {
+ this._chartTotal.set_text(this._chartSummaryText);
+ this._chartTotal.remove_style_class_name('codeburn-chart-total-hover');
+ bar.remove_style_class_name('codeburn-chart-bar-hover');
+ return Clutter.EVENT_PROPAGATE;
+ });
+
+ this._chartBars.add_child(col);
+ }
+ }
+
+ _renderContent() {
+ this._contentArea.destroy_all_children();
+ switch (this._insight) {
+ case 'trend': return this._renderTrendView();
+ case 'forecast': return this._renderForecastView();
+ case 'pulse': return this._renderPulseView();
+ case 'stats': return this._renderStatsView();
+ default: return this._renderActivityView();
+ }
+ }
+
+ _renderActivityView() {
+ const current = this._payload?.current ?? {};
+ this._contentArea.add_child(this._sectionTitle('Activity'));
+ const actHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
+ actHeader.add_child(new St.Label({ text: 'Name', style_class: 'codeburn-th', x_expand: true }));
+ actHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
+ actHeader.add_child(new St.Label({ text: 'Turns', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
+ actHeader.add_child(new St.Label({ text: '1-shot', style_class: 'codeburn-th codeburn-th-right codeburn-th-turns' }));
+ this._contentArea.add_child(actHeader);
+ const rows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-rows' });
+ const activities = Array.isArray(current.topActivities) ? current.topActivities : [];
+ if (!activities.length) {
+ rows.add_child(new St.Label({ text: 'No activity for this period', style_class: 'codeburn-empty' }));
+ } else {
+ const maxCost = activities.reduce((m, a) => Math.max(m, Number(a.cost) || 0), 0) || 1;
+ for (const a of activities.slice(0, TOP_ACTIVITIES)) {
+ rows.add_child(this._buildActivityRow(a, maxCost));
+ }
+ }
+ this._contentArea.add_child(rows);
+
+ const models = Array.isArray(current.topModels) ? current.topModels : [];
+ if (models.length) {
+ this._contentArea.add_child(this._sectionTitle('Models'));
+ const modHeader = new St.BoxLayout({ style_class: 'codeburn-table-header' });
+ modHeader.add_child(new St.Label({ text: 'Model', style_class: 'codeburn-th', x_expand: true }));
+ modHeader.add_child(new St.Label({ text: 'Cost', style_class: 'codeburn-th codeburn-th-right codeburn-th-cost' }));
+ modHeader.add_child(new St.Label({ text: 'Calls', style_class: 'codeburn-th codeburn-th-right codeburn-th-calls' }));
+ this._contentArea.add_child(modHeader);
+ const mrows = new St.BoxLayout({ vertical: true, style_class: 'codeburn-models-rows' });
+ for (const m of models.slice(0, 3)) mrows.add_child(this._buildModelRow(m));
+ this._contentArea.add_child(mrows);
+ }
+ }
+
+ _renderTrendView() {
+ const daily = this._payload?.history?.daily ?? [];
+ if (!daily.length) {
+ this._contentArea.add_child(new St.Label({ text: 'Not enough history yet', style_class: 'codeburn-empty' }));
+ return;
+ }
+ for (const d of daily.slice(-7).reverse()) {
+ const row = new St.BoxLayout({ style_class: 'codeburn-trend-row' });
+ row.add_child(new St.Label({ text: d.date, style_class: 'codeburn-trend-date', x_expand: true }));
+ const costLabel = new St.Label({ text: this._fmt(d.cost), style_class: 'codeburn-trend-cost' });
+ costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
+ row.add_child(costLabel);
+ const callsLabel = new St.Label({ text: `${Number(d.calls).toLocaleString()} calls`, style_class: 'codeburn-trend-calls' });
+ callsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
+ row.add_child(callsLabel);
+ this._contentArea.add_child(row);
+ }
+ }
+
+ _renderForecastView() {
+ const daily = this._payload?.history?.daily ?? [];
+ if (daily.length < 3) {
+ this._contentArea.add_child(new St.Label({ text: 'Need at least 3 days of history', style_class: 'codeburn-empty' }));
+ return;
+ }
+ const last7 = daily.slice(-7);
+ const avg = last7.reduce((s, d) => s + Number(d.cost || 0), 0) / last7.length;
+ const yesterday = daily.at(-2);
+ const yestCost = Number(yesterday?.cost || 0);
+ const todCost = Number(daily.at(-1)?.cost || 0);
+ const dod = yestCost > 0 ? ((todCost - yestCost) / yestCost) * 100 : 0;
+ const now = new Date();
+ const dayOfMonth = now.getUTCDate();
+ const daysInMonth = new Date(now.getUTCFullYear(), now.getUTCMonth() + 1, 0).getUTCDate();
+
+ this._contentArea.add_child(this._kvRow('7-day avg', this._fmt(avg)));
+ this._contentArea.add_child(this._kvRow('Yesterday', yesterday ? this._fmt(yestCost) : '-'));
+ this._contentArea.add_child(this._kvRow('Day-over-day', `${dod > 0 ? '+' : ''}${dod.toFixed(1)}%`));
+ this._contentArea.add_child(this._kvRow('Month projection', this._fmt(avg * daysInMonth)));
+ this._contentArea.add_child(this._kvRow('Days elapsed', `${dayOfMonth} of ${daysInMonth}`));
+ }
+
+ _renderPulseView() {
+ const current = this._payload?.current ?? {};
+ const daily = this._payload?.history?.daily ?? [];
+ this._contentArea.add_child(this._sectionTitle('Pulse'));
+ const row = new St.BoxLayout({ style_class: 'codeburn-pulse-row' });
+ row.add_child(this._pulseTile(this._fmt(current.cost), 'cost'));
+ row.add_child(this._pulseTile(Number(current.calls || 0).toLocaleString(), 'calls'));
+ row.add_child(this._pulseTile(`${Number(current.cacheHitPercent || 0).toFixed(0)}%`, 'cache hit'));
+ this._contentArea.add_child(row);
+
+ if (daily.length) {
+ this._contentArea.add_child(this._sectionTitle('Last 7 days'));
+ const last7 = daily.slice(-7);
+ const sumCost = last7.reduce((s, d) => s + Number(d.cost || 0), 0);
+ const sumCalls = last7.reduce((s, d) => s + Number(d.calls || 0), 0);
+ const peakDay = last7.reduce((best, d) => Number(d.cost || 0) > Number(best.cost || 0) ? d : best, last7[0]);
+ this._contentArea.add_child(this._kvRow('Total spend', this._fmt(sumCost)));
+ this._contentArea.add_child(this._kvRow('Total calls', Number(sumCalls).toLocaleString()));
+ this._contentArea.add_child(this._kvRow('Peak day', `${peakDay?.date || '-'} ${this._fmt(peakDay?.cost)}`));
+ }
+ }
+
+ _renderStatsView() {
+ const current = this._payload?.current ?? {};
+ const daily = this._payload?.history?.daily ?? [];
+ this._contentArea.add_child(this._sectionTitle('Stats'));
+ const models = Array.isArray(current.topModels) ? current.topModels : [];
+ const favModel = models[0]?.name ?? '-';
+ const activeDays = daily.filter(d => Number(d.cost || 0) > 0).length;
+ const peakDay = daily.reduce((best, d) => Number(d.cost || 0) > Number((best || {}).cost || 0) ? d : best, null);
+ let streak = 0;
+ for (let i = daily.length - 1; i >= 0; i--) {
+ if (Number(daily[i].cost || 0) > 0) streak++;
+ else break;
+ }
+ this._contentArea.add_child(this._kvRow('Favorite model', favModel));
+ this._contentArea.add_child(this._kvRow('Active days', `${activeDays}`));
+ this._contentArea.add_child(this._kvRow('Current streak', `${streak} days`));
+ if (peakDay) this._contentArea.add_child(this._kvRow('Peak day', `${peakDay.date} ${this._fmt(peakDay.cost)}`));
+ }
+
+ _renderFindings(optimize) {
+ const count = Number(optimize?.findingCount ?? 0);
+ if (count === 0) {
+ this._findingsBtn.hide();
+ return;
+ }
+ const savings = Number(optimize?.savingsUSD ?? 0);
+ this._findingsCount.set_text(`${count} optimize findings`);
+ this._findingsSavings.set_text(`save ~${this._fmt(savings)}`);
+ this._findingsBtn.show();
+ }
+
+ _renderError(message) {
+ this._panelLabel.set_text('!');
+ if (message?.includes('not found') || message?.includes('No such file')) {
+ this._heroLabel.set_text('CodeBurn CLI not found');
+ this._heroMeta.set_text('Install: npm i -g codeburn');
+ } else {
+ this._heroLabel.set_text('Error loading data');
+ this._heroMeta.set_text(message?.substring(0, 80) || 'Unknown error');
+ }
+ this._heroAmount.set_text('');
+ this._findingsBtn.hide();
+ }
+
+ // -- Budget --
+
+ _updateBudget() {
+ const enabled = this._settings.get_boolean('budget-alert-enabled');
+ const threshold = this._settings.get_double('budget-threshold');
+ if (!enabled || threshold <= 0 || !this._payload?.current) {
+ this._budgetLabel.visible = false;
+ return;
+ }
+ const cost = Number(this._payload.current.cost ?? 0) * this._fxRate;
+ const thresholdConverted = threshold * this._fxRate;
+ if (cost >= thresholdConverted) {
+ this._budgetLabel.set_text(`Budget exceeded: ${this._fmt(cost)} / ${this._fmt(thresholdConverted)}`);
+ this._budgetLabel.visible = true;
+ } else {
+ this._budgetLabel.visible = false;
+ }
+ }
+
+ // -- Currency --
+
+ _loadCurrency() {
+ const configPath = GLib.build_filenamev([GLib.get_home_dir(), '.config', 'codeburn', 'config.json']);
+ try {
+ const [ok, contents] = GLib.file_get_contents(configPath);
+ if (ok) {
+ const config = JSON.parse(new TextDecoder().decode(contents));
+ if (config.currency?.code) {
+ const known = CURRENCIES.find(c => c.code === config.currency.code);
+ if (known) return known;
+ return { code: config.currency.code, symbol: config.currency.symbol || `${config.currency.code} ` };
+ }
+ }
+ } catch (_) { /* default */ }
+ return CURRENCIES[0];
+ }
+
+ _toggleCurrencyPicker() {
+ this._currencyPicker.visible = !this._currencyPicker.visible;
+ }
+
+ _setCurrency(code) {
+ try {
+ Gio.Subprocess.new(['codeburn', 'currency', code], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE);
+ } catch (_) { /* CLI missing */ }
+ const known = CURRENCIES.find(c => c.code === code);
+ this._currency = known || { code, symbol: `${code} ` };
+ this._currencyBtn.set_label(`${this._currency.code} ⌄`);
+ this._updateFxRate();
+ }
+
+ _updateFxRate() {
+ const code = this._currency?.code || 'USD';
+ if (this._fxCache[code] !== undefined) {
+ this._fxRate = this._fxCache[code];
+ if (this._payload) this._render(this._payload);
+ return;
+ }
+ const url = `https://api.frankfurter.app/latest?from=USD&to=${code}`;
+ const msg = Soup.Message.new('GET', url);
+ this._soupSession.send_and_read_async(msg, GLib.PRIORITY_DEFAULT, null, (session, result) => {
+ if (this._destroyed) return;
+ try {
+ const bytes = session.send_and_read_finish(result);
+ if (!bytes) return;
+ const json = JSON.parse(new TextDecoder().decode(bytes.get_data()));
+ const rate = json?.rates?.[code];
+ if (typeof rate === 'number' && rate > 0) {
+ this._fxCache[code] = rate;
+ this._fxRate = rate;
+ if (this._payload) this._render(this._payload);
+ }
+ } catch (_) { /* FX fetch failed */ }
+ });
+ }
+
+ _fmt(value) {
+ return formatCost(value, this._currency, this._fxRate, this._exactCosts);
+ }
+
+ // -- UI helpers --
+
+ _sectionTitle(text) {
+ return new St.Label({ text, style_class: 'codeburn-section-title' });
+ }
+
+ _kvRow(label, value) {
+ const row = new St.BoxLayout({ style_class: 'codeburn-kv-row' });
+ row.add_child(new St.Label({ text: label, style_class: 'codeburn-kv-label', x_expand: true }));
+ row.add_child(new St.Label({ text: String(value ?? '-'), style_class: 'codeburn-kv-value' }));
+ return row;
+ }
+
+ _pulseTile(value, label) {
+ const tile = new St.BoxLayout({ vertical: true, style_class: 'codeburn-pulse-tile', x_expand: true });
+ tile.add_child(new St.Label({ text: value, style_class: 'codeburn-pulse-value' }));
+ tile.add_child(new St.Label({ text: label, style_class: 'codeburn-pulse-label' }));
+ return tile;
+ }
+
+ _buildActivityRow(activity, maxCost) {
+ const row = new St.BoxLayout({ vertical: true, style_class: 'codeburn-activity-row' });
+ const topLine = new St.BoxLayout({ style_class: 'codeburn-activity-top' });
+ topLine.add_child(new St.Label({ text: activity.name, style_class: 'codeburn-activity-name', x_expand: true }));
+ const costLabel = new St.Label({ text: this._fmt(activity.cost), style_class: 'codeburn-activity-cost' });
+ costLabel.clutter_text.x_align = Clutter.ActorAlign.END;
+ topLine.add_child(costLabel);
+ const turnsLabel = new St.Label({ text: `${Number(activity.turns) || 0}`, style_class: 'codeburn-activity-turns' });
+ turnsLabel.clutter_text.x_align = Clutter.ActorAlign.END;
+ topLine.add_child(turnsLabel);
+ const osText = activity.oneShotRate != null ? `${Math.round(Number(activity.oneShotRate) * 100)}%` : '--';
+ const osLabel = new St.Label({ text: osText, style_class: 'codeburn-activity-oneshot' });
+ osLabel.clutter_text.x_align = Clutter.ActorAlign.END;
+ topLine.add_child(osLabel);
+ row.add_child(topLine);
+
+ const track = new St.BoxLayout({ style_class: 'codeburn-bar-track' });
+ const pct = Math.max(0.02, Math.min(1, Number(activity.cost) / maxCost));
+ const fill = new St.Widget({ style_class: 'codeburn-bar-fill' });
+ fill.set_width(Math.round(BAR_TRACK_WIDTH * pct));
+ track.add_child(fill);
+ row.add_child(track);
+ return row;
+ }
+
+ _buildModelRow(model) {
+ const row = new St.BoxLayout({ style_class: 'codeburn-model-row' });
+ row.add_child(new St.Label({ text: model.name, style_class: 'codeburn-model-name', x_expand: true }));
+ const mc = new St.Label({ text: this._fmt(model.cost), style_class: 'codeburn-model-cost' });
+ mc.clutter_text.x_align = Clutter.ActorAlign.END;
+ row.add_child(mc);
+ const mcalls = new St.Label({ text: `${Number(model.calls || 0).toLocaleString()}`, style_class: 'codeburn-model-calls' });
+ mcalls.clutter_text.x_align = Clutter.ActorAlign.END;
+ row.add_child(mcalls);
+ return row;
+ }
+
+ // -- Theme --
+
+ _applyThemeClass() {
+ const forceDark = this._settings.get_boolean('force-dark-mode');
+ const scheme = this._themeSettings.get_string('color-scheme');
+ const isDark = forceDark || scheme === 'prefer-dark';
+ if (isDark) {
+ this._root?.add_style_class_name('codeburn-dark');
+ this._root?.remove_style_class_name('codeburn-light');
+ } else {
+ this._root?.add_style_class_name('codeburn-light');
+ this._root?.remove_style_class_name('codeburn-dark');
+ }
+ }
+
+ // -- Terminal spawning --
+
+ _spawnTerminal(argv) {
+ const command = `${argv.join(' ')}; echo; read -n 1 -s -r -p 'Press any key to close...'`;
+ try {
+ Gio.Subprocess.new(['gnome-terminal', '--', 'bash', '-lc', command], Gio.SubprocessFlags.NONE);
+ } catch (e) {
+ log(`CodeBurn: terminal spawn error: ${e.message}`);
+ }
+ this.menu.close();
+ }
+
+ // -- Cleanup --
+
+ destroy() {
+ this._destroyed = true;
+ if (this._refreshSourceId) {
+ GLib.Source.remove(this._refreshSourceId);
+ this._refreshSourceId = 0;
+ }
+ if (this._themeSettings && this._themeSignal) {
+ this._themeSettings.disconnect(this._themeSignal);
+ this._themeSignal = null;
+ this._themeSettings = null;
+ }
+ for (const id of this._settingsChangedIds) this._settings.disconnect(id);
+ this._settingsChangedIds = [];
+ this._dataClient?.destroy();
+ if (this._soupSession) {
+ this._soupSession.abort();
+ this._soupSession = null;
+ }
+ super.destroy();
+ }
+});
diff --git a/gnome/install.sh b/gnome/install.sh
new file mode 100755
index 0000000..df03881
--- /dev/null
+++ b/gnome/install.sh
@@ -0,0 +1,38 @@
+#!/bin/bash
+set -euo pipefail
+
+UUID="codeburn@codeburn.dev"
+INSTALL_DIR="${HOME}/.local/share/gnome-shell/extensions/${UUID}"
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+
+echo "Installing CodeBurn GNOME extension..."
+
+# Compile GSettings schema
+echo "Compiling schemas..."
+glib-compile-schemas "${SCRIPT_DIR}/schemas/"
+
+# Create install directory
+mkdir -p "${INSTALL_DIR}"
+
+# Copy extension files
+cp "${SCRIPT_DIR}/metadata.json" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/extension.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/indicator.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/dataClient.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/prefs.js" "${INSTALL_DIR}/"
+cp "${SCRIPT_DIR}/stylesheet.css" "${INSTALL_DIR}/"
+
+# Copy schemas
+mkdir -p "${INSTALL_DIR}/schemas"
+cp "${SCRIPT_DIR}/schemas/"* "${INSTALL_DIR}/schemas/"
+
+# Copy icons
+mkdir -p "${INSTALL_DIR}/icons"
+cp "${SCRIPT_DIR}/icons/"* "${INSTALL_DIR}/icons/"
+
+echo "Extension installed to ${INSTALL_DIR}"
+echo ""
+echo "Next steps:"
+echo " 1. Restart GNOME Shell (log out and back in on Wayland)"
+echo " 2. Enable: gnome-extensions enable ${UUID}"
+echo " 3. Configure: gnome-extensions prefs ${UUID}"
diff --git a/gnome/metadata.json b/gnome/metadata.json
new file mode 100644
index 0000000..be8d2c0
--- /dev/null
+++ b/gnome/metadata.json
@@ -0,0 +1,8 @@
+{
+ "name": "CodeBurn Monitor",
+ "description": "Monitor AI coding assistant token usage and costs",
+ "uuid": "codeburn@codeburn.dev",
+ "shell-version": ["45", "46", "47", "48", "49", "50"],
+ "url": "https://github.com/getagentseal/codeburn",
+ "settings-schema": "org.gnome.shell.extensions.codeburn"
+}
diff --git a/gnome/prefs.js b/gnome/prefs.js
new file mode 100644
index 0000000..08d4b82
--- /dev/null
+++ b/gnome/prefs.js
@@ -0,0 +1,170 @@
+import Adw from 'gi://Adw';
+import Gtk from 'gi://Gtk';
+import Gio from 'gi://Gio';
+import { ExtensionPreferences } from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
+
+const PROVIDERS = [
+ { id: 'claude', label: 'Claude' },
+ { id: 'codex', label: 'Codex' },
+ { id: 'copilot', label: 'Copilot' },
+ { id: 'cursor', label: 'Cursor' },
+ { id: 'droid', label: 'Droid' },
+ { id: 'gemini', label: 'Gemini' },
+ { id: 'goose', label: 'Goose' },
+ { id: 'kilo-code', label: 'Kilo Code' },
+ { id: 'kiro', label: 'Kiro' },
+ { id: 'kimi', label: 'Kimi' },
+ { id: 'openclaw', label: 'OpenClaw' },
+ { id: 'opencode', label: 'OpenCode' },
+ { id: 'pi', label: 'Pi' },
+ { id: 'qwen', label: 'Qwen' },
+ { id: 'roo-code', label: 'Roo Code' },
+ { id: 'antigravity', label: 'Antigravity' },
+];
+
+const PERIODS = [
+ { id: 'today', label: 'Today' },
+ { id: 'week', label: '7 Days' },
+ { id: '30days', label: '30 Days' },
+ { id: 'month', label: 'Month' },
+ { id: 'all', label: '6 Months' },
+];
+
+export default class CodeBurnPreferences extends ExtensionPreferences {
+ fillPreferencesWindow(window) {
+ const settings = this.getSettings();
+
+ const displayPage = new Adw.PreferencesPage({
+ title: 'Display',
+ icon_name: 'preferences-desktop-display-symbolic',
+ });
+ window.add(displayPage);
+
+ const displayGroup = new Adw.PreferencesGroup({
+ title: 'Display',
+ description: 'Configure how CodeBurn appears in the panel',
+ });
+ displayPage.add(displayGroup);
+
+ const refreshRow = new Adw.SpinRow({
+ title: 'Refresh Interval',
+ subtitle: 'Seconds between data refreshes',
+ adjustment: new Gtk.Adjustment({
+ lower: 5,
+ upper: 300,
+ step_increment: 5,
+ page_increment: 30,
+ value: settings.get_uint('refresh-interval'),
+ }),
+ });
+ settings.bind('refresh-interval', refreshRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(refreshRow);
+
+ const compactRow = new Adw.SwitchRow({
+ title: 'Compact Mode',
+ subtitle: 'Show only the icon, hide the cost label',
+ });
+ settings.bind('compact-mode', compactRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(compactRow);
+
+ const darkModeRow = new Adw.SwitchRow({
+ title: 'Force Dark Mode',
+ subtitle: 'Always use dark theme for the popup',
+ });
+ settings.bind('force-dark-mode', darkModeRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(darkModeRow);
+
+ const exactCostsRow = new Adw.SwitchRow({
+ title: 'Show Exact Costs',
+ subtitle: 'Show full values like $2,655.23 instead of $2.7k',
+ });
+ settings.bind('show-exact-costs', exactCostsRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ displayGroup.add(exactCostsRow);
+
+ const periodModel = new Gtk.StringList();
+ for (const p of PERIODS)
+ periodModel.append(p.label);
+
+ const periodRow = new Adw.ComboRow({
+ title: 'Default Period',
+ subtitle: 'Time period shown when extension opens',
+ model: periodModel,
+ });
+ const currentPeriod = settings.get_string('default-period');
+ const periodIndex = PERIODS.findIndex(p => p.id === currentPeriod);
+ periodRow.set_selected(periodIndex >= 0 ? periodIndex : 0);
+ periodRow.connect('notify::selected', () => {
+ const idx = periodRow.get_selected();
+ if (idx >= 0 && idx < PERIODS.length)
+ settings.set_string('default-period', PERIODS[idx].id);
+ });
+ displayGroup.add(periodRow);
+
+ const alertsGroup = new Adw.PreferencesGroup({
+ title: 'Budget Alerts',
+ description: 'Get warned when spending exceeds a threshold',
+ });
+ displayPage.add(alertsGroup);
+
+ const budgetEnabledRow = new Adw.SwitchRow({
+ title: 'Enable Budget Alerts',
+ subtitle: 'Show a warning when daily spending exceeds the threshold',
+ });
+ settings.bind('budget-alert-enabled', budgetEnabledRow, 'active', Gio.SettingsBindFlags.DEFAULT);
+ alertsGroup.add(budgetEnabledRow);
+
+ const budgetRow = new Adw.SpinRow({
+ title: 'Daily Budget (USD)',
+ subtitle: 'Set to 0 to disable',
+ adjustment: new Gtk.Adjustment({
+ lower: 0,
+ upper: 1000,
+ step_increment: 1,
+ page_increment: 10,
+ value: settings.get_double('budget-threshold'),
+ }),
+ digits: 2,
+ });
+ settings.bind('budget-threshold', budgetRow, 'value', Gio.SettingsBindFlags.DEFAULT);
+ alertsGroup.add(budgetRow);
+
+ const providersGroup = new Adw.PreferencesGroup({
+ title: 'Providers',
+ description: 'Toggle providers on/off for cost accounting',
+ });
+ displayPage.add(providersGroup);
+
+ const disabledProviders = settings.get_strv('disabled-providers');
+
+ for (const provider of PROVIDERS) {
+ const row = new Adw.SwitchRow({
+ title: provider.label,
+ active: !disabledProviders.includes(provider.id),
+ });
+ row.connect('notify::active', () => {
+ const current = settings.get_strv('disabled-providers');
+ if (row.get_active()) {
+ settings.set_strv('disabled-providers', current.filter(p => p !== provider.id));
+ } else {
+ if (!current.includes(provider.id))
+ settings.set_strv('disabled-providers', [...current, provider.id]);
+ }
+ });
+ providersGroup.add(row);
+ }
+
+ const advancedGroup = new Adw.PreferencesGroup({
+ title: 'Advanced',
+ });
+ displayPage.add(advancedGroup);
+
+ const pathRow = new Adw.EntryRow({
+ title: 'CodeBurn CLI Path',
+ text: settings.get_string('codeburn-path'),
+ });
+ pathRow.connect('changed', () => {
+ settings.set_string('codeburn-path', pathRow.get_text());
+ });
+ advancedGroup.add(pathRow);
+ }
+}
diff --git a/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml b/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml
new file mode 100644
index 0000000..e122cd8
--- /dev/null
+++ b/gnome/schemas/org.gnome.shell.extensions.codeburn.gschema.xml
@@ -0,0 +1,68 @@
+
+
+
+
+
+ 30
+ Refresh interval
+ Seconds between automatic data refreshes
+
+
+
+
+ 'today'
+ Default time period
+ Period shown when extension opens (today, week, 30days, month, all)
+
+
+
+ 0.0
+ Budget threshold
+ Daily budget threshold in USD. Set to 0 to disable.
+
+
+
+ false
+ Enable budget alerts
+ Show warning when spending exceeds budget threshold
+
+
+
+ false
+ Compact mode
+ Show only icon in panel, hide cost label
+
+
+
+ false
+ Force dark mode
+ Always use dark theme for the popup, regardless of system theme
+
+
+
+ false
+ Show exact costs
+ Show full decimal values instead of compact notation (e.g. $2,655.23 instead of $2.7k)
+
+
+
+ ''
+ CodeBurn CLI path
+ Custom path to the codeburn executable. Leave empty to use PATH.
+
+
+
+ 'all'
+ Default provider filter
+ Default provider to filter by (all shows everything)
+
+
+
+ []
+ Disabled providers
+ Providers excluded from cost accounting and display
+
+
+
+
diff --git a/gnome/stylesheet.css b/gnome/stylesheet.css
new file mode 100644
index 0000000..74bf896
--- /dev/null
+++ b/gnome/stylesheet.css
@@ -0,0 +1,610 @@
+/* ---- panel button ---- */
+.codeburn-panel {
+ spacing: 4px;
+}
+.codeburn-flame {
+ font-size: 14px;
+}
+.codeburn-label {
+ font-weight: 500;
+ padding-left: 2px;
+ padding-right: 2px;
+}
+
+/* ---- popup host ---- */
+.codeburn-menu {
+ padding: 0;
+}
+.codeburn-host {
+ padding: 0;
+ margin: 0;
+ background: transparent;
+ border: none;
+}
+.codeburn-host:hover,
+.codeburn-host:focus,
+.codeburn-host:active,
+.codeburn-host:selected {
+ background: transparent;
+}
+.codeburn-root {
+ width: 340px;
+ height: 540px;
+ padding: 0;
+ spacing: 0;
+}
+.codeburn-scroll {
+ padding: 0;
+}
+
+/* ---- brand header ---- */
+.codeburn-brand-header {
+ padding: 14px 16px 10px 16px;
+ spacing: 2px;
+}
+.codeburn-brand-row {
+ spacing: 0;
+}
+.codeburn-brand-primary {
+ font-weight: 700;
+ font-size: 18px;
+}
+.codeburn-brand-accent {
+ font-weight: 700;
+ font-size: 18px;
+ color: #ff8c42;
+}
+.codeburn-brand-subhead {
+ font-size: 10.5px;
+ opacity: 0.55;
+}
+
+/* ---- tab rows ---- */
+.codeburn-tab-row {
+ padding: 4px 10px 8px 10px;
+ spacing: 4px;
+}
+.codeburn-period-row {
+ padding-top: 0;
+ padding-bottom: 10px;
+}
+.codeburn-tab,
+.codeburn-period {
+ padding: 5px 6px;
+ border-radius: 6px;
+ font-size: 11px;
+ font-weight: 500;
+ background: transparent;
+ border: none;
+ opacity: 0.7;
+ transition-duration: 80ms;
+}
+.codeburn-tab:hover,
+.codeburn-period:hover {
+ background: rgba(255, 140, 66, 0.08);
+ opacity: 1;
+}
+.codeburn-tab-active,
+.codeburn-period-active {
+ background: rgba(255, 140, 66, 0.18);
+ color: #ff8c42;
+ opacity: 1;
+ font-weight: 600;
+}
+.codeburn-agent-scroll {
+ padding: 0;
+}
+.codeburn-agent-badge {
+ padding: 3px 10px;
+ border-radius: 10px;
+ background: rgba(255, 140, 66, 0.12);
+ color: #ff8c42;
+ font-size: 10.5px;
+ font-weight: 500;
+}
+
+/* ---- hero ---- */
+.codeburn-hero {
+ padding: 4px 16px 10px 16px;
+ spacing: 2px;
+}
+.codeburn-hero-top {
+ spacing: 6px;
+}
+.codeburn-hero-dot {
+ width: 6px;
+ height: 6px;
+ border-radius: 3px;
+ background-color: #ff8c42;
+ margin-top: 7px;
+}
+.codeburn-hero-label {
+ font-size: 11px;
+ opacity: 0.65;
+ font-weight: 500;
+}
+.codeburn-hero-amount {
+ font-size: 28px;
+ font-weight: 700;
+ color: #ffd700;
+}
+.codeburn-hero-meta {
+ font-size: 11px;
+ opacity: 0.6;
+}
+
+/* ---- activity section ---- */
+.codeburn-section-title {
+ font-weight: 600;
+ font-size: 11px;
+ opacity: 0.6;
+ padding-bottom: 2px;
+}
+/* ---- table headers ---- */
+.codeburn-table-header {
+ spacing: 6px;
+ padding: 2px 0 4px 0;
+}
+.codeburn-th {
+ font-size: 10px;
+ font-weight: 600;
+ opacity: 0.45;
+}
+.codeburn-th-cost {
+ min-width: 64px;
+}
+.codeburn-th-turns {
+ min-width: 40px;
+}
+.codeburn-th-calls {
+ min-width: 50px;
+}
+
+.codeburn-activity-rows {
+ spacing: 0;
+}
+.codeburn-activity-row {
+ spacing: 3px;
+ padding: 6px 0;
+}
+.codeburn-activity-top {
+ spacing: 6px;
+}
+.codeburn-activity-name {
+ font-size: 11.5px;
+ font-weight: 500;
+ min-width: 120px;
+}
+.codeburn-activity-cost {
+ font-size: 11.5px;
+ font-family: monospace;
+ font-weight: 600;
+ color: #ffd700;
+ min-width: 64px;
+}
+.codeburn-activity-turns {
+ font-size: 10.5px;
+ font-family: monospace;
+ opacity: 0.6;
+ min-width: 40px;
+}
+.codeburn-activity-oneshot {
+ font-size: 10.5px;
+ font-family: monospace;
+ color: #4ec972;
+ min-width: 40px;
+}
+.codeburn-bar-track {
+ height: 4px;
+ border-radius: 2px;
+ background-color: rgba(255, 255, 255, 0.08);
+ width: 240px;
+}
+.codeburn-bar-fill {
+ height: 4px;
+ border-radius: 2px;
+ background-color: #ff8c42;
+}
+.codeburn-empty {
+ font-style: italic;
+ opacity: 0.55;
+ padding: 6px 0;
+}
+
+/* ---- loading skeleton ---- */
+.codeburn-loading {
+ padding: 10px 16px;
+ spacing: 10px;
+}
+.codeburn-skeleton-bar {
+ background-color: rgba(255, 140, 66, 0.15);
+ border-radius: 4px;
+}
+.codeburn-light .codeburn-skeleton-bar {
+ background-color: rgba(200, 80, 30, 0.12);
+}
+
+/* ---- findings CTA ---- */
+.codeburn-findings {
+ margin: 2px 16px 10px 16px;
+ padding: 9px 11px;
+ border-radius: 8px;
+ background: rgba(255, 140, 66, 0.12);
+ border: none;
+ transition-duration: 120ms;
+}
+.codeburn-findings:hover {
+ background: rgba(255, 140, 66, 0.2);
+}
+.codeburn-findings-inner {
+ spacing: 8px;
+}
+.codeburn-findings-count {
+ font-size: 11.5px;
+ font-weight: 600;
+ color: #ff8c42;
+}
+.codeburn-findings-savings {
+ font-size: 11.5px;
+ font-weight: 500;
+ color: #ff8c42;
+ opacity: 0.8;
+}
+
+/* ---- footer ---- */
+.codeburn-footer {
+ padding: 10px 12px;
+ spacing: 6px;
+}
+.codeburn-footer-btn {
+ padding: 6px 10px;
+ border-radius: 6px;
+ background: rgba(255, 255, 255, 0.05);
+ border: none;
+ font-size: 11px;
+ font-weight: 500;
+ transition-duration: 80ms;
+}
+.codeburn-footer-btn:hover {
+ background: rgba(255, 255, 255, 0.1);
+}
+.codeburn-currency-box {
+ spacing: 2px;
+}
+.codeburn-currency-btn {
+ font-family: monospace;
+ min-width: 62px;
+}
+.codeburn-currency-picker {
+ background: rgba(30, 30, 30, 0.95);
+ border-radius: 8px;
+ padding: 4px;
+ height: 180px;
+}
+.codeburn-currency-list {
+ spacing: 1px;
+}
+.codeburn-currency-item {
+ padding: 4px 10px;
+ border-radius: 4px;
+ font-size: 11px;
+ font-family: monospace;
+ background: transparent;
+ border: none;
+}
+.codeburn-currency-item:hover {
+ background: rgba(255, 140, 66, 0.12);
+}
+.codeburn-currency-item-active {
+ background: rgba(255, 140, 66, 0.2);
+ color: #ff8c42;
+ font-weight: 600;
+}
+.codeburn-footer-cta {
+ background: #c9521d;
+ color: #ffffff;
+}
+.codeburn-footer-cta:hover {
+ background: #ff8c42;
+}
+.codeburn-updated {
+ font-size: 10px;
+ opacity: 0.45;
+ padding: 0 16px 10px 16px;
+}
+
+/* ---- insight pills row ---- */
+.codeburn-insight-row {
+ padding: 4px 10px 8px 10px;
+ spacing: 4px;
+}
+.codeburn-insight-pill {
+ padding: 4px 4px;
+ border-radius: 6px;
+ font-size: 10.5px;
+ font-weight: 500;
+ background: transparent;
+ border: none;
+ opacity: 0.65;
+ transition-duration: 80ms;
+}
+.codeburn-insight-pill:hover {
+ background: rgba(255, 140, 66, 0.08);
+ opacity: 1;
+}
+.codeburn-insight-pill-active {
+ background: rgba(255, 140, 66, 0.18);
+ color: #ff8c42;
+ opacity: 1;
+ font-weight: 600;
+}
+
+/* ---- token histogram chart ---- */
+.codeburn-chart {
+ padding: 0 16px 10px 16px;
+ spacing: 4px;
+}
+.codeburn-chart-header {
+ spacing: 6px;
+}
+.codeburn-chart-label {
+ font-weight: 600;
+ font-size: 11px;
+ opacity: 0.6;
+}
+.codeburn-chart-total {
+ font-family: monospace;
+ font-size: 11px;
+ opacity: 0.7;
+ color: #ff8c42;
+}
+.codeburn-chart-bars {
+ spacing: 2px;
+ height: 52px;
+}
+.codeburn-chart-col {
+ height: 52px;
+}
+.codeburn-chart-spacer {
+ background: transparent;
+}
+.codeburn-chart-bar {
+ background-color: #ff8c42;
+ border-radius: 2px 2px 0 0;
+}
+.codeburn-chart-bar-hover {
+ background-color: #ffa94d;
+}
+.codeburn-chart-total-hover {
+ font-weight: 600;
+}
+.codeburn-divider {
+ height: 1px;
+ background-color: rgba(255, 255, 255, 0.08);
+ margin: 4px 16px;
+}
+
+/* ---- trend, pulse, stats, kv rows ---- */
+.codeburn-content {
+ padding: 6px 16px 10px 16px;
+ spacing: 6px;
+}
+.codeburn-trend-row,
+.codeburn-kv-row {
+ padding: 4px 0;
+ spacing: 8px;
+}
+.codeburn-trend-date,
+.codeburn-kv-label {
+ font-size: 11.5px;
+ font-weight: 500;
+}
+.codeburn-trend-cost,
+.codeburn-kv-value {
+ font-family: monospace;
+ font-size: 11.5px;
+ font-weight: 600;
+ color: #ffd700;
+}
+.codeburn-trend-calls {
+ font-size: 10.5px;
+ opacity: 0.6;
+ min-width: 62px;
+}
+
+/* ---- pulse tiles ---- */
+.codeburn-pulse-row {
+ spacing: 6px;
+ padding: 4px 0;
+}
+.codeburn-pulse-tile {
+ padding: 10px 8px;
+ border-radius: 8px;
+ background: rgba(255, 140, 66, 0.08);
+ spacing: 2px;
+}
+.codeburn-pulse-value {
+ font-size: 16px;
+ font-weight: 700;
+ color: #ff8c42;
+ font-family: monospace;
+}
+.codeburn-pulse-label {
+ font-size: 10px;
+ opacity: 0.6;
+}
+
+/* ---- models rows ---- */
+.codeburn-models-rows {
+ spacing: 0;
+ padding-top: 4px;
+}
+.codeburn-model-row {
+ spacing: 8px;
+ padding: 6px 0;
+}
+.codeburn-model-name {
+ font-size: 11.5px;
+ min-width: 120px;
+}
+.codeburn-model-cost {
+ font-family: monospace;
+ font-size: 11.5px;
+ color: #ffd700;
+ min-width: 64px;
+}
+.codeburn-model-calls {
+ font-family: monospace;
+ font-size: 10.5px;
+ opacity: 0.6;
+ min-width: 50px;
+}
+
+/* ---- settings gear button ---- */
+.codeburn-prefs-btn {
+ padding: 6px 8px;
+ font-size: 14px;
+}
+
+/* ---- budget warning ---- */
+.codeburn-budget-warning {
+ color: #e5a50a;
+ font-weight: bold;
+ font-size: 11.5px;
+ padding: 6px 16px;
+}
+
+/* ---- dark theme ---- */
+.codeburn-dark {
+ background-color: rgba(30, 30, 30, 0.98);
+ color: #e0e0e0;
+ border-radius: 12px;
+}
+.codeburn-dark .codeburn-brand-primary {
+ color: #ffffff;
+}
+.codeburn-dark .codeburn-brand-subhead {
+ color: rgba(255, 255, 255, 0.55);
+}
+.codeburn-dark .codeburn-hero-label,
+.codeburn-dark .codeburn-hero-meta {
+ color: rgba(255, 255, 255, 0.65);
+}
+.codeburn-dark .codeburn-section-title,
+.codeburn-dark .codeburn-th,
+.codeburn-dark .codeburn-chart-label {
+ color: rgba(255, 255, 255, 0.5);
+}
+.codeburn-dark .codeburn-activity-name,
+.codeburn-dark .codeburn-model-name,
+.codeburn-dark .codeburn-trend-date,
+.codeburn-dark .codeburn-kv-label {
+ color: #e0e0e0;
+}
+.codeburn-dark .codeburn-activity-turns,
+.codeburn-dark .codeburn-model-calls,
+.codeburn-dark .codeburn-trend-calls {
+ color: rgba(255, 255, 255, 0.5);
+}
+.codeburn-dark .codeburn-footer-btn {
+ background: rgba(255, 255, 255, 0.08);
+ color: #e0e0e0;
+}
+.codeburn-dark .codeburn-footer-btn:hover {
+ background: rgba(255, 255, 255, 0.14);
+}
+.codeburn-dark .codeburn-currency-picker {
+ background: rgba(20, 20, 20, 0.98);
+}
+.codeburn-dark .codeburn-currency-item {
+ color: #e0e0e0;
+}
+.codeburn-dark .codeburn-tab,
+.codeburn-dark .codeburn-period,
+.codeburn-dark .codeburn-insight-pill {
+ color: rgba(255, 255, 255, 0.7);
+}
+.codeburn-dark .codeburn-updated {
+ color: rgba(255, 255, 255, 0.45);
+}
+
+/* ---- light theme ---- */
+.codeburn-light {
+ background-color: rgba(255, 255, 255, 0.98);
+ color: #1a1a1a;
+ border-radius: 12px;
+}
+.codeburn-light .codeburn-brand-primary {
+ color: #1a1a1a;
+}
+.codeburn-light .codeburn-brand-subhead {
+ color: rgba(0, 0, 0, 0.5);
+}
+.codeburn-light .codeburn-hero-label,
+.codeburn-light .codeburn-hero-meta {
+ color: rgba(0, 0, 0, 0.6);
+}
+.codeburn-light .codeburn-hero-amount {
+ color: #c9521d;
+}
+.codeburn-light .codeburn-section-title,
+.codeburn-light .codeburn-th,
+.codeburn-light .codeburn-chart-label {
+ color: rgba(0, 0, 0, 0.45);
+}
+.codeburn-light .codeburn-activity-name,
+.codeburn-light .codeburn-model-name,
+.codeburn-light .codeburn-trend-date,
+.codeburn-light .codeburn-kv-label {
+ color: #1a1a1a;
+}
+.codeburn-light .codeburn-activity-cost,
+.codeburn-light .codeburn-model-cost,
+.codeburn-light .codeburn-trend-cost,
+.codeburn-light .codeburn-kv-value {
+ color: #c9521d;
+}
+.codeburn-light .codeburn-activity-turns,
+.codeburn-light .codeburn-model-calls,
+.codeburn-light .codeburn-trend-calls {
+ color: rgba(0, 0, 0, 0.5);
+}
+.codeburn-light .codeburn-activity-oneshot {
+ color: #1b7a35;
+}
+.codeburn-light .codeburn-bar-track {
+ background-color: rgba(0, 0, 0, 0.08);
+}
+.codeburn-light .codeburn-bar-fill {
+ background-color: #c9521d;
+}
+.codeburn-light .codeburn-chart-bar {
+ background-color: #c9521d;
+}
+.codeburn-light .codeburn-footer-btn {
+ background: rgba(0, 0, 0, 0.06);
+ color: #1a1a1a;
+}
+.codeburn-light .codeburn-footer-btn:hover {
+ background: rgba(0, 0, 0, 0.1);
+}
+.codeburn-light .codeburn-currency-picker {
+ background: rgba(245, 245, 245, 0.98);
+}
+.codeburn-light .codeburn-currency-item {
+ color: #1a1a1a;
+}
+.codeburn-light .codeburn-tab,
+.codeburn-light .codeburn-period,
+.codeburn-light .codeburn-insight-pill {
+ color: rgba(0, 0, 0, 0.65);
+}
+.codeburn-light .codeburn-pulse-tile {
+ background: rgba(255, 140, 66, 0.1);
+}
+.codeburn-light .codeburn-updated {
+ color: rgba(0, 0, 0, 0.4);
+}
+.codeburn-light .codeburn-divider {
+ background-color: rgba(0, 0, 0, 0.1);
+}
diff --git a/mac/README.md b/mac/README.md
index 3a7f1d7..b12b836 100644
--- a/mac/README.md
+++ b/mac/README.md
@@ -6,19 +6,17 @@ Native Swift + SwiftUI menubar app. The codeburn menubar surface.
- macOS 14+ (Sonoma)
- Swift 6.0+ toolchain (bundled with Xcode 16 or standalone)
-- `codeburn` CLI installed globally (`npm install -g codeburn`) or available at a path you pass via `CODEBURN_BIN`
+- `codeburn` CLI installed globally (`npm install -g codeburn`)
## Install (end users)
One command:
```bash
-npx codeburn menubar
+codeburn menubar
```
-That's it. The command downloads the latest `.app` from GitHub Releases, drops it into `~/Applications`, clears Gatekeeper quarantine, and launches it. Re-running it upgrades in place with `--force`, or just launches the existing copy otherwise.
-
-If you already have the CLI installed globally (`npm install -g codeburn`), `codeburn menubar` works the same way.
+That's it. The command records the persistent `codeburn` CLI path, downloads the latest `.app` from the newest `mac-v*` GitHub Release with a matching checksum, verifies it, drops it into `~/Applications`, clears Gatekeeper quarantine, and launches it. Re-running it upgrades in place with `--force`, or just launches the existing copy otherwise.
### Build from source
@@ -39,7 +37,7 @@ cd mac
swift build
# Point the app at your dev CLI build instead of the globally installed `codeburn`:
npm --prefix .. run build
-CODEBURN_BIN="node $(pwd)/../dist/cli.js" swift run
+CODEBURN_ALLOW_DEV_BIN=1 CODEBURN_BIN="node $(pwd)/../dist/cli.js" swift run
```
The app registers itself as a menubar accessory (`LSUIElement = true` at runtime). No Dock icon.
@@ -48,7 +46,7 @@ The app registers itself as a menubar accessory (`LSUIElement = true` at runtime
On launch and every 60 seconds thereafter, the app spawns `codeburn status --format menubar-json --no-optimize` directly (argv, no shell) via `CodeburnCLI.makeProcess` and decodes the JSON into `MenubarPayload`. The manual refresh button in the footer invokes the same command without `--no-optimize`, which includes optimize findings but takes longer.
-Override the binary via the `CODEBURN_BIN` environment variable (default: `codeburn` on PATH). The value is validated against a strict allowlist (alphanumerics plus `._/-` space) before use, so a malicious env var can't inject shell commands.
+Release installs record a persistent absolute CLI path in `~/Library/Application Support/CodeBurn/codeburn-cli-path.v1`, then fall back to Homebrew's common `codeburn` locations. For development only, set `CODEBURN_ALLOW_DEV_BIN=1` with `CODEBURN_BIN`; the value is validated against a strict allowlist before use, so a malicious env var can't inject shell commands.
## Project layout
diff --git a/mac/Scripts/package-app.sh b/mac/Scripts/package-app.sh
index 5672b5e..c9982a7 100755
--- a/mac/Scripts/package-app.sh
+++ b/mac/Scripts/package-app.sh
@@ -9,6 +9,8 @@
set -euo pipefail
VERSION="${1:-dev}"
+ASSET_VERSION="${VERSION#mac-}"
+BUNDLE_VERSION="${ASSET_VERSION#v}"
BUNDLE_NAME="CodeBurnMenubar.app"
BUNDLE_ID="org.agentseal.codeburn-menubar"
EXECUTABLE_NAME="CodeBurnMenubar"
@@ -66,9 +68,9 @@ cat > "${BUNDLE}/Contents/Info.plist" <CFBundlePackageType
APPL
CFBundleShortVersionString
- ${VERSION}
+ ${BUNDLE_VERSION}
CFBundleVersion
- ${VERSION}
+ ${BUNDLE_VERSION}
LSMinimumSystemVersion
${MIN_MACOS}
LSUIElement
@@ -85,19 +87,25 @@ cat > "${BUNDLE}/Contents/PkgInfo" <<'PKG'
APPL????
PKG
-# Ad-hoc sign so macOS treats the bundle as internally consistent. This satisfies the
-# minimum bundle-validity checks on macOS 14+ and prevents a class of Gatekeeper edge
-# cases on managed Macs. A Developer ID signature (separate setup) would additionally
-# surface the publisher name in Finder; not required here.
+# Ad-hoc sign so macOS treats the bundle as internally consistent. Release
+# notarization can layer a Developer ID signature on top, but this local step
+# must still fail closed if signing or verification breaks.
echo "▸ Ad-hoc signing..."
-codesign --force --sign - --timestamp=none --deep "${BUNDLE}" 2>/dev/null || true
-codesign --verify --deep --strict "${BUNDLE}" 2>/dev/null || echo " (signature verify skipped)"
+codesign --force --sign - --timestamp=none --deep "${BUNDLE}"
+codesign --verify --deep --strict "${BUNDLE}"
-ZIP_NAME="CodeBurnMenubar-${VERSION}.zip"
+ZIP_NAME="CodeBurnMenubar-${ASSET_VERSION}.zip"
ZIP_PATH="${DIST_DIR}/${ZIP_NAME}"
echo "▸ Packaging ${ZIP_NAME}..."
-(cd "${DIST_DIR}" && /usr/bin/ditto -c -k --keepParent "${BUNDLE_NAME}" "${ZIP_NAME}")
+(cd "${DIST_DIR}" && COPYFILE_DISABLE=1 /usr/bin/ditto -c -k --norsrc --keepParent "${BUNDLE_NAME}" "${ZIP_NAME}")
+
+CHECKSUM_NAME="${ZIP_NAME}.sha256"
+CHECKSUM_PATH="${DIST_DIR}/${CHECKSUM_NAME}"
+echo "▸ Computing SHA-256 checksum..."
+(cd "${DIST_DIR}" && shasum -a 256 "${ZIP_NAME}" > "${CHECKSUM_NAME}")
echo ""
echo "✓ Built ${ZIP_PATH}"
+echo "✓ Checksum ${CHECKSUM_PATH}"
+cat "${CHECKSUM_PATH}"
ls -la "${DIST_DIR}"
diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift
index 4ac3948..c901362 100644
--- a/mac/Sources/CodeBurnMenubar/AppStore.swift
+++ b/mac/Sources/CodeBurnMenubar/AppStore.swift
@@ -2,6 +2,7 @@ import Foundation
import Observation
private let cacheTTLSeconds: TimeInterval = 30
+private let interactiveRefreshResetSeconds: TimeInterval = 120
struct CachedPayload {
let payload: MenubarPayload
@@ -25,14 +26,48 @@ final class AppStore {
}
var showingAccentPicker: Bool = false
var currency: String = "USD"
- var isLoading: Bool = false
- var lastError: String?
+ var isLoading: Bool { loadingCountsByKey.values.contains { $0 > 0 } }
+ var isCurrentKeyLoading: Bool { loadingCountsByKey[currentKey, default: 0] > 0 }
+ var hasAttemptedCurrentKeyLoad: Bool { attemptedKeys.contains(currentKey) }
+ var lastError: String? { lastErrorByKey[currentKey] }
+ private var loadingCountsByKey: [PayloadCacheKey: Int] = [:]
+ private var loadingStartedAtByKey: [PayloadCacheKey: Date] = [:]
+ private var attemptedKeys: Set = []
+ private var lastErrorByKey: [PayloadCacheKey: String] = [:]
var subscription: SubscriptionUsage?
var subscriptionError: String?
- var subscriptionLoadState: SubscriptionLoadState = .idle
+ var subscriptionLoadState: SubscriptionLoadState = ClaudeCredentialStore.isBootstrapCompleted ? .loading : .notBootstrapped
var capacityEstimates: [String: CapacityEstimate] = [:]
+ var codexUsage: CodexUsage?
+ var codexError: String?
+ var codexLoadState: SubscriptionLoadState = CodexCredentialStore.isBootstrapCompleted ? .loading : .notBootstrapped
+
+ /// Generation tokens for the in-flight refresh tasks. Incremented on every
+ /// disconnect / reset so a fetch that started before the disconnect cannot
+ /// resume after the await and re-populate the freshly-cleared state.
+ private var claudeRefreshGen: Int = 0
+ private var codexRefreshGen: Int = 0
+
private var cache: [PayloadCacheKey: CachedPayload] = [:]
+ private var cacheDate: String = ""
+ private var switchTask: Task?
+ private var payloadRefreshGeneration: UInt64 = 0
+ /// Tracks the last successful fetch timestamp per key for stuck-loading
+ /// diagnostics. NOT used for cache-freshness logic — `CachedPayload.fetchedAt`
+ /// is authoritative there. This map persists across cache wipes (day
+ /// rollover, etc.) so we can distinguish "fresh install, never fetched"
+ /// from "cache was wiped 10 minutes ago and we still haven't refilled".
+ private var lastSuccessByKey: [PayloadCacheKey: Date] = [:]
+
+ private func staleSecondsForKey(_ key: PayloadCacheKey) -> TimeInterval {
+ guard let last = lastSuccessByKey[key] else { return .infinity }
+ return Date().timeIntervalSince(last)
+ }
+
+ private var todayAllKey: PayloadCacheKey {
+ PayloadCacheKey(period: .today, provider: .all)
+ }
private var currentKey: PayloadCacheKey {
PayloadCacheKey(period: selectedPeriod, provider: selectedProvider)
@@ -45,7 +80,16 @@ final class AppStore {
/// Today (across all providers) is pinned for the always-visible menubar icon, independent of
/// the popover's selected period or provider.
var todayPayload: MenubarPayload? {
- cache[PayloadCacheKey(period: .today, provider: .all)]?.payload
+ cache[todayAllKey]?.payload
+ }
+
+ var todayPayloadAgeSeconds: Int? {
+ guard let cached = cache[todayAllKey] else { return nil }
+ return Int(Date().timeIntervalSince(cached.fetchedAt))
+ }
+
+ var needsStatusPayloadRefresh: Bool {
+ cache[todayAllKey]?.isFresh != true
}
/// All-provider payload for the selected period. Used by the tab strip to show
@@ -58,47 +102,262 @@ final class AppStore {
cache[currentKey] != nil
}
+ var hasStaleLoading: Bool {
+ let now = Date()
+ return loadingStartedAtByKey.values.contains {
+ now.timeIntervalSince($0) > loadingWatchdogSeconds
+ }
+ }
+
+ var hasStaleInteractivePayload: Bool {
+ staleInteractivePayloadAgeSeconds != nil
+ }
+
+ var hasMissingInteractivePayloadWithoutAttempt: Bool {
+ cache[currentKey] == nil && !isCurrentKeyLoading && !hasAttemptedCurrentKeyLoad
+ }
+
+ var shouldResetInteractiveRefreshPipeline: Bool {
+ hasStaleLoading || hasStaleInteractivePayload || hasMissingInteractivePayloadWithoutAttempt
+ }
+
+ var staleInteractivePayloadAgeSeconds: Int? {
+ let keys = Set([
+ currentKey,
+ todayAllKey,
+ PayloadCacheKey(period: selectedPeriod, provider: .all),
+ ])
+ let staleAges = keys.compactMap { key -> TimeInterval? in
+ guard let cached = cache[key] else { return nil }
+ let age = Date().timeIntervalSince(cached.fetchedAt)
+ return age > interactiveRefreshResetSeconds ? age : nil
+ }
+ return staleAges.max().map(Int.init)
+ }
+
+ var needsInteractivePayloadRefresh: Bool {
+ let periodAllKey = PayloadCacheKey(period: selectedPeriod, provider: .all)
+ return cache[currentKey]?.isFresh != true ||
+ cache[todayAllKey]?.isFresh != true ||
+ cache[periodAllKey]?.isFresh != true ||
+ hasStaleLoading
+ }
+
+ /// True if any cached payload reports at least one provider. Used to keep the
+ /// AgentTabStrip visible across period/provider switches even when the current
+ /// key's payload is briefly empty (e.g. immediately after a `switchTo` and
+ /// before the new fetch lands).
+ var hasAnyProvidersInCache: Bool {
+ cache.values.contains { !$0.payload.current.providers.isEmpty }
+ }
+
+#if DEBUG
+ func setCachedPayloadForTesting(_ payload: MenubarPayload, period: Period, provider: ProviderFilter, fetchedAt: Date) {
+ cache[PayloadCacheKey(period: period, provider: provider)] = CachedPayload(payload: payload, fetchedAt: fetchedAt)
+ }
+#endif
+
var findingsCount: Int {
payload.optimize.findingCount
}
- /// Switch to a period. Always fetches fresh data so the user never sees stale numbers.
- func switchTo(period: Period) async {
+ /// Switch to a period. Cancels any in-flight switch and fetches provider-specific +
+ /// all-provider data in parallel so tab strip costs stay in sync with the hero.
+ func switchTo(period: Period) {
selectedPeriod = period
- await refresh(includeOptimize: true, force: true)
+ startInteractiveSelectionRefresh()
}
- /// Switch to a provider filter. Always fetches fresh data so the user never sees stale numbers.
- func switchTo(provider: ProviderFilter) async {
+ /// Switch to a provider filter. Cancels any in-flight switch so rapid tab tapping only
+ /// runs the CLI for the final selection. Fetches provider-specific and all-provider data
+ /// in parallel so the tab strip costs stay in sync with the hero.
+ func switchTo(provider: ProviderFilter) {
selectedProvider = provider
- await refresh(includeOptimize: true, force: true)
+ startInteractiveSelectionRefresh()
+ }
+
+ private func startInteractiveSelectionRefresh() {
+ switchTask?.cancel()
+ resetLoadingState()
+ let period = selectedPeriod
+ let provider = selectedProvider
+ lastErrorByKey[PayloadCacheKey(period: period, provider: provider)] = nil
+ switchTask = Task {
+ if provider == .all {
+ await refresh(includeOptimize: false, force: true, showLoading: true)
+ } else {
+ async let main: Void = refresh(includeOptimize: false, force: true, showLoading: true)
+ async let all: Void = refreshQuietly(period: period)
+ _ = await (main, all)
+ }
+ }
}
private var inFlightKeys: Set = []
- /// Refresh the currently selected (period, provider) combination. Guards against concurrent
- /// fetches for the same key so a slow initial request can't overwrite a newer one that
- /// finished first (which would show stale numbers the user has already moved past).
- /// When `force` is false (background timer), skips the CLI call if the cache is still fresh.
- func refresh(includeOptimize: Bool, force: Bool = false) async {
+ func resetLoadingState() {
+ payloadRefreshGeneration &+= 1
+ loadingCountsByKey.removeAll()
+ loadingStartedAtByKey.removeAll()
+ inFlightKeys.removeAll()
+ }
+
+ func resetRefreshState(clearCache: Bool = false) {
+ switchTask?.cancel()
+ switchTask = nil
+ resetLoadingState()
+ attemptedKeys.removeAll()
+ lastErrorByKey.removeAll()
+ if clearCache {
+ cache.removeAll()
+ }
+ }
+
+ private let loadingWatchdogSeconds: TimeInterval = 60
+
+ @discardableResult
+ func clearStaleLoadingIfNeeded() -> Bool {
+ let now = Date()
+ let staleEntries = loadingStartedAtByKey.filter {
+ now.timeIntervalSince($0.value) > loadingWatchdogSeconds
+ }
+ guard !staleEntries.isEmpty else { return false }
+
+ payloadRefreshGeneration &+= 1
+ for (key, started) in staleEntries {
+ NSLog("CodeBurn: loading stuck for %ds on %@/%@ — auto-clearing",
+ Int(now.timeIntervalSince(started)), key.period.rawValue, key.provider.rawValue)
+ loadingCountsByKey[key] = nil
+ loadingStartedAtByKey[key] = nil
+ inFlightKeys.remove(key)
+ if cache[key] == nil {
+ lastErrorByKey[key] = "Refresh took longer than expected. CodeBurn will keep retrying in the background."
+ }
+ }
+ return true
+ }
+
+ private func beginLoading(for key: PayloadCacheKey) {
+ if loadingCountsByKey[key, default: 0] == 0 {
+ loadingStartedAtByKey[key] = Date()
+ }
+ loadingCountsByKey[key, default: 0] += 1
+ }
+
+ private func finishLoading(for key: PayloadCacheKey) {
+ guard let count = loadingCountsByKey[key], count > 0 else { return }
+ if count == 1 {
+ loadingCountsByKey[key] = nil
+ loadingStartedAtByKey[key] = nil
+ } else {
+ loadingCountsByKey[key] = count - 1
+ }
+ }
+
+ private func currentCacheDate() -> String {
+ let formatter = DateFormatter()
+ formatter.dateFormat = "yyyy-MM-dd"
+ return formatter.string(from: Date())
+ }
+
+ private func invalidateStaleDayCache() {
+ let today = currentCacheDate()
+ if cacheDate != today {
+ payloadRefreshGeneration &+= 1
+ cache.removeAll()
+ loadingCountsByKey.removeAll()
+ loadingStartedAtByKey.removeAll()
+ inFlightKeys.removeAll()
+ attemptedKeys.removeAll()
+ lastErrorByKey.removeAll()
+ cacheDate = today
+ NSLog("CodeBurn: reset menubar payload cache for new day %@", today)
+ }
+ }
+
+ func invalidateCache() {
+ cache.removeAll()
+ }
+
+ func refresh(includeOptimize: Bool, force: Bool = false, showLoading: Bool = false) async {
+ invalidateStaleDayCache()
let key = currentKey
+ let cacheDateAtStart = cacheDate
+ let generationAtStart = payloadRefreshGeneration
if !force, cache[key]?.isFresh == true { return }
- guard !inFlightKeys.contains(key) else { return }
+ if inFlightKeys.contains(key) { return }
inFlightKeys.insert(key)
- if cache[key] == nil {
- isLoading = true
+ attemptedKeys.insert(key)
+ lastErrorByKey[key] = nil
+ let didShowLoading = showLoading || cache[key] == nil
+ if didShowLoading {
+ beginLoading(for: key)
+ }
+ // Diagnostic anchor: if this key has been empty for a long time (the
+ // popover would currently be showing "Loading..."), log how stale the
+ // miss is so the next time a user reports a stuck-loading bug we have
+ // a concrete data point — "no successful fetch for (today, claude)
+ // in 14 minutes" beats squinting at unified-log noise. We deliberately
+ // skip the first-attempt case (no prior success ever, finite check
+ // below filters .infinity) — that's just the cold path, not a bug.
+ let staleSeconds = staleSecondsForKey(key)
+ if staleSeconds.isFinite, staleSeconds > 120 {
+ NSLog("CodeBurn: refresh attempt for stale key \(key.period.rawValue)/\(key.provider.rawValue) — last success was \(Int(staleSeconds))s ago")
}
defer {
inFlightKeys.remove(key)
- isLoading = false
+ if didShowLoading {
+ finishLoading(for: key)
+ }
}
do {
let fresh = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: includeOptimize)
+ if generationAtStart != payloadRefreshGeneration {
+ NSLog("CodeBurn: dropping fetch result for \(key.period.rawValue)/\(key.provider.rawValue) — refresh pipeline reset mid-fetch")
+ return
+ }
+ if Task.isCancelled {
+ // Distinguish cancellation (user switched tabs mid-fetch) from
+ // the silent-no-result path. Without this log, a cancelled
+ // fetch leaves cache empty + lastError nil and the user sees
+ // perpetual loading with nothing in the diagnostics.
+ NSLog("CodeBurn: fetch for \(key.period.rawValue)/\(key.provider.rawValue) cancelled before result was applied")
+ return
+ }
+ // Day-rollover race guard: if the calendar date changed during the
+ // fetch, this payload was computed against yesterday's date and
+ // would pollute today's freshly-cleared cache. Drop it; the next
+ // tick will refetch with today's data.
+ if cacheDate != cacheDateAtStart || cacheDate != currentCacheDate() {
+ invalidateStaleDayCache()
+ NSLog("CodeBurn: dropping fetch result for \(key.period.rawValue)/\(key.provider.rawValue) — calendar rolled mid-fetch")
+ return
+ }
cache[key] = CachedPayload(payload: fresh, fetchedAt: Date())
- lastError = nil
+ lastSuccessByKey[key] = Date()
+ lastErrorByKey[key] = nil
} catch {
- lastError = String(describing: error)
+ if Task.isCancelled { return }
NSLog("CodeBurn: fetch failed for \(key.period.rawValue)/\(key.provider.rawValue): \(error)")
+ if includeOptimize, cache[key] == nil {
+ do {
+ let fallback = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: false)
+ guard !Task.isCancelled else { return }
+ if generationAtStart != payloadRefreshGeneration { return }
+ if cacheDate != cacheDateAtStart || cacheDate != currentCacheDate() {
+ invalidateStaleDayCache()
+ return
+ }
+ cache[key] = CachedPayload(payload: fallback, fetchedAt: Date())
+ lastSuccessByKey[key] = Date()
+ lastErrorByKey[key] = nil
+ return
+ } catch {
+ if Task.isCancelled { return }
+ NSLog("CodeBurn: fallback fetch also failed: \(error)")
+ }
+ }
+ lastErrorByKey[key] = String(describing: error)
}
let allKey = PayloadCacheKey(period: selectedPeriod, provider: .all)
@@ -110,37 +369,390 @@ final class AppStore {
/// Background refresh for a period other than the visible one (e.g. keeping today fresh for the menubar badge).
/// Does not toggle isLoading, so the popover's loading overlay is unaffected.
/// Always uses the .all provider since the menubar badge shows total spend.
- func refreshQuietly(period: Period) async {
+ func refreshQuietly(period: Period, force: Bool = false) async {
+ invalidateStaleDayCache()
+ let key = PayloadCacheKey(period: period, provider: .all)
+ if !force, cache[key]?.isFresh == true { return }
+ if inFlightKeys.contains(key) { return }
+ inFlightKeys.insert(key)
+ attemptedKeys.insert(key)
+ let cacheDateAtStart = cacheDate
+ let generationAtStart = payloadRefreshGeneration
+ if period == .today, let age = todayPayloadAgeSeconds, age > 120 {
+ NSLog("CodeBurn: refreshing stale today status payload after %ds", age)
+ }
+ defer {
+ inFlightKeys.remove(key)
+ }
do {
- let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: true)
- cache[PayloadCacheKey(period: period, provider: .all)] = CachedPayload(payload: fresh, fetchedAt: Date())
+ let fresh = try await DataClient.fetch(period: period, provider: .all, includeOptimize: false)
+ if generationAtStart != payloadRefreshGeneration {
+ NSLog("CodeBurn: dropping quiet fetch result for \(period.rawValue) — refresh pipeline reset mid-fetch")
+ return
+ }
+ // Same day-rollover guard as refresh(): drop yesterday's payload if
+ // the calendar rolled over during the fetch.
+ if cacheDate != cacheDateAtStart || cacheDate != currentCacheDate() {
+ invalidateStaleDayCache()
+ return
+ }
+ cache[key] = CachedPayload(payload: fresh, fetchedAt: Date())
+ lastSuccessByKey[key] = Date()
+ lastErrorByKey[key] = nil
} catch {
NSLog("CodeBurn: quiet refresh failed for \(period.rawValue): \(error)")
}
}
- /// Fetch Claude subscription usage. Sets subscription = nil on missing creds (API users / unauthenticated).
- /// Triggered lazily when the user opens the Plan pill, so the Keychain prompt only fires on intent.
- func refreshSubscription() async {
- subscriptionLoadState = .loading
+ /// User-initiated. Reads Claude's source (this is what triggers the macOS keychain
+ /// prompt for `Claude Code-credentials`). Once successful, subsequent background
+ /// refreshes go through our own keychain item without prompting.
+ func bootstrapSubscription() async {
+ subscriptionLoadState = .bootstrapping
do {
- let usage = try await SubscriptionClient.fetch()
+ let usage = try await ClaudeSubscriptionService.bootstrap()
subscription = usage
subscriptionError = nil
subscriptionLoadState = .loaded
await captureSnapshots(for: usage)
- } catch SubscriptionError.noCredentials {
- subscription = nil
- subscriptionError = nil
- subscriptionLoadState = .noCredentials
+ } catch let err as ClaudeSubscriptionService.FetchError {
+ applyFetchError(err)
} catch {
- subscription = nil
subscriptionError = String(describing: error)
subscriptionLoadState = .failed
- NSLog("CodeBurn: subscription fetch failed: \(error)")
}
}
+ /// Background refresh. No-op if the user has not yet connected. Never triggers
+ /// a keychain prompt — uses our own keychain item exclusively.
+ func refreshSubscription() async {
+ _ = await refreshSubscriptionReportingSuccess()
+ }
+
+ /// Same as `refreshSubscription` but returns whether the fetch produced a
+ /// `.loaded` state, so the caller can anchor cadence timing on real success
+ /// rather than every attempt.
+ @discardableResult
+ func refreshSubscriptionReportingSuccess() async -> Bool {
+ guard ClaudeCredentialStore.isBootstrapCompleted else {
+ if subscriptionLoadState != .notBootstrapped {
+ subscriptionLoadState = .notBootstrapped
+ }
+ return false
+ }
+ let gen = claudeRefreshGen
+ if subscription == nil { subscriptionLoadState = .loading }
+ do {
+ guard let usage = try await ClaudeSubscriptionService.refreshIfBootstrapped() else {
+ return false
+ }
+ // Disconnect-during-fetch guard: if the user clicked Disconnect
+ // while we were awaiting Anthropic, the generation token will
+ // have advanced and we must drop this result instead of writing
+ // it back over the freshly-cleared state.
+ guard gen == claudeRefreshGen else { return false }
+ subscription = usage
+ subscriptionError = nil
+ subscriptionLoadState = .loaded
+ await captureSnapshots(for: usage)
+ return true
+ } catch let err as ClaudeSubscriptionService.FetchError {
+ guard gen == claudeRefreshGen else { return false }
+ applyFetchError(err)
+ return false
+ } catch {
+ guard gen == claudeRefreshGen else { return false }
+ subscriptionError = sanitizeForUI(String(describing: error))
+ subscriptionLoadState = .failed
+ return false
+ }
+ }
+
+ /// User-initiated disconnect — clears our keychain item and bootstrap flag,
+ /// plus all derived state so a reconnect (potentially under a different
+ /// account or tier) starts clean. capacityEstimates and the snapshot store
+ /// would otherwise contaminate "Based on last cycle" projections.
+ func disconnectSubscription() {
+ ClaudeSubscriptionService.disconnect()
+ // Bump the generation token so any in-flight refreshSubscription that
+ // resumes after this point detects the disconnect and discards its
+ // result instead of re-populating the cleared state.
+ claudeRefreshGen &+= 1
+ subscription = nil
+ subscriptionError = nil
+ subscriptionLoadState = .notBootstrapped
+ capacityEstimates = [:]
+ Task.detached { await SubscriptionSnapshotStore.clearAll() }
+ // Notify the AppDelegate to clear its cadence-loop anchor so the next
+ // reconnect doesn't measure against a pre-disconnect timestamp.
+ NotificationCenter.default.post(name: .codeBurnSubscriptionDisconnected, object: nil)
+ }
+
+ // MARK: - Codex
+
+ func bootstrapCodex() async {
+ codexLoadState = .bootstrapping
+ do {
+ let usage = try await CodexSubscriptionService.bootstrap()
+ codexUsage = usage
+ codexError = nil
+ codexLoadState = .loaded
+ } catch let err as CodexSubscriptionService.FetchError {
+ applyCodexFetchError(err)
+ } catch {
+ codexError = sanitizeForUI(String(describing: error))
+ codexLoadState = .failed
+ }
+ }
+
+ func refreshCodex() async {
+ _ = await refreshCodexReportingSuccess()
+ }
+
+ @discardableResult
+ func refreshCodexReportingSuccess() async -> Bool {
+ guard CodexCredentialStore.isBootstrapCompleted else {
+ if codexLoadState != .notBootstrapped { codexLoadState = .notBootstrapped }
+ return false
+ }
+ let gen = codexRefreshGen
+ if codexUsage == nil { codexLoadState = .loading }
+ do {
+ guard let usage = try await CodexSubscriptionService.refreshIfBootstrapped() else {
+ return false
+ }
+ guard gen == codexRefreshGen else { return false }
+ codexUsage = usage
+ codexError = nil
+ codexLoadState = .loaded
+ return true
+ } catch let err as CodexSubscriptionService.FetchError {
+ guard gen == codexRefreshGen else { return false }
+ applyCodexFetchError(err)
+ return false
+ } catch {
+ guard gen == codexRefreshGen else { return false }
+ codexError = sanitizeForUI(String(describing: error))
+ codexLoadState = .failed
+ return false
+ }
+ }
+
+ func disconnectCodex() {
+ CodexSubscriptionService.disconnect()
+ codexRefreshGen &+= 1
+ codexUsage = nil
+ codexError = nil
+ codexLoadState = .notBootstrapped
+ NotificationCenter.default.post(name: .codeBurnSubscriptionDisconnected, object: nil)
+ }
+
+ private func applyCodexFetchError(_ err: CodexSubscriptionService.FetchError) {
+ let sanitized = sanitizeForUI(err.errorDescription)
+ codexError = sanitized
+ if err.isTerminal {
+ codexLoadState = .terminalFailure(reason: sanitized)
+ } else if let retryAt = err.rateLimitRetryAt {
+ codexLoadState = .transientFailure(retryAt: retryAt)
+ } else if case .notBootstrapped = err {
+ codexLoadState = .notBootstrapped
+ } else if case let .bootstrapFailed(storeErr) = err, case .bootstrapNoSource = storeErr {
+ codexLoadState = .noCredentials
+ } else {
+ codexLoadState = .failed
+ }
+ }
+
+ private func applyFetchError(_ err: ClaudeSubscriptionService.FetchError) {
+ let sanitized = sanitizeForUI(err.errorDescription)
+ subscriptionError = sanitized
+ if err.isTerminal {
+ subscriptionLoadState = .terminalFailure(reason: sanitized)
+ } else if let retryAt = err.rateLimitRetryAt {
+ subscriptionLoadState = .transientFailure(retryAt: retryAt)
+ } else if case .notBootstrapped = err {
+ subscriptionLoadState = .notBootstrapped
+ } else if case let .bootstrapFailed(storeErr) = err, case .bootstrapNoSource = storeErr {
+ subscriptionLoadState = .noCredentials
+ } else {
+ subscriptionLoadState = .failed
+ }
+ }
+
+ /// Strip control characters and any token-shaped substrings from server-error
+ /// strings before they land in NSLog or the UI. Anthropic / OpenAI error
+ /// envelopes don't typically echo tokens, but we also surface this in
+ /// unified-log paths readable by other local users via `log stream`.
+ private func sanitizeForUI(_ s: String?) -> String? {
+ guard let s, !s.isEmpty else { return nil }
+ var cleaned = s.replacingOccurrences(of: "\u{0000}", with: "")
+ // Token-shaped redaction. Apply to all known auth-token formats so
+ // an error body that quotes the request/response token is masked.
+ let patterns: [(pattern: String, replacement: String)] = [
+ (#"sk-ant-[A-Za-z0-9_-]+"#, "sk-ant-***"),
+ (#"sk-[A-Za-z0-9_-]{16,}"#, "sk-***"),
+ (#"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"#, "eyJ***"),
+ (#"(?i)Bearer\s+\S+"#, "Bearer ***"),
+ ]
+ for entry in patterns {
+ cleaned = cleaned.replacingOccurrences(of: entry.pattern, with: entry.replacement, options: .regularExpression)
+ }
+ // Cap length so a runaway server body cannot fill stderr.
+ if cleaned.count > 240 { cleaned = String(cleaned.prefix(240)) + "…" }
+ return cleaned
+ }
+
+ /// Snapshot of live quota state for a given provider. Returns nil when the user
+ /// has not connected yet — the bar slot stays empty so we never trigger a
+ /// keychain prompt at startup. Once bootstrapped, the bar persists across all
+ /// subsequent states (loading / stale / transient failure / terminal failure)
+ /// so it doesn't flicker on every refresh tick.
+ /// Aggregate quota status across all connected providers, used by the menu
+ /// bar flame icon (color) and the popover warning row. Severity = worst
+ /// observed across any provider's worst window. Warning providers are
+ /// every connected provider at >= 70% utilization.
+ struct AggregateQuotaStatus {
+ let severity: QuotaSummary.Severity
+ let warnings: [(name: String, percent: Double)] // sorted desc by percent
+ }
+
+ var aggregateQuotaStatus: AggregateQuotaStatus {
+ var providers: [(name: String, percent: Double)] = []
+ if let usage = subscription, shouldIncludeCachedQuota(loadState: subscriptionLoadState) {
+ let worst = [
+ usage.fiveHourPercent,
+ usage.sevenDayPercent,
+ usage.sevenDayOpusPercent,
+ usage.sevenDaySonnetPercent,
+ ].compactMap { $0 }.max() ?? 0
+ if worst > 0 { providers.append(("Claude", worst)) }
+ }
+ if let usage = codexUsage, shouldIncludeCachedQuota(loadState: codexLoadState) {
+ let worst = max(usage.primary?.usedPercent ?? 0, usage.secondary?.usedPercent ?? 0)
+ if worst > 0 { providers.append(("Codex", worst)) }
+ }
+ let worst = providers.map(\.percent).max() ?? 0
+ let severity = QuotaSummary.severity(for: worst / 100)
+ let sorted = providers.sorted { $0.percent > $1.percent }
+ let warnings = sorted.filter { $0.percent >= 70 }
+ return AggregateQuotaStatus(severity: severity, warnings: warnings)
+ }
+
+ private func shouldIncludeCachedQuota(loadState: SubscriptionLoadState) -> Bool {
+ switch loadState {
+ case .notBootstrapped, .bootstrapping, .noCredentials:
+ return false
+ case .loading, .loaded, .failed, .terminalFailure, .transientFailure:
+ return true
+ }
+ }
+
+ func quotaSummary(for filter: ProviderFilter) -> QuotaSummary? {
+ switch filter {
+ case .claude: return claudeQuotaSummary(filter: filter)
+ case .codex: return codexQuotaSummary(filter: filter)
+ default: return nil
+ }
+ }
+
+ private func claudeQuotaSummary(filter: ProviderFilter) -> QuotaSummary? {
+ if case .notBootstrapped = subscriptionLoadState { return nil }
+ if case .bootstrapping = subscriptionLoadState { return nil }
+ if case .noCredentials = subscriptionLoadState { return nil }
+
+ let connection: QuotaSummary.Connection = {
+ switch subscriptionLoadState {
+ case .notBootstrapped, .bootstrapping, .noCredentials: return .disconnected
+ case .loading: return subscription == nil ? .loading : .stale
+ case .loaded: return .connected
+ case .failed: return subscription == nil ? .loading : .stale
+ case let .terminalFailure(reason): return .terminalFailure(reason: reason)
+ case .transientFailure: return .transientFailure
+ }
+ }()
+
+ var primary: QuotaSummary.Window?
+ var details: [QuotaSummary.Window] = []
+ if let usage = subscription {
+ if let pct = usage.fiveHourPercent {
+ details.append(.init(label: "5-hour", percent: pct / 100, resetsAt: usage.fiveHourResetsAt))
+ }
+ if let pct = usage.sevenDayPercent {
+ let weekly = QuotaSummary.Window(label: "Weekly", percent: pct / 100, resetsAt: usage.sevenDayResetsAt)
+ primary = weekly
+ details.append(weekly)
+ }
+ if let pct = usage.sevenDayOpusPercent {
+ details.append(.init(label: "Weekly · Opus", percent: pct / 100, resetsAt: usage.sevenDayOpusResetsAt))
+ }
+ if let pct = usage.sevenDaySonnetPercent {
+ details.append(.init(label: "Weekly · Sonnet", percent: pct / 100, resetsAt: usage.sevenDaySonnetResetsAt))
+ }
+ }
+ let plan = subscription?.tier.displayName
+ return QuotaSummary(providerFilter: filter, connection: connection, primary: primary, details: details, planLabel: plan, footerLines: [])
+ }
+
+ private func codexQuotaSummary(filter: ProviderFilter) -> QuotaSummary? {
+ if case .notBootstrapped = codexLoadState { return nil }
+ if case .bootstrapping = codexLoadState { return nil }
+ if case .noCredentials = codexLoadState { return nil }
+
+ let connection: QuotaSummary.Connection = {
+ switch codexLoadState {
+ case .notBootstrapped, .bootstrapping, .noCredentials: return .disconnected
+ case .loading: return codexUsage == nil ? .loading : .stale
+ case .loaded: return .connected
+ case .failed: return codexUsage == nil ? .loading : .stale
+ case let .terminalFailure(reason): return .terminalFailure(reason: reason)
+ case .transientFailure: return .transientFailure
+ }
+ }()
+
+ var primary: QuotaSummary.Window?
+ var details: [QuotaSummary.Window] = []
+ if let usage = codexUsage {
+ if let w = usage.primary {
+ let row = QuotaSummary.Window(label: w.windowLabel, percent: w.usedPercent / 100, resetsAt: w.resetsAt)
+ primary = row
+ details.append(row)
+ }
+ if let w = usage.secondary {
+ let row = QuotaSummary.Window(label: w.windowLabel, percent: w.usedPercent / 100, resetsAt: w.resetsAt)
+ // Some Codex plans (free / guest tiers) only return a secondary
+ // window. Promote it to primary so the chip bar always has a
+ // data source instead of rendering as an empty track.
+ if primary == nil { primary = row }
+ details.append(row)
+ }
+ // Surface per-model additional rate limits (e.g. "GPT-5.3-Codex-Spark")
+ // only when the user has actually hit them. Skipping zero rows keeps
+ // the popover compact for the common case where the user only uses
+ // the main Codex window.
+ for extra in usage.additionalLimits {
+ if let p = extra.primary, p.usedPercent > 0 {
+ details.append(.init(label: "\(extra.name) · \(p.windowLabel)", percent: p.usedPercent / 100, resetsAt: p.resetsAt))
+ }
+ if let s = extra.secondary, s.usedPercent > 0 {
+ details.append(.init(label: "\(extra.name) · \(s.windowLabel)", percent: s.usedPercent / 100, resetsAt: s.resetsAt))
+ }
+ }
+ }
+ let plan = codexUsage?.plan.displayName
+ var footerLines: [String] = []
+ if let balance = codexUsage?.creditsBalance, balance > 0 {
+ // Format as plain dollars; ChatGPT settles in USD regardless of
+ // the user's display-currency preference.
+ let formatter = NumberFormatter()
+ formatter.numberStyle = .currency
+ formatter.currencyCode = "USD"
+ formatter.maximumFractionDigits = 2
+ let formatted = formatter.string(from: NSNumber(value: balance)) ?? "$\(balance)"
+ footerLines.append("Credits remaining · \(formatted)")
+ }
+ return QuotaSummary(providerFilter: filter, connection: connection, primary: primary, details: details, planLabel: plan, footerLines: footerLines)
+ }
+
/// Persist one snapshot per window so we can answer "what did the prior cycle end at?"
/// when the current window has just reset and projection from current data isn't meaningful.
/// Also computes the effective_tokens consumed inside each 7-day window from local history,
@@ -174,7 +786,10 @@ final class AppStore {
/// last 7 days of dailyHistory. Used as the "tokens consumed in 7-day window" reading paired
/// with the API-reported percent for capacity estimation.
private func effectiveTokensInLast7Days(history: [DailyHistoryEntry], asOf now: Date) -> Double {
- let cutoff = ISO8601DateFormatter().string(from: now.addingTimeInterval(-7 * 86400)).prefix(10)
+ let f = DateFormatter()
+ f.dateFormat = "yyyy-MM-dd"
+ f.timeZone = .current
+ let cutoff = f.string(from: now.addingTimeInterval(-7 * 86400))
return history
.filter { $0.date >= cutoff }
.reduce(0.0) { $0 + $1.effectiveTokens }
@@ -227,12 +842,16 @@ enum SupportedCurrency: String, CaseIterable, Identifiable {
enum ProviderFilter: String, CaseIterable, Identifiable {
case all = "All"
case claude = "Claude"
+ case cline = "Cline"
case codex = "Codex"
case cursor = "Cursor"
+ case cursorAgent = "Cursor Agent"
case copilot = "Copilot"
case droid = "Droid"
case gemini = "Gemini"
+ case ibmBob = "IBM Bob"
case kiro = "Kiro"
+ case kimi = "Kimi"
case kiloCode = "KiloCode"
case openclaw = "OpenClaw"
case opencode = "OpenCode"
@@ -240,15 +859,23 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
case qwen = "Qwen"
case omp = "OMP"
case rooCode = "Roo Code"
+ case crush = "Crush"
+ case antigravity = "Antigravity"
+ case goose = "Goose"
var id: String { rawValue }
var providerKeys: [String] {
switch self {
- case .cursor: ["cursor", "cursor agent"]
+ case .cursor: ["cursor"]
+ case .cursorAgent: ["cursor-agent", "cursor agent"]
+ case .cline: ["cline"]
case .rooCode: ["roo-code", "roo code"]
case .kiloCode: ["kilo-code", "kilocode"]
+ case .ibmBob: ["ibm-bob", "ibm bob"]
case .openclaw: ["openclaw"]
+ case .antigravity: ["antigravity"]
+ case .goose: ["goose"]
default: [rawValue.lowercased()]
}
}
@@ -257,29 +884,43 @@ enum ProviderFilter: String, CaseIterable, Identifiable {
switch self {
case .all: "all"
case .claude: "claude"
+ case .cline: "cline"
case .codex: "codex"
case .cursor: "cursor"
+ case .cursorAgent: "cursor-agent"
case .copilot: "copilot"
case .droid: "droid"
case .gemini: "gemini"
+ case .ibmBob: "ibm-bob"
case .kiloCode: "kilo-code"
case .kiro: "kiro"
+ case .kimi: "kimi"
case .openclaw: "openclaw"
case .opencode: "opencode"
case .pi: "pi"
case .qwen: "qwen"
case .omp: "omp"
case .rooCode: "roo-code"
+ case .crush: "crush"
+ case .antigravity: "antigravity"
+ case .goose: "goose"
}
}
}
+extension Notification.Name {
+ static let codeBurnSubscriptionDisconnected = Notification.Name("com.codeburn.subscriptionDisconnected")
+}
+
enum SubscriptionLoadState: Sendable, Equatable {
- case idle // never tried, awaiting user intent
- case loading // fetch in progress
- case loaded // success; subscription is populated
- case noCredentials // tried; user has no Claude OAuth (API user / not logged in)
- case failed // tried; error occurred
+ case notBootstrapped // no Keychain access yet — waiting for user to click Connect
+ case bootstrapping // user clicked Connect; reading Claude's keychain (PROMPTS)
+ case loading // background fetch in progress (subscription may already be populated)
+ case loaded // success; subscription is populated
+ case noCredentials // bootstrap tried; user has no Claude credentials at all
+ case failed // generic non-recoverable failure
+ case terminalFailure(reason: String?) // refresh-token invalid; user must reconnect
+ case transientFailure(retryAt: Date?) // 429 / network blip; backing off automatically
}
enum InsightMode: String, CaseIterable, Identifiable {
@@ -296,7 +937,7 @@ enum Period: String, CaseIterable, Identifiable {
case sevenDays = "7 Days"
case thirtyDays = "30 Days"
case month = "Month"
- case all = "All"
+ case all = "6 Months"
var id: String { rawValue }
@@ -333,7 +974,7 @@ private let thousandsFormatter: NumberFormatter = {
return f
}()
-extension Double {
+@MainActor extension Double {
func asCurrency() -> String {
let state = CurrencyState.shared
let converted = self * state.rate
diff --git a/mac/Sources/CodeBurnMenubar/AppVersion.swift b/mac/Sources/CodeBurnMenubar/AppVersion.swift
new file mode 100644
index 0000000..c5ee14a
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/AppVersion.swift
@@ -0,0 +1,43 @@
+import Foundation
+
+enum AppVersion {
+ static var bundleShortVersion: String {
+ Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
+ }
+
+ static var bundleBuildVersion: String {
+ Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? ""
+ }
+
+ static var normalizedBundleShortVersion: String {
+ normalize(bundleShortVersion)
+ }
+
+ static var normalizedBundleBuildVersion: String {
+ normalize(bundleBuildVersion)
+ }
+
+ static var displayBundleShortVersion: String {
+ display(bundleShortVersion)
+ }
+
+ static func normalize(_ version: String) -> String {
+ let trimmed = version.trimmingCharacters(in: .whitespacesAndNewlines)
+ if trimmed.lowercased().hasPrefix("mac-v") {
+ return String(trimmed.dropFirst(5))
+ }
+ if trimmed.lowercased().hasPrefix("v") {
+ return String(trimmed.dropFirst())
+ }
+ return trimmed
+ }
+
+ static func display(_ version: String) -> String {
+ let normalized = normalize(version)
+ guard !normalized.isEmpty else { return "v?" }
+ if normalized == "?" || normalized == "dev" || normalized == "dev-preview" || normalized == "—" {
+ return normalized
+ }
+ return "v\(normalized)"
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift
index 1c87221..6191575 100644
--- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift
+++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift
@@ -3,8 +3,11 @@ import AppKit
import Observation
private let refreshIntervalSeconds: UInt64 = 30
-private let nanosPerSecond: UInt64 = 1_000_000_000
-private let refreshIntervalNanos: UInt64 = refreshIntervalSeconds * nanosPerSecond
+private let forceRefreshWatchdogSeconds: TimeInterval = 90
+private let refreshLoopWatchdogSeconds: TimeInterval = 90
+private let statusPayloadRefreshWatchdogSeconds: TimeInterval = 60
+private let refreshRateLimitSeconds: TimeInterval = 5
+private let interactiveQuotaRefreshFloorSeconds: TimeInterval = 30
private let statusItemWidth: CGFloat = NSStatusItem.variableLength
private let popoverWidth: CGFloat = 360
private let popoverHeight: CGFloat = 660
@@ -15,9 +18,12 @@ struct CodeBurnApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene {
- // SwiftUI App needs at least one scene. Settings is invisible by default.
+ // The Settings scene gives us a real macOS Settings window with the
+ // standard ⌘, shortcut and the menubar "Settings…" item. Provider tabs
+ // (Claude today, Codex/Cursor/etc. in follow-ups) live inside SettingsView.
Settings {
- EmptyView()
+ SettingsView()
+ .environment(delegate.store)
}
}
}
@@ -26,49 +32,92 @@ struct CodeBurnApp: App {
final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
private var statusItem: NSStatusItem!
private var popover: NSPopover!
- private let store = AppStore()
+ fileprivate let store = AppStore()
let updateChecker = UpdateChecker()
/// Held for the lifetime of the app to opt out of App Nap and Automatic Termination.
private var backgroundActivity: NSObjectProtocol?
+ private var pendingRefreshWork: DispatchWorkItem?
+ private var refreshTimer: DispatchSourceTimer?
+ private var forceRefreshTask: Task?
+ private var forceRefreshStartedAt: Date?
+ private var forceRefreshGeneration: UInt64 = 0
+ private var statusPayloadRefreshTask: Task?
+ private var statusPayloadRefreshStartedAt: Date?
+ private var statusPayloadRefreshGeneration: UInt64 = 0
+ private var manualRefreshTask: Task?
+ private var manualRefreshGeneration: UInt64 = 0
+ private var claudeQuotaRefreshTask: Task?
+ private var codexQuotaRefreshTask: Task?
+ private var refreshLoopHeartbeatAt: Date = .distantPast
+ private var lastLaunchAgentHeartbeatAt: Date = .distantPast
+
+ func applicationWillFinishLaunching(_ notification: Notification) {
+ // Set accessory policy before the app's focus chain forms. On macOS Tahoe
+ // (26.x), setting it after didFinishLaunching causes ghost status items
+ // because the policy gets baked into the initial focus chain.
+ NSApp.setActivationPolicy(.accessory)
+ }
+
+ private func observeSubscriptionDisconnect() {
+ NotificationCenter.default.addObserver(
+ forName: .codeBurnSubscriptionDisconnected,
+ object: nil,
+ queue: .main
+ ) { [weak self] _ in
+ Task { @MainActor [weak self] in
+ self?.resetSubscriptionCadenceAnchor()
+ }
+ }
+ }
func applicationDidFinishLaunching(_ notification: Notification) {
- // On macOS Tahoe (26.x), accessory apps may fail to render their status item
- // if the activation policy is set before the status item is created. Starting
- // as a regular app and switching to accessory after setup works around this.
- NSApp.setActivationPolicy(.regular)
- NSApp.activate(ignoringOtherApps: true)
-
ProcessInfo.processInfo.automaticTerminationSupportEnabled = false
ProcessInfo.processInfo.disableSuddenTermination()
backgroundActivity = ProcessInfo.processInfo.beginActivity(
- options: [.userInitiated, .automaticTerminationDisabled, .suddenTerminationDisabled],
- reason: "CodeBurn menubar polls AI coding cost every 15 seconds while idle in the background."
+ options: [.automaticTerminationDisabled, .suddenTerminationDisabled],
+ reason: "CodeBurn menubar background refresh"
)
restorePersistedCurrency()
setupStatusItem()
setupPopover()
-
- // Switch to accessory policy after status item is set up to hide from Dock
- DispatchQueue.main.async {
- NSApp.setActivationPolicy(.accessory)
- }
observeStore()
startRefreshLoop()
setupWakeObservers()
setupDistributedNotificationListener()
installLaunchAgentIfNeeded()
registerLoginItemIfNeeded()
+ observeSubscriptionDisconnect()
Task { await updateChecker.checkIfNeeded() }
}
private func setupWakeObservers() {
+ // Pause the refresh loop while the machine is asleep. Without this,
+ // Task.sleep keeps a wakeup pending across the suspension and the
+ // loop tick fires the same instant the wake notifications do,
+ // producing 2-3 concurrent CLI spawns within ms of every wake.
+ NSWorkspace.shared.notificationCenter.addObserver(
+ forName: NSWorkspace.willSleepNotification,
+ object: nil,
+ queue: .main
+ ) { [weak self] _ in
+ Task { @MainActor in
+ self?.prepareRefreshPipelineForSleep()
+ }
+ }
+
+ // didWakeNotification + screensDidWakeNotification can both fire on
+ // the same wake. forceRefreshTask squashes overlap; both notifications
+ // still bypass the short manual-click rate limit so a just-before-sleep
+ // refresh cannot block wake recovery.
NSWorkspace.shared.notificationCenter.addObserver(
forName: NSWorkspace.didWakeNotification,
object: nil,
queue: .main
) { [weak self] _ in
- Task { @MainActor in self?.forceRefresh() }
+ Task { @MainActor in
+ self?.recoverRefreshPipelineAfterInterruption(resetLoading: true, reason: "wake")
+ }
}
NSWorkspace.shared.notificationCenter.addObserver(
@@ -76,7 +125,9 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
object: nil,
queue: .main
) { [weak self] _ in
- Task { @MainActor in self?.forceRefresh() }
+ Task { @MainActor in
+ self?.recoverRefreshPipelineAfterInterruption(resetLoading: true, reason: "screen wake")
+ }
}
}
@@ -86,7 +137,73 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
object: nil,
queue: .main
) { [weak self] _ in
- Task { @MainActor in self?.forceRefresh() }
+ Task { @MainActor in
+ self?.handleLaunchAgentHeartbeat()
+ }
+ }
+ }
+
+ private func handleLaunchAgentHeartbeat() {
+ let now = Date()
+ guard now.timeIntervalSince(lastLaunchAgentHeartbeatAt) >= refreshRateLimitSeconds else { return }
+ lastLaunchAgentHeartbeatAt = now
+ let loopAge = now.timeIntervalSince(refreshLoopHeartbeatAt)
+ guard refreshTimer == nil || loopAge > refreshLoopWatchdogSeconds else {
+ _ = store.clearStaleLoadingIfNeeded()
+ _ = clearStaleForceRefreshIfNeeded(now: now)
+ _ = clearStaleStatusPayloadRefreshIfNeeded(now: now)
+ return
+ }
+ if refreshTimer != nil {
+ NSLog("CodeBurn: refresh loop stale for %ds after launch agent - restarting", Int(loopAge))
+ }
+ startRefreshLoop(forceQuotaOnStart: false)
+ }
+
+ private func prepareRefreshPipelineForSleep() {
+ forceRefreshTask?.cancel()
+ forceRefreshTask = nil
+ forceRefreshStartedAt = nil
+ forceRefreshGeneration &+= 1
+ manualRefreshTask?.cancel()
+ manualRefreshTask = nil
+ manualRefreshGeneration &+= 1
+ statusPayloadRefreshTask?.cancel()
+ statusPayloadRefreshTask = nil
+ statusPayloadRefreshStartedAt = nil
+ statusPayloadRefreshGeneration &+= 1
+ store.resetLoadingState()
+ stopRefreshTimer()
+ refreshLoopHeartbeatAt = .distantPast
+ lastRefreshTime = .distantPast
+ }
+
+ private func recoverRefreshPipelineAfterInterruption(resetLoading: Bool, clearCache: Bool = false, reason: String) {
+ if resetLoading {
+ forceRefreshTask?.cancel()
+ forceRefreshTask = nil
+ forceRefreshStartedAt = nil
+ forceRefreshGeneration &+= 1
+ manualRefreshTask?.cancel()
+ manualRefreshTask = nil
+ manualRefreshGeneration &+= 1
+ statusPayloadRefreshTask?.cancel()
+ statusPayloadRefreshTask = nil
+ statusPayloadRefreshStartedAt = nil
+ statusPayloadRefreshGeneration &+= 1
+ store.resetRefreshState(clearCache: clearCache)
+ } else {
+ _ = store.clearStaleLoadingIfNeeded()
+ }
+ let now = Date()
+ let loopAge = now.timeIntervalSince(refreshLoopHeartbeatAt)
+ if refreshTimer == nil || loopAge > refreshLoopWatchdogSeconds {
+ if refreshTimer != nil {
+ NSLog("CodeBurn: refresh loop stale for %ds after %@ - restarting", Int(loopAge), reason)
+ }
+ startRefreshLoop(forceQuotaOnStart: false)
+ } else {
+ runRefreshLoopTick(reason: reason, forcePayload: true, forceQuota: false)
}
}
@@ -147,7 +264,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
guard !UserDefaults.standard.bool(forKey: key) else { return }
let appPath = Bundle.main.bundlePath
- let script = "tell application \"System Events\" to make login item at end with properties {path:\"\(appPath)\", hidden:false}"
+ let script = "tell application \"System Events\" to make login item at end with properties {path:\(appleScriptStringLiteral(appPath)), hidden:false}"
let process = Process()
process.launchPath = "/usr/bin/osascript"
@@ -166,16 +283,115 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
}
}
+ private func appleScriptStringLiteral(_ value: String) -> String {
+ var escaped = value.replacingOccurrences(of: "\\", with: "\\\\")
+ escaped = escaped.replacingOccurrences(of: "\"", with: "\\\"")
+ escaped = escaped.replacingOccurrences(of: "\r", with: "")
+ escaped = escaped.replacingOccurrences(of: "\n", with: "")
+ return "\"\(escaped)\""
+ }
+
private var lastRefreshTime: Date = .distantPast
- private func forceRefresh() {
- let now = Date()
- guard now.timeIntervalSince(lastRefreshTime) > 5 else { return }
- lastRefreshTime = now
+ @discardableResult
+ private func clearStaleForceRefreshIfNeeded(now: Date = Date()) -> Bool {
+ if forceRefreshTask != nil {
+ guard let started = forceRefreshStartedAt else {
+ NSLog("CodeBurn: force refresh task had no start timestamp - clearing")
+ forceRefreshTask?.cancel()
+ forceRefreshTask = nil
+ forceRefreshGeneration &+= 1
+ store.resetLoadingState()
+ return true
+ }
+ let elapsed = now.timeIntervalSince(started)
+ guard elapsed > forceRefreshWatchdogSeconds else { return false }
+ NSLog("CodeBurn: force refresh stuck for %ds - cancelling and restarting", Int(elapsed))
+ forceRefreshTask?.cancel()
+ forceRefreshTask = nil
+ forceRefreshStartedAt = nil
+ forceRefreshGeneration &+= 1
+ store.resetLoadingState()
+ return true
+ }
+ return false
+ }
- Task {
- await store.refresh(includeOptimize: true, force: true)
+ @discardableResult
+ private func clearStaleStatusPayloadRefreshIfNeeded(now: Date = Date()) -> Bool {
+ if statusPayloadRefreshTask != nil {
+ guard let started = statusPayloadRefreshStartedAt else {
+ NSLog("CodeBurn: today status refresh task had no start timestamp - clearing")
+ statusPayloadRefreshTask?.cancel()
+ statusPayloadRefreshTask = nil
+ statusPayloadRefreshGeneration &+= 1
+ return true
+ }
+ let elapsed = now.timeIntervalSince(started)
+ guard elapsed > statusPayloadRefreshWatchdogSeconds else { return false }
+ NSLog("CodeBurn: today status refresh stuck for %ds - cancelling", Int(elapsed))
+ statusPayloadRefreshTask?.cancel()
+ statusPayloadRefreshTask = nil
+ statusPayloadRefreshStartedAt = nil
+ statusPayloadRefreshGeneration &+= 1
+ return true
+ }
+ return false
+ }
+
+ private func refreshTodayStatusPayloadIfNeeded(reason: String, force: Bool = false) {
+ let now = Date()
+ _ = clearStaleStatusPayloadRefreshIfNeeded(now: now)
+ guard statusPayloadRefreshTask == nil else { return }
+ guard force || store.needsStatusPayloadRefresh else { return }
+
+ if let age = store.todayPayloadAgeSeconds, age > 120 {
+ NSLog("CodeBurn: today status payload stale for %ds on %@ refresh", age, reason)
+ }
+
+ statusPayloadRefreshStartedAt = now
+ statusPayloadRefreshGeneration &+= 1
+ let generation = statusPayloadRefreshGeneration
+ statusPayloadRefreshTask = Task { [weak self] in
+ guard let self else { return }
+ await self.store.refreshQuietly(period: .today, force: true)
+ self.refreshStatusButton()
+ guard self.statusPayloadRefreshGeneration == generation, !Task.isCancelled else { return }
+ self.statusPayloadRefreshTask = nil
+ self.statusPayloadRefreshStartedAt = nil
+ }
+ }
+
+ private func forceRefresh(bypassRateLimit: Bool = false, forceQuota: Bool = false) {
+ let now = Date()
+ _ = clearStaleForceRefreshIfNeeded(now: now)
+ if forceRefreshTask != nil {
+ refreshTodayStatusPayloadIfNeeded(reason: "blocked force refresh")
+ }
+ guard forceRefreshTask == nil else { return }
+ if !bypassRateLimit {
+ guard now.timeIntervalSince(lastRefreshTime) > refreshRateLimitSeconds else { return }
+ }
+ lastRefreshTime = now
+ forceRefreshStartedAt = now
+ forceRefreshGeneration &+= 1
+ let generation = forceRefreshGeneration
+
+ forceRefreshTask = Task {
+ async let main: Void = store.refresh(includeOptimize: false, force: true, showLoading: true)
+ async let quotas: Bool = refreshLiveQuotaProgressIfDue(force: forceQuota)
+ if store.selectedPeriod != .today || store.selectedProvider != .all {
+ await store.refreshQuietly(period: .today)
+ }
+ _ = await main
refreshStatusButton()
+ await MainActor.run { [weak self] in
+ guard let self, self.forceRefreshGeneration == generation else { return }
+ self.forceRefreshTask = nil
+ self.forceRefreshStartedAt = nil
+ self.lastRefreshTime = Date()
+ }
+ _ = await quotas
}
}
@@ -201,21 +417,229 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
}
}
- private func startRefreshLoop() {
- Task {
- await store.refresh(includeOptimize: true)
- refreshStatusButton()
+ fileprivate var lastSubscriptionRefreshAt: Date?
+ fileprivate var lastCodexRefreshAt: Date?
+
+ @discardableResult
+ private func refreshLiveQuotaProgressIfDue(force: Bool = false) async -> Bool {
+ let cadence = SubscriptionRefreshCadence.current
+ if !force && cadence == .manual { return false }
+
+ let now = Date()
+ let threshold = force ? 0 : TimeInterval(cadence.rawValue)
+ let shouldRefreshClaude = force || now.timeIntervalSince(lastSubscriptionRefreshAt ?? .distantPast) >= threshold
+ let shouldRefreshCodex = force || now.timeIntervalSince(lastCodexRefreshAt ?? .distantPast) >= threshold
+ guard shouldRefreshClaude || shouldRefreshCodex else { return false }
+
+ switch (shouldRefreshClaude, shouldRefreshCodex) {
+ case (true, true):
+ async let claude = refreshClaudeQuotaSingleFlight()
+ async let codex = refreshCodexQuotaSingleFlight()
+ if await claude { lastSubscriptionRefreshAt = Date() }
+ if await codex { lastCodexRefreshAt = Date() }
+ case (true, false):
+ if await refreshClaudeQuotaSingleFlight() {
+ lastSubscriptionRefreshAt = Date()
+ }
+ case (false, true):
+ if await refreshCodexQuotaSingleFlight() {
+ lastCodexRefreshAt = Date()
+ }
+ case (false, false):
+ break
+ }
+ return true
+ }
+
+ private func refreshClaudeQuotaSingleFlight() async -> Bool {
+ if let task = claudeQuotaRefreshTask {
+ return await task.value
+ }
+ let task = Task { [store] in
+ await store.refreshSubscriptionReportingSuccess()
+ }
+ claudeQuotaRefreshTask = task
+ let result = await task.value
+ if claudeQuotaRefreshTask != nil {
+ claudeQuotaRefreshTask = nil
+ }
+ return result
+ }
+
+ private func refreshCodexQuotaSingleFlight() async -> Bool {
+ if let task = codexQuotaRefreshTask {
+ return await task.value
+ }
+ let task = Task { [store] in
+ await store.refreshCodexReportingSuccess()
+ }
+ codexQuotaRefreshTask = task
+ let result = await task.value
+ if codexQuotaRefreshTask != nil {
+ codexQuotaRefreshTask = nil
+ }
+ return result
+ }
+
+ private func refreshLiveQuotaProgressForPopoverOpen() {
+ let now = Date()
+ let claudeElapsed = now.timeIntervalSince(lastSubscriptionRefreshAt ?? .distantPast)
+ let codexElapsed = now.timeIntervalSince(lastCodexRefreshAt ?? .distantPast)
+ guard claudeElapsed >= interactiveQuotaRefreshFloorSeconds ||
+ codexElapsed >= interactiveQuotaRefreshFloorSeconds else { return }
+
+ Task { [weak self] in
+ guard let self else { return }
+ _ = await self.refreshLiveQuotaProgressIfDue(force: true)
}
}
+ private func refreshPayloadForPopoverOpen() {
+ guard store.needsInteractivePayloadRefresh else { return }
+ let shouldResetPipeline = store.shouldResetInteractiveRefreshPipeline
+ if shouldResetPipeline, let age = store.staleInteractivePayloadAgeSeconds {
+ NSLog("CodeBurn: popover opened with %ds stale payload cache - resetting refresh pipeline", age)
+ }
+ recoverRefreshPipelineAfterInterruption(
+ resetLoading: shouldResetPipeline,
+ reason: "popover open"
+ )
+ }
+
+ private func stopRefreshTimer() {
+ refreshTimer?.setEventHandler {}
+ refreshTimer?.cancel()
+ refreshTimer = nil
+ }
+
+ private func runRefreshLoopTick(reason: String, forcePayload: Bool = false, forceQuota: Bool = false) {
+ refreshLoopHeartbeatAt = Date()
+ let hadForceRefreshInFlight = forceRefreshTask != nil
+ let clearedStaleForceRefresh = clearStaleForceRefreshIfNeeded()
+ let clearedStaleStatusRefresh = clearStaleStatusPayloadRefreshIfNeeded()
+ let clearedStaleLoading = store.clearStaleLoadingIfNeeded()
+ let statusPayloadStale = store.needsStatusPayloadRefresh
+ let sinceLast = Date().timeIntervalSince(lastRefreshTime)
+ let shouldForceRefresh = forcePayload ||
+ clearedStaleForceRefresh ||
+ clearedStaleLoading ||
+ sinceLast >= TimeInterval(refreshIntervalSeconds)
+
+ if shouldForceRefresh {
+ forceRefresh(bypassRateLimit: true, forceQuota: forceQuota)
+ }
+
+ let forceRefreshWasBlocked = hadForceRefreshInFlight && forceRefreshTask != nil
+ if statusPayloadStale && (!shouldForceRefresh || forceRefreshWasBlocked || clearedStaleStatusRefresh) {
+ refreshTodayStatusPayloadIfNeeded(reason: reason, force: forcePayload)
+ }
+ }
+
+ private func startRefreshLoop(forceQuotaOnStart: Bool = false) {
+ stopRefreshTimer()
+ runRefreshLoopTick(reason: "start", forcePayload: true, forceQuota: forceQuotaOnStart)
+
+ let timer = DispatchSource.makeTimerSource(queue: .main)
+ timer.schedule(
+ deadline: .now() + .seconds(Int(refreshIntervalSeconds)),
+ repeating: .seconds(Int(refreshIntervalSeconds)),
+ leeway: .seconds(2)
+ )
+ timer.setEventHandler { [weak self] in
+ Task { @MainActor [weak self] in
+ self?.runRefreshLoopTick(reason: "timer")
+ }
+ }
+ refreshTimer = timer
+ refreshLoopHeartbeatAt = Date()
+ timer.resume()
+ }
+
+ @MainActor
+ func refreshSubscriptionNow() {
+ manualRefreshTask?.cancel()
+ manualRefreshGeneration &+= 1
+ let generation = manualRefreshGeneration
+ forceRefreshTask?.cancel()
+ forceRefreshTask = nil
+ forceRefreshStartedAt = nil
+ forceRefreshGeneration &+= 1
+ statusPayloadRefreshTask?.cancel()
+ statusPayloadRefreshTask = nil
+ statusPayloadRefreshStartedAt = nil
+ statusPayloadRefreshGeneration &+= 1
+ pendingRefreshWork?.cancel()
+ pendingRefreshWork = nil
+ stopRefreshTimer()
+ store.resetRefreshState(clearCache: true)
+ lastRefreshTime = .distantPast
+ refreshStatusButton()
+
+ manualRefreshTask = Task { [weak self] in
+ guard let self else { return }
+ // "Refresh Now" should refresh the menubar payload AND every
+ // connected provider's live quota. The user's intent is "make
+ // this match reality right now."
+ let needsTodayTotal = self.store.selectedPeriod != .today || self.store.selectedProvider != .all
+ async let payload: Void = self.store.refresh(includeOptimize: false, force: true, showLoading: true)
+ async let quotas: Bool = self.refreshLiveQuotaProgressIfDue(force: true)
+ if needsTodayTotal {
+ await self.store.refreshQuietly(period: .today, force: true)
+ }
+ _ = await payload
+ guard self.manualRefreshGeneration == generation, !Task.isCancelled else { return }
+ self.lastRefreshTime = Date()
+ self.refreshStatusButton()
+ _ = await quotas
+ guard self.manualRefreshGeneration == generation, !Task.isCancelled else { return }
+ self.manualRefreshTask = nil
+ if self.refreshTimer == nil {
+ self.startRefreshLoop()
+ }
+ }
+ }
+
+ /// Reset the cadence anchor so the next loop tick re-evaluates from "now"
+ /// rather than measuring against a timestamp from the previous connection.
+ /// Triggered on disconnect of any provider — the cost of clearing both
+ /// anchors is one extra refresh tick on the unaffected provider, far less
+ /// disruptive than waiting a full cadence after a reconnect.
+ @MainActor
+ func resetSubscriptionCadenceAnchor() {
+ lastSubscriptionRefreshAt = nil
+ lastCodexRefreshAt = nil
+ }
+
private func observeStore() {
- withObservationTracking {
- _ = store.payload
- _ = store.todayPayload
+ // Read closure uses [weak self] so the implicit self capture from
+ // accessing store.* doesn't pin self for the lifetime of an
+ // unfired observation. withObservationTracking is one-shot per
+ // call: once any read property changes, onChange fires and the
+ // registration is consumed, then we re-arm. There is at most one
+ // active subscription at a time.
+ withObservationTracking { [weak self] in
+ guard let self else { return }
+ _ = self.store.payload
+ _ = self.store.todayPayload
+ // Track currency so the menubar title catches up immediately on
+ // currency switch instead of waiting for the next 30s payload tick.
+ _ = self.store.currency
+ // Track the live-quota state too so the flame icon re-tints on
+ // every subscription / codex usage update, not just every 30s.
+ _ = self.store.subscription
+ _ = self.store.subscriptionLoadState
+ _ = self.store.codexUsage
+ _ = self.store.codexLoadState
} onChange: { [weak self] in
- Task { @MainActor in
- self?.refreshStatusButton()
- self?.observeStore()
+ DispatchQueue.main.async {
+ guard let self else { return }
+ self.pendingRefreshWork?.cancel()
+ let work = DispatchWorkItem { [weak self] in
+ self?.refreshStatusButton()
+ self?.observeStore()
+ }
+ self.pendingRefreshWork = work
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.05, execute: work)
}
}
}
@@ -255,18 +679,46 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
/// stubborn gap between icon and text on some macOS releases (the icon hugs the left edge
/// of the status item, the title starts at its own baseline), so we inline both so they
/// flow as one typographic unit with a single, controllable gap.
+ private static func flameTint(for severity: QuotaSummary.Severity) -> NSColor? {
+ switch severity {
+ case .normal: return nil // template, auto-adapt
+ case .warning: return NSColor.systemYellow // 70-90%
+ case .critical: return NSColor.systemOrange // 90-100%
+ case .danger: return NSColor.systemRed // 100%+
+ }
+ }
+
private func refreshStatusButton() {
guard let button = statusItem.button else { return }
+ // Skip while the popover is anchored to this button. Rewriting the
+ // attributedTitle changes the button's intrinsic width, which makes
+ // macOS reflow the status item in the menubar and detaches the
+ // anchored popover (it pops to a stale default position). The
+ // popoverDidClose delegate calls back through here once the popover
+ // is dismissed so the menubar cost catches up immediately on close.
+ if popover != nil && popover.isShown { return }
// Clear any previously-set image so the attachment is the only glyph rendered.
button.image = nil
button.imagePosition = .noImage
let font = NSFont.monospacedDigitSystemFont(ofSize: menubarTitleFontSize, weight: .medium)
- let flameConfig = NSImage.SymbolConfiguration(pointSize: menubarTitleFontSize, weight: .medium)
+ let baseConfig = NSImage.SymbolConfiguration(pointSize: menubarTitleFontSize, weight: .medium)
+ // Tint the flame based on the worst-affected connected provider's quota.
+ // Normal (<70%) keeps the template (auto white-on-dark / black-on-light);
+ // warning/critical/danger override with a fixed palette color so the
+ // user gets a glanceable signal even when the menu bar is busy.
+ let aggregate = store.aggregateQuotaStatus
+ let tint = Self.flameTint(for: aggregate.severity)
+ let flameConfig: NSImage.SymbolConfiguration
+ if let tint {
+ flameConfig = baseConfig.applying(.init(paletteColors: [tint]))
+ } else {
+ flameConfig = baseConfig
+ }
let flame = NSImage(systemSymbolName: "flame.fill", accessibilityDescription: "CodeBurn")?
.withSymbolConfiguration(flameConfig)
- flame?.isTemplate = true
+ flame?.isTemplate = (tint == nil)
let attachment = NSTextAttachment()
attachment.image = flame
@@ -322,14 +774,42 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
if popover.isShown {
popover.performClose(sender)
} else {
- NSApp.activate(ignoringOtherApps: true)
+ // Do NOT call NSApp.activate(ignoringOtherApps:) here. On macOS
+ // Tahoe an accessory app activating while a popover anchors to
+ // its NSStatusItem can race with the system menu bar's auto-hide
+ // logic and leave the user's apple-menu hidden until the popover
+ // closes. The popover's window takes keyboard focus on its own
+ // via makeKeyAndOrderFront, which is enough for keystrokes to
+ // reach the SwiftUI content.
popover.show(relativeTo: button.bounds, of: button, preferredEdge: .minY)
- popover.contentViewController?.view.window?.makeKey()
+ if let window = popover.contentViewController?.view.window {
+ // Pin the popover's window above the status-bar layer but tag
+ // it as auxiliary so macOS Tahoe does not treat it as an
+ // app-level focus event — that's what was hiding the system
+ // menu bar (Terminal's apple-logo / Shell / Edit / View row)
+ // every time the popover opened.
+ window.level = .statusBar
+ window.collectionBehavior.insert(.fullScreenAuxiliary)
+ window.collectionBehavior.insert(.canJoinAllSpaces)
+ window.makeKeyAndOrderFront(nil)
+ }
+ refreshPayloadForPopoverOpen()
+ refreshLiveQuotaProgressForPopoverOpen()
}
}
private func showContextMenu(from button: NSStatusBarButton) {
let menu = NSMenu()
+
+ let settingsItem = NSMenuItem(title: "Settings…", action: #selector(openSettings), keyEquivalent: ",")
+ settingsItem.target = self
+ menu.addItem(settingsItem)
+
+ let refreshNow = NSMenuItem(title: "Refresh Now", action: #selector(refreshNowAction), keyEquivalent: "r")
+ refreshNow.target = self
+ menu.addItem(refreshNow)
+
+ menu.addItem(.separator())
let updateItem = NSMenuItem(title: "Check for Updates", action: #selector(checkForUpdates), keyEquivalent: "")
updateItem.target = self
menu.addItem(updateItem)
@@ -337,11 +817,48 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
let quitItem = NSMenuItem(title: "Quit CodeBurn", action: #selector(quitApp), keyEquivalent: "q")
quitItem.target = self
menu.addItem(quitItem)
+
statusItem.menu = menu
button.performClick(nil)
statusItem.menu = nil
}
+ private var settingsWindowController: NSWindowController?
+
+ @objc private func openSettings() {
+ // Accessory-policy apps (no Dock icon, no main menu) don't get the
+ // SwiftUI Settings scene wired into the responder chain reliably, so
+ // the standard `showSettingsWindow:` selector silently no-ops. We host
+ // the SwiftUI view in our own NSWindowController instead.
+ if let controller = settingsWindowController {
+ NSApp.activate(ignoringOtherApps: true)
+ controller.window?.makeKeyAndOrderFront(nil)
+ return
+ }
+
+ let hosting = NSHostingController(
+ rootView: SettingsView().environment(store)
+ )
+ let window = NSWindow(
+ contentRect: NSRect(x: 0, y: 0, width: 520, height: 380),
+ styleMask: [.titled, .closable, .miniaturizable],
+ backing: .buffered,
+ defer: false
+ )
+ window.title = "CodeBurn Settings"
+ window.contentViewController = hosting
+ window.center()
+ window.isReleasedWhenClosed = false
+ let controller = NSWindowController(window: window)
+ settingsWindowController = controller
+ NSApp.activate(ignoringOtherApps: true)
+ controller.showWindow(nil)
+ }
+
+ @objc private func refreshNowAction() {
+ refreshSubscriptionNow()
+ }
+
private func codeburnAlertIcon() -> NSImage? {
let config = NSImage.SymbolConfiguration(pointSize: 32, weight: .medium)
guard let symbol = NSImage(systemSymbolName: "flame.fill", accessibilityDescription: "CodeBurn")?
@@ -363,14 +880,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
await updateChecker.check()
let alert = NSAlert()
alert.icon = codeburnAlertIcon()
- if updateChecker.updateAvailable, let latest = updateChecker.latestVersion {
+ if let error = updateChecker.updateError {
+ alert.messageText = "Update Check Failed"
+ alert.informativeText = error
+ alert.alertStyle = .warning
+ } else if updateChecker.updateAvailable, let latest = updateChecker.latestVersion {
alert.messageText = "Update Available"
- alert.informativeText = "v\(latest) is available (you have v\(updateChecker.currentVersion)). Run:\n\ncodeburn menubar --force"
+ alert.informativeText = "\(AppVersion.display(latest)) is available (you have \(AppVersion.display(updateChecker.currentVersion))). Run:\n\ncodeburn menubar --force"
+ alert.alertStyle = .informational
} else {
alert.messageText = "Up to Date"
- alert.informativeText = "You're on the latest version (v\(updateChecker.currentVersion))."
+ alert.informativeText = "You're on the latest version (\(AppVersion.display(updateChecker.currentVersion)))."
+ alert.alertStyle = .informational
}
- alert.alertStyle = .informational
alert.addButton(withTitle: "OK")
alert.runModal()
}
@@ -385,4 +907,10 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate {
func popoverShouldDetach(_ popover: NSPopover) -> Bool {
false
}
+
+ func popoverDidClose(_ notification: Notification) {
+ // Catch up on any menubar title updates that were skipped while the
+ // popover was anchored.
+ refreshStatusButton()
+ }
}
diff --git a/mac/Sources/CodeBurnMenubar/CurrencyState.swift b/mac/Sources/CodeBurnMenubar/CurrencyState.swift
index e668139..c27acc1 100644
--- a/mac/Sources/CodeBurnMenubar/CurrencyState.swift
+++ b/mac/Sources/CodeBurnMenubar/CurrencyState.swift
@@ -10,8 +10,8 @@ private let minValidFXRate: Double = 0.0001
private let maxValidFXRate: Double = 1_000_000
private let fxFetchTimeoutSeconds: TimeInterval = 10
-@Observable
-final class CurrencyState: @unchecked Sendable {
+@MainActor @Observable
+final class CurrencyState: Sendable {
static let shared = CurrencyState()
var code: String = "USD"
@@ -31,7 +31,7 @@ final class CurrencyState: @unchecked Sendable {
}
}
- static func symbolForCode(_ code: String) -> String {
+ nonisolated static func symbolForCode(_ code: String) -> String {
// Some locales return "US$" for USD or "CA$" for CAD via NumberFormatter. Prefer the
// plain glyph form everyone recognises.
if let override = symbolOverrides[code] { return override }
@@ -42,7 +42,7 @@ final class CurrencyState: @unchecked Sendable {
return formatter.currencySymbol ?? code
}
- private static let symbolOverrides: [String: String] = [
+ nonisolated private static let symbolOverrides: [String: String] = [
"USD": "$",
"CAD": "$",
"AUD": "$",
diff --git a/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift b/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift
new file mode 100644
index 0000000..e47db7b
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift
@@ -0,0 +1,440 @@
+import Foundation
+import Security
+
+/// Owns the lifecycle of Claude OAuth credentials end-to-end. Replaces
+/// SubscriptionClient + SubscriptionRefreshGate with a model that mirrors
+/// CodexBar's proven pattern:
+///
+/// 1. **Bootstrap is user-initiated.** The first read of Claude's keychain
+/// entry — which triggers a macOS keychain prompt — only happens when
+/// the user clicks "Connect" in the Plan tab. The menubar does not
+/// touch Claude's keychain on launch.
+///
+/// 2. **We persist refreshed tokens.** When Anthropic returns a new access
+/// token (or a rotated refresh token) we write it back to our own keychain
+/// item. The next fetch uses it directly — one API call per cycle, not
+/// three. This was the root cause of "connect once, never updates": the
+/// previous code refreshed on every tick because the new token was
+/// thrown away.
+///
+/// 3. **Our own keychain item, not Claude's.** We bootstrap from Claude's
+/// entry once, then maintain `com.codeburn.menubar.claude.oauth.v1` in
+/// the user's keychain. Subsequent reads do not prompt because we own
+/// that item's ACL.
+///
+/// 4. **In-memory cache (5 min)** so back-to-back reads in the same refresh
+/// cycle don't even hit the keychain.
+enum ClaudeCredentialStore {
+ private static let bootstrapCompletedKey = "codeburn.claude.bootstrapCompleted"
+ private static let inMemoryTTL: TimeInterval = 5 * 60
+ private static let proactiveRefreshMargin: TimeInterval = 5 * 60
+
+ private static let oauthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
+ private static let refreshURL = URL(string: "https://platform.claude.com/v1/oauth/token")!
+
+ private static let claudeKeychainService = "Claude Code-credentials"
+ private static let credentialsRelativePath = ".claude/.credentials.json"
+ private static let maxCredentialBytes = 64 * 1024
+
+ /// Legacy local cache file. New writes use the macOS Keychain; this path is
+ /// read once for migration and then removed.
+ private static let cacheFilename = "claude-credentials.v1.json"
+ private static let ourKeychainService = "org.agentseal.codeburn.menubar.claude.oauth.v1"
+ private static let ourKeychainAccount = "default"
+
+ private static let lock = NSLock()
+ private nonisolated(unsafe) static var memoryCache: CachedRecord?
+
+ struct CachedRecord {
+ let record: CredentialRecord
+ let cachedAt: Date
+
+ var isFresh: Bool { Date().timeIntervalSince(cachedAt) < ClaudeCredentialStore.inMemoryTTL }
+ }
+
+ struct CredentialRecord: Codable, Equatable {
+ let accessToken: String
+ let refreshToken: String?
+ let expiresAt: Date?
+ let rateLimitTier: String?
+ }
+
+ enum StoreError: Error, LocalizedError {
+ case bootstrapNoSource // neither file nor Claude keychain has credentials
+ case bootstrapDecodeFailed
+ case keychainWriteFailed(OSStatus)
+ case keychainReadFailed(OSStatus)
+ case refreshHTTPError(Int, String?)
+ case refreshNetworkError(Error)
+ case refreshDecodeFailed
+ case noRefreshToken
+
+ var errorDescription: String? {
+ switch self {
+ case .bootstrapNoSource:
+ return "No Claude credentials found. Sign in with `claude` first."
+ case .bootstrapDecodeFailed:
+ return "Claude credentials are malformed."
+ case let .keychainWriteFailed(status):
+ return "Could not write to keychain (status \(status))."
+ case let .keychainReadFailed(status):
+ return "Could not read from keychain (status \(status))."
+ case let .refreshHTTPError(code, body):
+ return "Token refresh failed (HTTP \(code))\(body.map { ": \($0)" } ?? "")"
+ case let .refreshNetworkError(err):
+ return "Token refresh network error: \(err.localizedDescription)"
+ case .refreshDecodeFailed:
+ return "Token refresh response was malformed."
+ case .noRefreshToken:
+ return "No refresh token available; reconnect required."
+ }
+ }
+
+ /// True when the failure means the user must re-authenticate (re-run
+ /// `claude` or click Reconnect). Used by the UI to distinguish between
+ /// "try again later" and "you must act".
+ var isTerminal: Bool {
+ if case let .refreshHTTPError(code, body) = self, code >= 400, code < 500 {
+ let lower = body?.lowercased() ?? ""
+ if lower.contains("invalid_grant") || lower.contains("invalid_client") || lower.contains("invalid_token") {
+ return true
+ }
+ return true // 4xx other than rate-limiting is terminal too
+ }
+ if case .noRefreshToken = self { return true }
+ return false
+ }
+ }
+
+ // MARK: - Bootstrap state
+
+ /// True once the user has explicitly connected (clicked Connect in the Plan
+ /// tab AND we successfully read their credentials). Persists across launches.
+ static var isBootstrapCompleted: Bool {
+ get { UserDefaults.standard.bool(forKey: bootstrapCompletedKey) }
+ set { UserDefaults.standard.set(newValue, forKey: bootstrapCompletedKey) }
+ }
+
+ /// Reset bootstrap state. Used when the user explicitly wants to disconnect
+ /// or when the refresh token has been revoked terminally.
+ static func resetBootstrap() {
+ lock.withLock { memoryCache = nil }
+ deleteOurCache()
+ isBootstrapCompleted = false
+ }
+
+ // MARK: - Public API
+
+ /// User-initiated entry point. Reads from Claude's source (PROMPTS for the
+ /// keychain on first use), writes to our own keychain item, marks bootstrap
+ /// as completed.
+ @discardableResult
+ static func bootstrap() throws -> CredentialRecord {
+ let record = try readClaudeSource()
+ try writeOurCache(record: record)
+ isBootstrapCompleted = true
+ cacheInMemory(record)
+ return record
+ }
+
+ /// Silent read for background refresh cycles. Reads only from our cache /
+ /// keychain item — never prompts. Returns nil if not bootstrapped.
+ static func currentRecord() throws -> CredentialRecord? {
+ guard isBootstrapCompleted else { return nil }
+ // Honour the in-memory TTL: a stale cached record can mask a token
+ // that another process (e.g. claude /login again) has just rotated
+ // on disk. Re-read the file when the cache passes the TTL.
+ if let cached = lock.withLock({ memoryCache }), cached.isFresh {
+ return cached.record
+ }
+ if let stored = try readOurCache() {
+ cacheInMemory(stored)
+ return stored
+ }
+ // Bootstrap flag is set but our cache file is missing — most likely
+ // a fresh install resetting state, or the user manually deleted the
+ // file. Force re-bootstrap on next user action.
+ isBootstrapCompleted = false
+ return nil
+ }
+
+ /// Returns a token guaranteed to be either fresh or just-refreshed. If the
+ /// current token expires within `proactiveRefreshMargin`, refreshes ahead
+ /// of time and persists the new token.
+ static func freshAccessToken() async throws -> String? {
+ guard let record = try currentRecord() else { return nil }
+ if let expiresAt = record.expiresAt, expiresAt.timeIntervalSinceNow < proactiveRefreshMargin {
+ let updated = try await refreshAndPersist(record: record)
+ return updated.accessToken
+ }
+ return record.accessToken
+ }
+
+ /// Called after an explicit 401. Refreshes, persists, returns the new token.
+ static func refreshAfter401() async throws -> String {
+ guard let record = try currentRecord() else { throw StoreError.noRefreshToken }
+ let updated = try await refreshAndPersist(record: record)
+ return updated.accessToken
+ }
+
+ static func subscriptionTier() throws -> String? {
+ try currentRecord()?.rateLimitTier
+ }
+
+ // MARK: - Bootstrap source
+
+ private static func readClaudeSource() throws -> CredentialRecord {
+ if let fromFile = try? readClaudeFile() { return fromFile }
+ if let fromKeychain = try readClaudeKeychain() { return fromKeychain }
+ throw StoreError.bootstrapNoSource
+ }
+
+ private static func readClaudeFile() throws -> CredentialRecord? {
+ let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(credentialsRelativePath)
+ guard FileManager.default.fileExists(atPath: url.path) else { return nil }
+ let data = try SafeFile.read(from: url.path, maxBytes: maxCredentialBytes)
+ return try parseClaudeBlob(data: sanitizeClaudeBlob(data))
+ }
+
+ /// Reads Claude's keychain credentials. The CLI has historically written
+ /// entries under different account names — older versions used "agentseal"
+ /// (a hardcoded company-style identifier) while Claude Code 2.1.x writes
+ /// under `$USER` (NSUserName()). After a user re-runs `/login`, both
+ /// entries can coexist and `SecItemCopyMatching` with kSecMatchLimitOne
+ /// often returns the older stale one. We try the user-keyed entry first
+ /// (the modern format), then fall back to the unscoped query for older
+ /// installations.
+ private static func readClaudeKeychain() throws -> CredentialRecord? {
+ if let record = try readClaudeKeychain(account: NSUserName()) {
+ return record
+ }
+ return try readClaudeKeychain(account: nil)
+ }
+
+ private static func readClaudeKeychain(account: String?) throws -> CredentialRecord? {
+ var query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: claudeKeychainService,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: true,
+ ]
+ if let account { query[kSecAttrAccount as String] = account }
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+ if status == errSecItemNotFound { return nil }
+ guard status == errSecSuccess, let data = result as? Data else {
+ throw StoreError.keychainReadFailed(status)
+ }
+ return try parseClaudeBlob(data: sanitizeClaudeBlob(data))
+ }
+
+ /// Claude Code's keychain writer line-wraps long values (newline + leading
+ /// spaces) mid-token, producing JSON with literal control chars inside string
+ /// values. Strip those plus pretty-print indentation between fields so the
+ /// JSON parser succeeds.
+ private static func sanitizeClaudeBlob(_ data: Data) -> Data {
+ guard var s = String(data: data, encoding: .utf8) else { return data }
+ s = s.replacingOccurrences(of: "\r", with: "")
+ if let regex = try? NSRegularExpression(pattern: "\\n[ \\t]*", options: []) {
+ let range = NSRange(s.startIndex.. CredentialRecord {
+ struct Root: Decodable { let claudeAiOauth: OAuth? }
+ struct OAuth: Decodable {
+ let accessToken: String?
+ let refreshToken: String?
+ let expiresAt: Double?
+ let rateLimitTier: String?
+ }
+ do {
+ let root = try JSONDecoder().decode(Root.self, from: data)
+ guard let oauth = root.claudeAiOauth,
+ let token = oauth.accessToken?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !token.isEmpty
+ else { throw StoreError.bootstrapDecodeFailed }
+ return CredentialRecord(
+ accessToken: token,
+ refreshToken: oauth.refreshToken,
+ expiresAt: oauth.expiresAt.map { Date(timeIntervalSince1970: $0 / 1000.0) },
+ rateLimitTier: oauth.rateLimitTier
+ )
+ } catch {
+ throw StoreError.bootstrapDecodeFailed
+ }
+ }
+
+ // MARK: - Local cache file (no keychain involvement)
+
+ private static func cacheFileURL() -> URL {
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
+ ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support")
+ return support
+ .appendingPathComponent("CodeBurn", isDirectory: true)
+ .appendingPathComponent(cacheFilename)
+ }
+
+ private static func readOurCache() throws -> CredentialRecord? {
+ if let record = try readOurKeychainCache() {
+ return record
+ }
+
+ let url = cacheFileURL()
+ guard FileManager.default.fileExists(atPath: url.path) else { return nil }
+ // Route through SafeFile.read so we lstat for symlinks before opening
+ // and bound the read with maxCredentialBytes. Without this, an
+ // attacker who can plant a symlink in ~/Library/Application Support/
+ // CodeBurn/ between disconnect and reconnect could redirect our read
+ // to /dev/zero (unbounded memory) or another file the user owns.
+ let data = try SafeFile.read(from: url.path, maxBytes: maxCredentialBytes)
+ guard let record = try? JSONDecoder().decode(CredentialRecord.self, from: data) else { return nil }
+ try? writeOurKeychainCache(record: record)
+ try? FileManager.default.removeItem(at: url)
+ return record
+ }
+
+ private static func writeOurCache(record: CredentialRecord) throws {
+ try writeOurKeychainCache(record: record)
+ }
+
+ private static func readOurKeychainCache() throws -> CredentialRecord? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: true,
+ ]
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+ if status == errSecItemNotFound { return nil }
+ guard status == errSecSuccess, let data = result as? Data else {
+ throw StoreError.keychainReadFailed(status)
+ }
+ return try? JSONDecoder().decode(CredentialRecord.self, from: data)
+ }
+
+ private static func writeOurKeychainCache(record: CredentialRecord) throws {
+ let url = cacheFileURL()
+ let data = try JSONEncoder().encode(record)
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ ]
+ let attributes: [String: Any] = [
+ kSecValueData as String: data,
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
+ ]
+ let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
+ if status == errSecItemNotFound {
+ var add = query
+ add.merge(attributes) { _, new in new }
+ let addStatus = SecItemAdd(add as CFDictionary, nil)
+ guard addStatus == errSecSuccess else {
+ throw StoreError.keychainWriteFailed(addStatus)
+ }
+ } else if status != errSecSuccess {
+ throw StoreError.keychainWriteFailed(status)
+ }
+ try? FileManager.default.removeItem(at: url)
+ }
+
+ private static func deleteOurCache() {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ ]
+ SecItemDelete(query as CFDictionary)
+ try? FileManager.default.removeItem(at: cacheFileURL())
+ }
+
+ private static func cacheInMemory(_ record: CredentialRecord) {
+ lock.withLock { memoryCache = CachedRecord(record: record, cachedAt: Date()) }
+ }
+
+ // MARK: - Refresh
+
+ private static func refreshAndPersist(record: CredentialRecord) async throws -> CredentialRecord {
+ guard let refreshToken = record.refreshToken, !refreshToken.isEmpty else {
+ throw StoreError.noRefreshToken
+ }
+
+ var request = URLRequest(url: refreshURL)
+ request.httpMethod = "POST"
+ request.timeoutInterval = 30
+ request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ var components = URLComponents()
+ components.queryItems = [
+ URLQueryItem(name: "grant_type", value: "refresh_token"),
+ URLQueryItem(name: "refresh_token", value: refreshToken),
+ URLQueryItem(name: "client_id", value: oauthClientID),
+ ]
+ request.httpBody = (components.percentEncodedQuery ?? "").data(using: .utf8)
+
+ let data: Data
+ let response: URLResponse
+ do {
+ (data, response) = try await URLSession.shared.data(for: request)
+ } catch {
+ throw StoreError.refreshNetworkError(error)
+ }
+ guard let http = response as? HTTPURLResponse else {
+ throw StoreError.refreshHTTPError(-1, nil)
+ }
+ guard http.statusCode == 200 else {
+ let body = String(data: data, encoding: .utf8)
+ throw StoreError.refreshHTTPError(http.statusCode, body)
+ }
+
+ struct RefreshResponse: Decodable {
+ let accessToken: String
+ let refreshToken: String?
+ let expiresIn: Int?
+ enum CodingKeys: String, CodingKey {
+ case accessToken = "access_token"
+ case refreshToken = "refresh_token"
+ case expiresIn = "expires_in"
+ }
+ }
+ guard let decoded = try? JSONDecoder().decode(RefreshResponse.self, from: data) else {
+ throw StoreError.refreshDecodeFailed
+ }
+
+ // Anthropic may rotate the refresh token. If it did, the OLD one is
+ // already invalid server-side — discarding the new one would lock
+ // the user out permanently. So we cache the new record in memory
+ // BEFORE attempting the keychain write, and if the write fails we
+ // still return the new record (memory cache will serve subsequent
+ // calls inside the 5-min TTL while we keep retrying the persist).
+ let updated = CredentialRecord(
+ accessToken: decoded.accessToken,
+ refreshToken: decoded.refreshToken ?? record.refreshToken,
+ expiresAt: decoded.expiresIn.map { Date().addingTimeInterval(TimeInterval($0)) } ?? record.expiresAt,
+ rateLimitTier: record.rateLimitTier
+ )
+ cacheInMemory(updated)
+ do {
+ try writeOurCache(record: updated)
+ } catch {
+ // Best effort — surface to logs but do not abandon the rotated
+ // token. Next refresh will retry persistence; UI will continue
+ // working from the in-memory cache.
+ NSLog("CodeBurn: cache write failed during refresh rotation: %@", String(describing: error))
+ }
+ return updated
+ }
+}
+
+private extension NSLock {
+ func withLock(_ body: () throws -> T) rethrows -> T {
+ lock(); defer { unlock() }
+ return try body()
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/ClaudeSubscriptionService.swift b/mac/Sources/CodeBurnMenubar/Data/ClaudeSubscriptionService.swift
new file mode 100644
index 0000000..f97641d
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/ClaudeSubscriptionService.swift
@@ -0,0 +1,241 @@
+import Foundation
+
+/// Orchestrates "given a credential record, fetch live quota from Anthropic
+/// and surface a result the UI can render". All token persistence lives in
+/// `ClaudeCredentialStore`; the only state this service holds is the
+/// 429 backoff window for the usage endpoint.
+enum ClaudeSubscriptionService {
+ private static let usageURL = URL(string: "https://api.anthropic.com/api/oauth/usage")!
+ private static let betaHeader = "oauth-2025-04-20"
+ private static let userAgent = "claude-code/2.1.0"
+ private static let usageBlockedUntilKey = "codeburn.claude.usage.blockedUntil"
+
+ enum FetchError: Error, LocalizedError {
+ case notBootstrapped
+ case bootstrapFailed(ClaudeCredentialStore.StoreError)
+ case rateLimited(retryAt: Date)
+ case usageHTTPError(Int, String?)
+ case usageDecodeFailed
+ case network(Error)
+ case credential(ClaudeCredentialStore.StoreError)
+
+ var errorDescription: String? {
+ switch self {
+ case .notBootstrapped:
+ return "Connect Claude in the Plan tab to start tracking quota."
+ case let .bootstrapFailed(err):
+ return err.errorDescription
+ case let .rateLimited(retryAt):
+ let f = RelativeDateTimeFormatter()
+ f.unitsStyle = .short
+ return "Anthropic rate-limited the quota endpoint. Retrying \(f.localizedString(for: retryAt, relativeTo: Date()))."
+ case let .usageHTTPError(code, body):
+ return "Quota fetch failed (HTTP \(code))\(body.map { ": \($0)" } ?? "")"
+ case .usageDecodeFailed:
+ return "Quota response was malformed."
+ case let .network(err):
+ return "Network error: \(err.localizedDescription)"
+ case let .credential(err):
+ return err.errorDescription
+ }
+ }
+
+ /// True when the user must take action (re-run claude/login or click
+ /// Reconnect). Drives the red "Reconnect" UI path.
+ var isTerminal: Bool {
+ if case let .credential(err) = self { return err.isTerminal }
+ if case let .bootstrapFailed(err) = self { return err.isTerminal }
+ return false
+ }
+
+ var rateLimitRetryAt: Date? {
+ if case let .rateLimited(retryAt) = self { return retryAt }
+ return nil
+ }
+ }
+
+ // MARK: - Public API
+
+ /// User-initiated. Reads Claude's keychain (PROMPTS), copies to our keychain,
+ /// then fetches usage. Idempotent — safe to call again to "reconnect".
+ static func bootstrap() async throws -> SubscriptionUsage {
+ // Honour the same 429 backoff that refreshIfBootstrapped respects.
+ // Without this, a user spamming Reconnect during a sustained
+ // rate-limit window hammers Anthropic on every click — exactly the
+ // pattern that escalates the backoff.
+ if let until = usageBlockedUntil(), until > Date() {
+ throw FetchError.rateLimited(retryAt: until)
+ }
+ let record: ClaudeCredentialStore.CredentialRecord
+ do {
+ record = try ClaudeCredentialStore.bootstrap()
+ } catch let err as ClaudeCredentialStore.StoreError {
+ throw FetchError.bootstrapFailed(err)
+ }
+ return try await fetchWithRecord(initial: record)
+ }
+
+ /// Background refresh. Never prompts. Returns nil if not yet bootstrapped.
+ static func refreshIfBootstrapped() async throws -> SubscriptionUsage? {
+ guard ClaudeCredentialStore.isBootstrapCompleted else {
+ return nil
+ }
+
+ // Honour an outstanding rate-limit window — we recorded a 429 recently
+ // and Anthropic told us when to come back.
+ if let until = usageBlockedUntil(), until > Date() {
+ throw FetchError.rateLimited(retryAt: until)
+ }
+
+ do {
+ let token = try await ClaudeCredentialStore.freshAccessToken()
+ guard let token else { throw FetchError.notBootstrapped }
+ return try await fetch(token: token, allowOne401Recovery: true)
+ } catch let err as ClaudeCredentialStore.StoreError {
+ throw FetchError.credential(err)
+ } catch let err as FetchError {
+ throw err
+ }
+ }
+
+ /// Reset everything — used on user-initiated disconnect.
+ static func disconnect() {
+ ClaudeCredentialStore.resetBootstrap()
+ clearUsageBlock()
+ }
+
+ // MARK: - Internal
+
+ private static func fetchWithRecord(initial record: ClaudeCredentialStore.CredentialRecord) async throws -> SubscriptionUsage {
+ do {
+ return try await fetch(token: record.accessToken, allowOne401Recovery: true)
+ } catch let err as FetchError {
+ throw err
+ } catch let err as ClaudeCredentialStore.StoreError {
+ throw FetchError.credential(err)
+ }
+ }
+
+ private static func fetch(token: String, allowOne401Recovery: Bool) async throws -> SubscriptionUsage {
+ var request = URLRequest(url: usageURL)
+ request.httpMethod = "GET"
+ request.timeoutInterval = 30
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.setValue(betaHeader, forHTTPHeaderField: "anthropic-beta")
+ request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
+
+ let data: Data
+ let response: URLResponse
+ do {
+ (data, response) = try await URLSession.shared.data(for: request)
+ } catch {
+ throw FetchError.network(error)
+ }
+ guard let http = response as? HTTPURLResponse else {
+ throw FetchError.usageHTTPError(-1, nil)
+ }
+
+ switch http.statusCode {
+ case 200:
+ clearUsageBlock()
+ do {
+ let decoded = try JSONDecoder().decode(UsageResponse.self, from: data)
+ let tier = try ClaudeCredentialStore.subscriptionTier()
+ return mapResponse(decoded, rawTier: tier)
+ } catch {
+ throw FetchError.usageDecodeFailed
+ }
+ case 401:
+ if allowOne401Recovery {
+ let newToken = try await ClaudeCredentialStore.refreshAfter401()
+ return try await fetch(token: newToken, allowOne401Recovery: false)
+ }
+ throw FetchError.usageHTTPError(401, String(data: data, encoding: .utf8))
+ case 429:
+ let body = String(data: data, encoding: .utf8)
+ let retryAfter = parseRetryAfter(body: body)
+ let until = recordUsageRateLimit(retryAfterSeconds: retryAfter)
+ throw FetchError.rateLimited(retryAt: until)
+ default:
+ throw FetchError.usageHTTPError(http.statusCode, String(data: data, encoding: .utf8))
+ }
+ }
+
+ // MARK: - 429 backoff
+
+ private static func usageBlockedUntil() -> Date? {
+ UserDefaults.standard.object(forKey: usageBlockedUntilKey) as? Date
+ }
+
+ private static func clearUsageBlock() {
+ UserDefaults.standard.removeObject(forKey: usageBlockedUntilKey)
+ }
+
+ @discardableResult
+ private static func recordUsageRateLimit(retryAfterSeconds: Int?) -> Date {
+ let seconds = max(retryAfterSeconds ?? 300, 60)
+ let until = Date().addingTimeInterval(TimeInterval(seconds))
+ UserDefaults.standard.set(until, forKey: usageBlockedUntilKey)
+ return until
+ }
+
+ private static func parseRetryAfter(body: String?) -> Int? {
+ guard let body, let data = body.data(using: .utf8) else { return nil }
+ if let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any] {
+ if let n = json["retry_after"] as? Int { return n }
+ if let s = json["retry_after"] as? String, let n = Int(s) { return n }
+ }
+ return nil
+ }
+
+ // MARK: - Response mapping
+
+ private struct UsageResponse: Decodable {
+ let fiveHour: Window?
+ let sevenDay: Window?
+ let sevenDayOpus: Window?
+ let sevenDaySonnet: Window?
+
+ enum CodingKeys: String, CodingKey {
+ case fiveHour = "five_hour"
+ case sevenDay = "seven_day"
+ case sevenDayOpus = "seven_day_opus"
+ case sevenDaySonnet = "seven_day_sonnet"
+ }
+ }
+
+ private struct Window: Decodable {
+ let utilization: Double?
+ let resetsAt: String?
+ enum CodingKeys: String, CodingKey {
+ case utilization
+ case resetsAt = "resets_at"
+ }
+ }
+
+ private static func mapResponse(_ r: UsageResponse, rawTier: String?) -> SubscriptionUsage {
+ SubscriptionUsage(
+ tier: SubscriptionUsage.tier(from: rawTier),
+ rawTier: rawTier,
+ fiveHourPercent: r.fiveHour?.utilization,
+ fiveHourResetsAt: parseDate(r.fiveHour?.resetsAt),
+ sevenDayPercent: r.sevenDay?.utilization,
+ sevenDayResetsAt: parseDate(r.sevenDay?.resetsAt),
+ sevenDayOpusPercent: r.sevenDayOpus?.utilization,
+ sevenDayOpusResetsAt: parseDate(r.sevenDayOpus?.resetsAt),
+ sevenDaySonnetPercent: r.sevenDaySonnet?.utilization,
+ sevenDaySonnetResetsAt: parseDate(r.sevenDaySonnet?.resetsAt),
+ fetchedAt: Date()
+ )
+ }
+
+ private static func parseDate(_ s: String?) -> Date? {
+ guard let s, !s.isEmpty else { return nil }
+ let f = ISO8601DateFormatter()
+ f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
+ if let d = f.date(from: s) { return d }
+ f.formatOptions = [.withInternetDateTime]
+ return f.date(from: s)
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift b/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift
new file mode 100644
index 0000000..cffae7b
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift
@@ -0,0 +1,336 @@
+import Foundation
+import Security
+
+/// Owns the Codex (ChatGPT-mode) OAuth credential lifecycle. Mirrors
+/// ClaudeCredentialStore but reads from ~/.codex/auth.json — Codex CLI
+/// already stores its tokens as plaintext JSON in the home directory, so
+/// no keychain prompt is involved on bootstrap. After the user clicks
+/// Connect we cache a copy under ~/Library/Application Support/CodeBurn so
+/// we keep using rotated tokens after refresh.
+enum CodexCredentialStore {
+ private static let bootstrapCompletedKey = "codeburn.codex.bootstrapCompleted"
+ private static let inMemoryTTL: TimeInterval = 5 * 60
+ private static let proactiveRefreshMargin: TimeInterval = 5 * 60
+
+ private static let oauthClientID = "app_EMoamEEZ73f0CkXaXp7hrann"
+ private static let refreshURL = URL(string: "https://auth.openai.com/oauth/token")!
+ private static let codexAuthPath = ".codex/auth.json"
+ private static let maxCredentialBytes = 64 * 1024
+
+ private static let cacheFilename = "codex-credentials.v1.json"
+ private static let ourKeychainService = "org.agentseal.codeburn.menubar.codex.oauth.v1"
+ private static let ourKeychainAccount = "default"
+
+ private static let lock = NSLock()
+ private nonisolated(unsafe) static var memoryCache: CachedRecord?
+
+ struct CachedRecord {
+ let record: CredentialRecord
+ let cachedAt: Date
+
+ var isFresh: Bool { Date().timeIntervalSince(cachedAt) < CodexCredentialStore.inMemoryTTL }
+ }
+
+ struct CredentialRecord: Codable, Equatable {
+ let accessToken: String
+ let refreshToken: String
+ let idToken: String?
+ let accountId: String?
+ let expiresAt: Date?
+ }
+
+ enum StoreError: Error, LocalizedError {
+ case bootstrapNoSource
+ case bootstrapDecodeFailed
+ case bootstrapNotChatGPT // user is on API-key mode; we need ChatGPT mode for quota
+ case fileWriteFailed(String)
+ case refreshHTTPError(Int, String?)
+ case refreshNetworkError(Error)
+ case refreshDecodeFailed
+ case noRefreshToken
+
+ var errorDescription: String? {
+ switch self {
+ case .bootstrapNoSource:
+ return "No Codex credentials found at ~/.codex/auth.json. Run `codex` to sign in."
+ case .bootstrapDecodeFailed:
+ return "Codex credentials are malformed."
+ case .bootstrapNotChatGPT:
+ return "Codex is in API-key mode; live quota tracking is only available for ChatGPT subscriptions."
+ case let .fileWriteFailed(message):
+ return "Could not write to local cache: \(message)"
+ case let .refreshHTTPError(code, body):
+ return "Codex token refresh failed (HTTP \(code))\(body.map { ": \($0)" } ?? "")"
+ case let .refreshNetworkError(err):
+ return "Codex token refresh network error: \(err.localizedDescription)"
+ case .refreshDecodeFailed:
+ return "Codex token refresh response was malformed."
+ case .noRefreshToken:
+ return "No refresh token available; reconnect required."
+ }
+ }
+
+ /// True when the user must take action: rerun `codex` to re-authenticate
+ /// or switch from API-key to ChatGPT mode. Drives the red Reconnect path.
+ var isTerminal: Bool {
+ if case let .refreshHTTPError(code, body) = self, code >= 400, code < 500 {
+ let lower = body?.lowercased() ?? ""
+ if lower.contains("refresh_token_expired") ||
+ lower.contains("refresh_token_reused") ||
+ lower.contains("refresh_token_invalidated") ||
+ lower.contains("invalid_grant")
+ {
+ return true
+ }
+ return true
+ }
+ switch self {
+ case .noRefreshToken, .bootstrapNotChatGPT, .bootstrapNoSource: return true
+ default: return false
+ }
+ }
+ }
+
+ // MARK: - Bootstrap state
+
+ static var isBootstrapCompleted: Bool {
+ get { UserDefaults.standard.bool(forKey: bootstrapCompletedKey) }
+ set { UserDefaults.standard.set(newValue, forKey: bootstrapCompletedKey) }
+ }
+
+ static func resetBootstrap() {
+ lock.withLock { memoryCache = nil }
+ deleteOurCache()
+ isBootstrapCompleted = false
+ }
+
+ // MARK: - Public API
+
+ @discardableResult
+ static func bootstrap() throws -> CredentialRecord {
+ let record = try readCodexAuth()
+ try writeOurCache(record: record)
+ isBootstrapCompleted = true
+ cacheInMemory(record)
+ return record
+ }
+
+ static func currentRecord() throws -> CredentialRecord? {
+ guard isBootstrapCompleted else { return nil }
+ if let cached = lock.withLock({ memoryCache }), cached.isFresh {
+ return cached.record
+ }
+ if let stored = try readOurCache() {
+ cacheInMemory(stored)
+ return stored
+ }
+ isBootstrapCompleted = false
+ return nil
+ }
+
+ static func freshAccessToken() async throws -> String? {
+ guard let record = try currentRecord() else { return nil }
+ if let expiresAt = record.expiresAt, expiresAt.timeIntervalSinceNow < proactiveRefreshMargin {
+ let updated = try await refreshAndPersist(record: record)
+ return updated.accessToken
+ }
+ return record.accessToken
+ }
+
+ static func refreshAfter401() async throws -> String {
+ guard let record = try currentRecord() else { throw StoreError.noRefreshToken }
+ let updated = try await refreshAndPersist(record: record)
+ return updated.accessToken
+ }
+
+ // MARK: - Bootstrap source: ~/.codex/auth.json
+
+ private static func readCodexAuth() throws -> CredentialRecord {
+ let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(codexAuthPath)
+ guard FileManager.default.fileExists(atPath: url.path) else {
+ throw StoreError.bootstrapNoSource
+ }
+ let data = try SafeFile.read(from: url.path, maxBytes: maxCredentialBytes)
+ struct Root: Decodable {
+ let auth_mode: String?
+ let tokens: Tokens?
+ }
+ struct Tokens: Decodable {
+ let access_token: String?
+ let refresh_token: String?
+ let id_token: String?
+ let account_id: String?
+ }
+ do {
+ let root = try JSONDecoder().decode(Root.self, from: data)
+ // Live quota is only meaningful for ChatGPT-mode auth. API-key users
+ // have a different billing surface (/v1/usage) which we do not yet
+ // implement here.
+ guard root.auth_mode == "chatgpt" else {
+ throw StoreError.bootstrapNotChatGPT
+ }
+ guard let tokens = root.tokens,
+ let access = tokens.access_token?.trimmingCharacters(in: .whitespacesAndNewlines),
+ let refresh = tokens.refresh_token?.trimmingCharacters(in: .whitespacesAndNewlines),
+ !access.isEmpty, !refresh.isEmpty
+ else {
+ throw StoreError.bootstrapDecodeFailed
+ }
+ return CredentialRecord(
+ accessToken: access,
+ refreshToken: refresh,
+ idToken: tokens.id_token,
+ accountId: tokens.account_id,
+ expiresAt: nil // Codex CLI does not record expiresAt in auth.json
+ )
+ } catch let err as StoreError {
+ throw err
+ } catch {
+ throw StoreError.bootstrapDecodeFailed
+ }
+ }
+
+ // MARK: - Local cache file
+
+ private static func cacheFileURL() -> URL {
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
+ ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support")
+ return support
+ .appendingPathComponent("CodeBurn", isDirectory: true)
+ .appendingPathComponent(cacheFilename)
+ }
+
+ private static func readOurCache() throws -> CredentialRecord? {
+ if let record = try readOurKeychainCache() {
+ return record
+ }
+
+ let url = cacheFileURL()
+ guard FileManager.default.fileExists(atPath: url.path) else { return nil }
+ // Symlink-defense + size cap (same hardening as ClaudeCredentialStore).
+ let data = try SafeFile.read(from: url.path, maxBytes: maxCredentialBytes)
+ guard let record = try? JSONDecoder().decode(CredentialRecord.self, from: data) else { return nil }
+ try? writeOurKeychainCache(record: record)
+ try? FileManager.default.removeItem(at: url)
+ return record
+ }
+
+ private static func writeOurCache(record: CredentialRecord) throws {
+ try writeOurKeychainCache(record: record)
+ }
+
+ private static func readOurKeychainCache() throws -> CredentialRecord? {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ kSecMatchLimit as String: kSecMatchLimitOne,
+ kSecReturnData as String: true,
+ ]
+ var result: CFTypeRef?
+ let status = SecItemCopyMatching(query as CFDictionary, &result)
+ if status == errSecItemNotFound { return nil }
+ guard status == errSecSuccess, let data = result as? Data else {
+ throw StoreError.fileWriteFailed("keychain read failed with status \(status)")
+ }
+ return try? JSONDecoder().decode(CredentialRecord.self, from: data)
+ }
+
+ private static func writeOurKeychainCache(record: CredentialRecord) throws {
+ let url = cacheFileURL()
+ let data = try JSONEncoder().encode(record)
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ ]
+ let attributes: [String: Any] = [
+ kSecValueData as String: data,
+ kSecAttrAccessible as String: kSecAttrAccessibleAfterFirstUnlockThisDeviceOnly,
+ ]
+ let status = SecItemUpdate(query as CFDictionary, attributes as CFDictionary)
+ if status == errSecItemNotFound {
+ var add = query
+ add.merge(attributes) { _, new in new }
+ let addStatus = SecItemAdd(add as CFDictionary, nil)
+ guard addStatus == errSecSuccess else {
+ throw StoreError.fileWriteFailed("keychain write failed with status \(addStatus)")
+ }
+ } else if status != errSecSuccess {
+ throw StoreError.fileWriteFailed("keychain update failed with status \(status)")
+ }
+ try? FileManager.default.removeItem(at: url)
+ }
+
+ private static func deleteOurCache() {
+ let query: [String: Any] = [
+ kSecClass as String: kSecClassGenericPassword,
+ kSecAttrService as String: ourKeychainService,
+ kSecAttrAccount as String: ourKeychainAccount,
+ ]
+ SecItemDelete(query as CFDictionary)
+ try? FileManager.default.removeItem(at: cacheFileURL())
+ }
+
+ private static func cacheInMemory(_ record: CredentialRecord) {
+ lock.withLock { memoryCache = CachedRecord(record: record, cachedAt: Date()) }
+ }
+
+ // MARK: - Refresh
+
+ private static func refreshAndPersist(record: CredentialRecord) async throws -> CredentialRecord {
+ guard !record.refreshToken.isEmpty else { throw StoreError.noRefreshToken }
+
+ var request = URLRequest(url: refreshURL)
+ request.httpMethod = "POST"
+ request.timeoutInterval = 30
+ request.setValue("application/json", forHTTPHeaderField: "Content-Type")
+ let body: [String: String] = [
+ "client_id": oauthClientID,
+ "grant_type": "refresh_token",
+ "refresh_token": record.refreshToken,
+ "scope": "openid profile email",
+ ]
+ request.httpBody = try JSONSerialization.data(withJSONObject: body)
+
+ let data: Data
+ let response: URLResponse
+ do {
+ (data, response) = try await URLSession.shared.data(for: request)
+ } catch {
+ throw StoreError.refreshNetworkError(error)
+ }
+ guard let http = response as? HTTPURLResponse else {
+ throw StoreError.refreshHTTPError(-1, nil)
+ }
+ guard http.statusCode == 200 else {
+ let body = String(data: data, encoding: .utf8)
+ throw StoreError.refreshHTTPError(http.statusCode, body)
+ }
+
+ struct RefreshResponse: Decodable {
+ let access_token: String
+ let refresh_token: String?
+ let id_token: String?
+ let expires_in: Int?
+ }
+ guard let decoded = try? JSONDecoder().decode(RefreshResponse.self, from: data) else {
+ throw StoreError.refreshDecodeFailed
+ }
+
+ let updated = CredentialRecord(
+ accessToken: decoded.access_token,
+ refreshToken: decoded.refresh_token ?? record.refreshToken,
+ idToken: decoded.id_token ?? record.idToken,
+ accountId: record.accountId,
+ expiresAt: decoded.expires_in.map { Date().addingTimeInterval(TimeInterval($0)) } ?? record.expiresAt
+ )
+ cacheInMemory(updated)
+ do {
+ try writeOurCache(record: updated)
+ } catch {
+ NSLog("CodeBurn: codex cache write failed during refresh rotation: %@", String(describing: error))
+ }
+ return updated
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/CodexSubscriptionService.swift b/mac/Sources/CodeBurnMenubar/Data/CodexSubscriptionService.swift
new file mode 100644
index 0000000..ac3bd94
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/CodexSubscriptionService.swift
@@ -0,0 +1,243 @@
+import Foundation
+
+/// Mirror of ClaudeSubscriptionService for Codex (ChatGPT-mode). Hits
+/// /backend-api/wham/usage with the bearer token from CodexCredentialStore,
+/// applies an independent 429 backoff, and surfaces terminal vs transient
+/// failures to the UI.
+enum CodexSubscriptionService {
+ private static let usageURL = URL(string: "https://chatgpt.com/backend-api/wham/usage")!
+ private static let usageBlockedUntilKey = "codeburn.codex.usage.blockedUntil"
+
+ enum FetchError: Error, LocalizedError {
+ case notBootstrapped
+ case bootstrapFailed(CodexCredentialStore.StoreError)
+ case rateLimited(retryAt: Date)
+ case usageHTTPError(Int, String?)
+ case usageDecodeFailed
+ case network(Error)
+ case credential(CodexCredentialStore.StoreError)
+
+ var errorDescription: String? {
+ switch self {
+ case .notBootstrapped:
+ return "Connect Codex in Settings to start tracking quota."
+ case let .bootstrapFailed(err): return err.errorDescription
+ case let .rateLimited(retryAt):
+ let f = RelativeDateTimeFormatter()
+ f.unitsStyle = .short
+ return "ChatGPT rate-limited the quota endpoint. Retrying \(f.localizedString(for: retryAt, relativeTo: Date()))."
+ case let .usageHTTPError(code, body):
+ return "Codex quota fetch failed (HTTP \(code))\(body.map { ": \($0)" } ?? "")"
+ case .usageDecodeFailed: return "Codex quota response was malformed."
+ case let .network(err): return "Network error: \(err.localizedDescription)"
+ case let .credential(err): return err.errorDescription
+ }
+ }
+
+ var isTerminal: Bool {
+ if case let .credential(err) = self { return err.isTerminal }
+ if case let .bootstrapFailed(err) = self { return err.isTerminal }
+ return false
+ }
+
+ var rateLimitRetryAt: Date? {
+ if case let .rateLimited(retryAt) = self { return retryAt }
+ return nil
+ }
+ }
+
+ static func bootstrap() async throws -> CodexUsage {
+ // Honour the same 429 backoff that refreshIfBootstrapped respects.
+ // A user clicking Reconnect during a sustained ChatGPT rate-limit
+ // window would otherwise re-hit /wham/usage on every click and keep
+ // the backoff window pegged.
+ if let until = usageBlockedUntil(), until > Date() {
+ throw FetchError.rateLimited(retryAt: until)
+ }
+ let record: CodexCredentialStore.CredentialRecord
+ do {
+ record = try CodexCredentialStore.bootstrap()
+ } catch let err as CodexCredentialStore.StoreError {
+ throw FetchError.bootstrapFailed(err)
+ }
+ return try await fetchWithToken(record.accessToken, allowOne401Recovery: true)
+ }
+
+ static func refreshIfBootstrapped() async throws -> CodexUsage? {
+ guard CodexCredentialStore.isBootstrapCompleted else { return nil }
+ if let until = usageBlockedUntil(), until > Date() {
+ throw FetchError.rateLimited(retryAt: until)
+ }
+ do {
+ let token = try await CodexCredentialStore.freshAccessToken()
+ guard let token else { throw FetchError.notBootstrapped }
+ return try await fetchWithToken(token, allowOne401Recovery: true)
+ } catch let err as CodexCredentialStore.StoreError {
+ throw FetchError.credential(err)
+ }
+ }
+
+ static func disconnect() {
+ CodexCredentialStore.resetBootstrap()
+ clearUsageBlock()
+ }
+
+ private static func fetchWithToken(_ token: String, allowOne401Recovery: Bool) async throws -> CodexUsage {
+ var request = URLRequest(url: usageURL)
+ request.httpMethod = "GET"
+ request.timeoutInterval = 30
+ request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
+ request.setValue("application/json", forHTTPHeaderField: "Accept")
+ request.setValue("CodeBurn", forHTTPHeaderField: "User-Agent")
+ // chatgpt.com routes the rate_limit envelope per ChatGPT account. Without
+ // this header the response often comes back as a guest-shape document
+ // missing rate_limit entirely, which our decoder then fails on.
+ if let accountId = try? CodexCredentialStore.currentRecord()?.accountId, !accountId.isEmpty {
+ request.setValue(accountId, forHTTPHeaderField: "ChatGPT-Account-Id")
+ }
+
+ let data: Data
+ let response: URLResponse
+ do {
+ (data, response) = try await URLSession.shared.data(for: request)
+ } catch {
+ throw FetchError.network(error)
+ }
+ guard let http = response as? HTTPURLResponse else {
+ throw FetchError.usageHTTPError(-1, nil)
+ }
+
+ switch http.statusCode {
+ case 200:
+ clearUsageBlock()
+ do {
+ return try decodeUsage(data: data)
+ } catch {
+ // Do not log the response body — it's user-account data from
+ // chatgpt.com and is readable by other local users via
+ // `log stream`. The decode error type alone is enough to
+ // bisect schema drift if needed.
+ NSLog("CodeBurn: codex usage decode failed: %@", String(describing: error))
+ throw FetchError.usageDecodeFailed
+ }
+ case 401:
+ if allowOne401Recovery {
+ let newToken = try await CodexCredentialStore.refreshAfter401()
+ return try await fetchWithToken(newToken, allowOne401Recovery: false)
+ }
+ throw FetchError.usageHTTPError(401, String(data: data, encoding: .utf8))
+ case 429:
+ // Honour the RFC Retry-After header when present — ChatGPT's quota
+ // endpoint sometimes sets it to a window shorter than our 5-min
+ // floor, and ignoring it forced users to wait longer than the
+ // server actually wanted.
+ let retryAfter = parseRetryAfterHeader(http.value(forHTTPHeaderField: "Retry-After"))
+ let until = recordUsageRateLimit(retryAfterSeconds: retryAfter)
+ throw FetchError.rateLimited(retryAt: until)
+ default:
+ throw FetchError.usageHTTPError(http.statusCode, String(data: data, encoding: .utf8))
+ }
+ }
+
+ private struct UsageDTO: Decodable {
+ let plan_type: String?
+ let rate_limit: RateLimit?
+ let additional_rate_limits: [AdditionalLimitDTO]?
+ let credits: Credits?
+
+ struct RateLimit: Decodable {
+ let primary_window: WindowDTO?
+ let secondary_window: WindowDTO?
+ }
+ struct AdditionalLimitDTO: Decodable {
+ let limit_name: String?
+ let rate_limit: RateLimit?
+ }
+ struct WindowDTO: Decodable {
+ let used_percent: Double?
+ let reset_at: Int?
+ let limit_window_seconds: Int?
+ }
+ // chatgpt.com sometimes serializes balance as a Double ("balance": 0.0)
+ // and other times as a String ("balance": "0.00"). Mirror CodexBar's
+ // resilient decode so a schema drift on either shape doesn't blow up
+ // the whole quota fetch.
+ struct Credits: Decodable {
+ let balance: Double?
+ enum CodingKeys: String, CodingKey { case balance }
+ init(from decoder: Decoder) throws {
+ let c = try decoder.container(keyedBy: CodingKeys.self)
+ if let n = try? c.decode(Double.self, forKey: .balance) {
+ balance = n
+ } else if let s = try? c.decode(String.self, forKey: .balance), let n = Double(s) {
+ balance = n
+ } else {
+ balance = nil
+ }
+ }
+ }
+ }
+
+ private static func decodeUsage(data: Data) throws -> CodexUsage {
+ let root = try JSONDecoder().decode(UsageDTO.self, from: data)
+ let additional: [CodexUsage.AdditionalLimit] = (root.additional_rate_limits ?? []).compactMap { dto in
+ guard let name = dto.limit_name, !name.isEmpty else { return nil }
+ return CodexUsage.AdditionalLimit(
+ name: name,
+ primary: makeWindow(dto.rate_limit?.primary_window),
+ secondary: makeWindow(dto.rate_limit?.secondary_window)
+ )
+ }
+ return CodexUsage(
+ plan: CodexUsage.planType(from: root.plan_type),
+ primary: makeWindow(root.rate_limit?.primary_window),
+ secondary: makeWindow(root.rate_limit?.secondary_window),
+ additionalLimits: additional,
+ creditsBalance: root.credits?.balance,
+ fetchedAt: Date()
+ )
+ }
+
+ private static func makeWindow(_ dto: UsageDTO.WindowDTO?) -> CodexUsage.Window? {
+ guard let dto, let used = dto.used_percent, let windowSeconds = dto.limit_window_seconds else {
+ return nil
+ }
+ let resetsAt = dto.reset_at.map { Date(timeIntervalSince1970: TimeInterval($0)) }
+ return CodexUsage.Window(usedPercent: used, resetsAt: resetsAt, limitWindowSeconds: windowSeconds)
+ }
+
+ // MARK: - 429 backoff
+
+ private static func usageBlockedUntil() -> Date? {
+ UserDefaults.standard.object(forKey: usageBlockedUntilKey) as? Date
+ }
+
+ private static func clearUsageBlock() {
+ UserDefaults.standard.removeObject(forKey: usageBlockedUntilKey)
+ }
+
+ @discardableResult
+ /// RFC 7231 says Retry-After is either a delta-seconds or an HTTP-date.
+ /// chatgpt.com appears to send delta-seconds today; we still parse both
+ /// shapes defensively so a future change to HTTP-date doesn't drop us
+ /// onto the silent 5-minute floor.
+ private static func parseRetryAfterHeader(_ value: String?) -> Int? {
+ guard let value = value?.trimmingCharacters(in: .whitespaces), !value.isEmpty else { return nil }
+ if let seconds = Int(value), seconds >= 0 { return seconds }
+ let f = DateFormatter()
+ f.locale = Locale(identifier: "en_US_POSIX")
+ f.timeZone = TimeZone(secondsFromGMT: 0)
+ f.dateFormat = "EEE, dd MMM yyyy HH:mm:ss zzz"
+ if let date = f.date(from: value) {
+ return max(0, Int(date.timeIntervalSinceNow))
+ }
+ return nil
+ }
+
+ private static func recordUsageRateLimit(retryAfterSeconds: Int?) -> Date {
+ let seconds = max(retryAfterSeconds ?? 300, 60)
+ let until = Date().addingTimeInterval(TimeInterval(seconds))
+ UserDefaults.standard.set(until, forKey: usageBlockedUntilKey)
+ return until
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/CodexUsage.swift b/mac/Sources/CodeBurnMenubar/Data/CodexUsage.swift
new file mode 100644
index 0000000..719b117
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/CodexUsage.swift
@@ -0,0 +1,98 @@
+import Foundation
+
+/// Codex (ChatGPT-mode) live quota snapshot returned by /backend-api/wham/usage.
+/// Two windows are exposed: primary (typically the 5-hour rolling window) and
+/// secondary (typically the weekly window). Window size is dynamic per
+/// account — `limitWindowSeconds` tells us whether it's a 5-hour or 7-day
+/// boundary so we can label correctly.
+struct CodexUsage: Sendable, Equatable {
+ enum PlanType: Sendable, Equatable {
+ case guest, free, go, plus, pro, prolite, freeWorkspace, team
+ case business, education, quorum, k12, enterprise, edu
+ /// Captures any plan_type string OpenAI ships that we haven't enumerated
+ /// yet, so the Settings/Plan UI can still show "Plan: " instead of
+ /// a generic "Subscription" placeholder. Preserves forward compatibility
+ /// without requiring a CodeBurn update for every new tier.
+ case unknown(String)
+
+ var displayName: String {
+ switch self {
+ case .guest: "Guest"
+ case .free: "Free"
+ case .go: "Go"
+ case .plus: "Plus"
+ case .pro: "Pro"
+ case .prolite: "Pro Lite"
+ case .freeWorkspace: "Free Workspace"
+ case .team: "Team"
+ case .business: "Business"
+ case .education: "Education"
+ case .quorum: "Quorum"
+ case .k12: "K-12"
+ case .enterprise: "Enterprise"
+ case .edu: "Edu"
+ case let .unknown(raw): raw.isEmpty ? "Subscription" : raw.capitalized
+ }
+ }
+ }
+
+ struct Window: Sendable, Equatable {
+ let usedPercent: Double // 0.0 ... 100.0
+ let resetsAt: Date?
+ let limitWindowSeconds: Int
+
+ /// Human label inferred from window size: 5h, 1d, 7d, etc.
+ var windowLabel: String {
+ switch limitWindowSeconds {
+ case 0..<3600: return "Hourly"
+ case 3600..<7200: return "Hour"
+ case 18000..<19000: return "5-hour"
+ case 86400..<87000: return "Daily"
+ case 604800..<605000: return "Weekly"
+ default:
+ let hours = limitWindowSeconds / 3600
+ if hours < 24 { return "\(hours)-hour" }
+ return "\(hours / 24)-day"
+ }
+ }
+ }
+
+ /// Additional per-model / per-feature quotas exposed by ChatGPT alongside
+ /// the main rate_limit (e.g. "GPT-5.3-Codex-Spark"). Each entry has its
+ /// own primary/secondary windows. Only ones with non-zero utilization are
+ /// surfaced in the popover so users on plans that don't touch these
+ /// features don't see clutter.
+ struct AdditionalLimit: Sendable, Equatable {
+ let name: String
+ let primary: Window?
+ let secondary: Window?
+ }
+
+ let plan: PlanType
+ let primary: Window?
+ let secondary: Window?
+ let additionalLimits: [AdditionalLimit]
+ let creditsBalance: Double?
+ let fetchedAt: Date
+
+ static func planType(from raw: String?) -> PlanType {
+ guard let raw = raw?.lowercased() else { return .unknown("") }
+ switch raw {
+ case "guest": return .guest
+ case "free": return .free
+ case "go": return .go
+ case "plus": return .plus
+ case "pro": return .pro
+ case "prolite", "pro_lite", "pro-lite": return .prolite
+ case "free_workspace": return .freeWorkspace
+ case "team": return .team
+ case "business": return .business
+ case "education": return .education
+ case "quorum": return .quorum
+ case "k12": return .k12
+ case "enterprise": return .enterprise
+ case "edu": return .edu
+ default: return .unknown(raw)
+ }
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/DataClient.swift b/mac/Sources/CodeBurnMenubar/Data/DataClient.swift
index a6884be..4b0083c 100644
--- a/mac/Sources/CodeBurnMenubar/Data/DataClient.swift
+++ b/mac/Sources/CodeBurnMenubar/Data/DataClient.swift
@@ -6,7 +6,7 @@ import Foundation
/// Pipe file descriptors pinned forever.
private let maxPayloadBytes = 20 * 1024 * 1024
private let maxStderrBytes = 256 * 1024
-private let spawnTimeoutSeconds: UInt64 = 20
+private let spawnTimeoutSeconds: UInt64 = 45
enum DataClientError: Error {
case spawn(String)
@@ -61,21 +61,27 @@ struct DataClient {
throw DataClientError.spawn(error.localizedDescription)
}
- // Drain both pipes concurrently so a large stderr can't deadlock stdout (the child
- // blocks on write once the pipe buffer fills). `drain` also enforces a byte cap.
- async let stdoutData = drain(outPipe.fileHandleForReading, limit: maxPayloadBytes)
- async let stderrData = drain(errPipe.fileHandleForReading, limit: maxStderrBytes)
-
- // Wall-clock timeout: if the CLI hangs (parser stuck, disk stall), kill it.
let timeoutTask = Task.detached(priority: .utility) {
try? await Task.sleep(nanoseconds: spawnTimeoutSeconds * 1_000_000_000)
if process.isRunning {
- process.terminate()
+ NSLog("CodeBurn: CLI subprocess timed out after %llus for %@ — terminating",
+ spawnTimeoutSeconds, subcommand.joined(separator: " "))
+ terminateWithEscalation(process)
}
}
defer { timeoutTask.cancel() }
- let (out, err) = await (stdoutData, stderrData)
+ let outHandle = outPipe.fileHandleForReading
+ let errHandle = errPipe.fileHandleForReading
+ let (out, err) = await withTaskCancellationHandler {
+ async let stdoutData = drain(outHandle, limit: maxPayloadBytes)
+ async let stderrData = drain(errHandle, limit: maxStderrBytes)
+ return await (stdoutData, stderrData)
+ } onCancel: {
+ terminateWithEscalation(process)
+ }
+ try? outHandle.close()
+ try? errHandle.close()
process.waitUntilExit()
if out.count >= maxPayloadBytes {
@@ -86,22 +92,45 @@ struct DataClient {
return ProcessResult(stdout: out, stderr: stderrString, exitCode: process.terminationStatus)
}
- /// Pulls bytes off a pipe until EOF or `limit`. Intentionally uses `availableData`, which
- /// returns empty on EOF -- no blocking once the child exits.
+ private static func terminateWithEscalation(_ process: Process) {
+ guard process.isRunning else { return }
+ process.terminate()
+ let pid = process.processIdentifier
+ DispatchQueue.global(qos: .utility).asyncAfter(deadline: .now() + 0.5) {
+ if process.isRunning { kill(pid, SIGKILL) }
+ }
+ }
+
private static func drain(_ handle: FileHandle, limit: Int) async -> Data {
- await Task.detached(priority: .utility) {
- var buffer = Data()
- while buffer.count < limit {
- let chunk = handle.availableData
- if chunk.isEmpty { break }
- let remaining = limit - buffer.count
- if chunk.count > remaining {
- buffer.append(chunk.prefix(remaining))
- break
- }
- buffer.append(chunk)
+ let fd = handle.fileDescriptor
+ let flags = Darwin.fcntl(fd, F_GETFL)
+ if flags >= 0 {
+ _ = Darwin.fcntl(fd, F_SETFL, flags | O_NONBLOCK)
+ } else {
+ NSLog("CodeBurn: fcntl F_GETFL failed on fd %d, drain may block", fd)
+ }
+
+ var buffer = Data()
+ var chunk = [UInt8](repeating: 0, count: 65_536)
+
+ while buffer.count < limit && !Task.isCancelled {
+ let toRead = min(chunk.count, limit - buffer.count)
+ let n = chunk.withUnsafeMutableBufferPointer { ptr in
+ Darwin.read(fd, ptr.baseAddress!, toRead)
}
- return buffer
- }.value
+ if n > 0 {
+ buffer.append(contentsOf: chunk.prefix(n))
+ } else if n == 0 {
+ break
+ } else if errno == EAGAIN || errno == EWOULDBLOCK {
+ try? await Task.sleep(nanoseconds: 5_000_000)
+ } else if errno == EINTR {
+ continue
+ } else {
+ NSLog("CodeBurn: drain read() failed on fd %d: errno %d", fd, errno)
+ break
+ }
+ }
+ return buffer
}
}
diff --git a/mac/Sources/CodeBurnMenubar/Data/QuotaSummary.swift b/mac/Sources/CodeBurnMenubar/Data/QuotaSummary.swift
new file mode 100644
index 0000000..c76f6ba
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/QuotaSummary.swift
@@ -0,0 +1,75 @@
+import Foundation
+
+/// Per-provider live-quota snapshot consumed by the AgentTab progress bar
+/// and the hover-detail popover. Today only Claude has a real quota source
+/// (Anthropic /api/oauth/usage); future providers (Cursor, Copilot, etc.)
+/// will plug in by producing the same struct from their own auth path.
+struct QuotaSummary: Equatable {
+ enum Connection: Equatable {
+ case connected
+ case disconnected // no credentials present
+ case loading
+ case stale // had data once, current fetch is in flight
+ case transientFailure // backing off; show last-known data dimmed
+ case terminalFailure(reason: String?) // user must reconnect
+ }
+
+ let providerFilter: ProviderFilter
+ let connection: Connection
+ let primary: Window? // weekly utilization, the headline bar
+ let details: [Window] // 5h, weekly, opus, sonnet — full hover card
+ /// Display label for the user's plan (e.g. "Max 20x", "Pro Lite"). Shown
+ /// in the top-right corner of the hover detail popover so users can
+ /// confirm at a glance which subscription is feeding the bar.
+ let planLabel: String?
+ /// Optional footer rows that the popover renders below the window list.
+ /// Used today only by Codex to surface the on-account credits balance,
+ /// but kept generic so future providers can add provider-specific facts
+ /// (e.g. "Anthropic incident in progress", "Cursor team seat").
+ let footerLines: [String]
+
+ struct Window: Equatable {
+ let label: String
+ let percent: Double // 0..1
+ let resetsAt: Date?
+ }
+
+ /// Color band thresholds for the inline chip bar and aggregate menubar
+ /// flame tint. Four tiers so the icon can step from "you're approaching
+ /// your limit" (yellow) through "you're about to hit the wall" (orange)
+ /// to "you're over" (red) — matches what the user expects from a warning
+ /// indicator in the menu bar.
+ static func severity(for percent: Double) -> Severity {
+ if percent >= 1.0 { return .danger }
+ if percent >= 0.9 { return .critical }
+ if percent >= 0.7 { return .warning }
+ return .normal
+ }
+
+ enum Severity {
+ case normal // <70%
+ case warning // 70-90%
+ case critical // 90-100%
+ case danger // >=100%
+ }
+}
+
+extension QuotaSummary.Window {
+ /// Human-readable countdown like "2h 11m" or "3d 14h" or "now".
+ var resetsInLabel: String {
+ guard let resetsAt else { return "" }
+ let seconds = max(0, resetsAt.timeIntervalSinceNow)
+ if seconds < 60 { return "now" }
+ let minutes = Int(seconds / 60)
+ let hours = minutes / 60
+ let days = hours / 24
+ if days > 0 { return "\(days)d \(hours % 24)h" }
+ if hours > 0 { return "\(hours)h \(minutes % 60)m" }
+ return "\(minutes)m"
+ }
+
+ var percentLabel: String {
+ let pct = Int((percent * 100).rounded())
+ return "\(pct)%"
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/SubscriptionClient.swift b/mac/Sources/CodeBurnMenubar/Data/SubscriptionClient.swift
deleted file mode 100644
index 3f71e30..0000000
--- a/mac/Sources/CodeBurnMenubar/Data/SubscriptionClient.swift
+++ /dev/null
@@ -1,268 +0,0 @@
-import Foundation
-import Security
-
-private let credentialsRelativePath = ".claude/.credentials.json"
-private let keychainService = "Claude Code-credentials"
-private let oauthClientID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e"
-private let refreshURL = URL(string: "https://platform.claude.com/v1/oauth/token")!
-private let usageURL = URL(string: "https://api.anthropic.com/api/oauth/usage")!
-private let betaHeader = "oauth-2025-04-20"
-private let userAgent = "claude-code/2.1.0"
-private let requestTimeout: TimeInterval = 30
-
-private let maxCredentialBytes = 64 * 1024
-
-enum SubscriptionError: Error, LocalizedError {
- case noCredentials
- case credentialsInvalid
- case refreshFailed(Int, String?)
- case usageFetchFailed(Int, String?)
- case decodeFailed(Error)
-
- var errorDescription: String? {
- switch self {
- case .noCredentials: "No Claude OAuth credentials found"
- case .credentialsInvalid: "Claude OAuth credentials malformed"
- case let .refreshFailed(code, body): "Token refresh failed (\(code))\(body.map { ": \($0)" } ?? "")"
- case let .usageFetchFailed(code, body): "Usage fetch failed (\(code))\(body.map { ": \($0)" } ?? "")"
- case let .decodeFailed(err): "Decode failed: \(err.localizedDescription)"
- }
- }
-}
-
-struct SubscriptionClient {
- static func fetch() async throws -> SubscriptionUsage {
- let creds = try loadCredentials()
-
- // Try the usage call with the existing token first. Only refresh on 401.
- do {
- let response = try await fetchUsage(token: creds.accessToken)
- return mapResponse(response, rawTier: creds.rateLimitTier)
- } catch SubscriptionError.usageFetchFailed(401, _) {
- guard let refreshToken = creds.refreshToken, !refreshToken.isEmpty else {
- throw SubscriptionError.usageFetchFailed(401, "no refresh token available")
- }
- let newToken = try await refreshAccessToken(refreshToken: refreshToken)
- let response = try await fetchUsage(token: newToken)
- return mapResponse(response, rawTier: creds.rateLimitTier)
- }
- }
-
- // MARK: - Credentials
-
- private static func loadCredentials() throws -> StoredCredentials {
- if let data = try readFileCredentials() {
- return try parseCredentials(data: sanitizeKeychainData(data))
- }
- if let creds = try readKeychainCredentials() {
- return creds
- }
- throw SubscriptionError.noCredentials
- }
-
- private static func readFileCredentials() throws -> Data? {
- let url = FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(credentialsRelativePath)
- guard FileManager.default.fileExists(atPath: url.path) else { return nil }
- // SafeFile refuses to follow symlinks and caps the read, so a 6 GB /dev/urandom
- // masquerading as the creds file can't blow up the app.
- return try SafeFile.read(from: url.path, maxBytes: maxCredentialBytes)
- }
-
- private static func readKeychainCredentials() throws -> StoredCredentials? {
- let query: [String: Any] = [
- kSecClass as String: kSecClassGenericPassword,
- kSecAttrService as String: keychainService,
- kSecMatchLimit as String: kSecMatchLimitOne,
- kSecReturnData as String: true,
- ]
- var result: CFTypeRef?
- let status = SecItemCopyMatching(query as CFDictionary, &result)
- if status == errSecItemNotFound { return nil }
- guard status == errSecSuccess, let data = result as? Data else {
- NSLog("CodeBurn: keychain query failed status=\(status)")
- return nil
- }
- return try parseCredentials(data: sanitizeKeychainData(data))
- }
-
- /// Claude Code's keychain writer line-wraps long string values (newline + leading spaces)
- /// mid-token, producing JSON with literal control chars and stray spaces inside string
- /// values. Replace every newline (CR/LF) plus the run of spaces/tabs that follows it.
- /// Drops both the wrapping in tokens AND pretty-print indentation between fields (both
- /// produce valid, compact JSON afterward).
- private static func sanitizeKeychainData(_ data: Data) -> Data {
- guard var s = String(data: data, encoding: .utf8) else { return data }
- s = s.replacingOccurrences(of: "\r", with: "")
- let regex = try? NSRegularExpression(pattern: "\\n[ \\t]*", options: [])
- if let regex {
- let range = NSRange(s.startIndex.. StoredCredentials {
- do {
- let root = try JSONDecoder().decode(CredentialsRoot.self, from: data)
- guard let oauth = root.claudeAiOauth else { throw SubscriptionError.credentialsInvalid }
- let token = oauth.accessToken?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
- guard !token.isEmpty else { throw SubscriptionError.credentialsInvalid }
- let expiresAt = oauth.expiresAt.map { Date(timeIntervalSince1970: $0 / 1000.0) }
- return StoredCredentials(
- accessToken: token,
- refreshToken: oauth.refreshToken,
- expiresAt: expiresAt,
- rateLimitTier: oauth.rateLimitTier
- )
- } catch let err as SubscriptionError {
- throw err
- } catch {
- throw SubscriptionError.decodeFailed(error)
- }
- }
-
- // MARK: - Refresh
-
- private static func refreshAccessToken(refreshToken: String) async throws -> String {
- var request = URLRequest(url: refreshURL)
- request.httpMethod = "POST"
- request.timeoutInterval = requestTimeout
- request.setValue("application/x-www-form-urlencoded", forHTTPHeaderField: "Content-Type")
- request.setValue("application/json", forHTTPHeaderField: "Accept")
- var components = URLComponents()
- components.queryItems = [
- URLQueryItem(name: "grant_type", value: "refresh_token"),
- URLQueryItem(name: "refresh_token", value: refreshToken),
- URLQueryItem(name: "client_id", value: oauthClientID),
- ]
- request.httpBody = (components.percentEncodedQuery ?? "").data(using: .utf8)
-
- let (data, response) = try await URLSession.shared.data(for: request)
- guard let http = response as? HTTPURLResponse else {
- throw SubscriptionError.refreshFailed(-1, nil)
- }
- guard http.statusCode == 200 else {
- let body = String(data: data, encoding: .utf8)
- throw SubscriptionError.refreshFailed(http.statusCode, body)
- }
- do {
- let decoded = try JSONDecoder().decode(TokenRefreshResponse.self, from: data)
- return decoded.accessToken
- } catch {
- throw SubscriptionError.decodeFailed(error)
- }
- }
-
- // MARK: - Usage fetch
-
- private static func fetchUsage(token: String) async throws -> UsageResponse {
- var request = URLRequest(url: usageURL)
- request.httpMethod = "GET"
- request.timeoutInterval = requestTimeout
- request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
- request.setValue("application/json", forHTTPHeaderField: "Accept")
- request.setValue(betaHeader, forHTTPHeaderField: "anthropic-beta")
- request.setValue(userAgent, forHTTPHeaderField: "User-Agent")
-
- let (data, response) = try await URLSession.shared.data(for: request)
- guard let http = response as? HTTPURLResponse else {
- throw SubscriptionError.usageFetchFailed(-1, nil)
- }
- guard http.statusCode == 200 else {
- let body = String(data: data, encoding: .utf8)
- throw SubscriptionError.usageFetchFailed(http.statusCode, body)
- }
- do {
- return try JSONDecoder().decode(UsageResponse.self, from: data)
- } catch {
- throw SubscriptionError.decodeFailed(error)
- }
- }
-
- // MARK: - Mapping
-
- private static func mapResponse(_ r: UsageResponse, rawTier: String?) -> SubscriptionUsage {
- SubscriptionUsage(
- tier: SubscriptionUsage.tier(from: rawTier),
- rawTier: rawTier,
- fiveHourPercent: r.fiveHour?.utilization,
- fiveHourResetsAt: parseDate(r.fiveHour?.resetsAt),
- sevenDayPercent: r.sevenDay?.utilization,
- sevenDayResetsAt: parseDate(r.sevenDay?.resetsAt),
- sevenDayOpusPercent: r.sevenDayOpus?.utilization,
- sevenDayOpusResetsAt: parseDate(r.sevenDayOpus?.resetsAt),
- sevenDaySonnetPercent: r.sevenDaySonnet?.utilization,
- sevenDaySonnetResetsAt: parseDate(r.sevenDaySonnet?.resetsAt),
- fetchedAt: Date()
- )
- }
-
- private static func parseDate(_ s: String?) -> Date? {
- guard let s, !s.isEmpty else { return nil }
- let f = ISO8601DateFormatter()
- f.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
- if let d = f.date(from: s) { return d }
- f.formatOptions = [.withInternetDateTime]
- return f.date(from: s)
- }
-}
-
-// MARK: - Internal models
-
-private struct StoredCredentials {
- let accessToken: String
- let refreshToken: String?
- let expiresAt: Date?
- let rateLimitTier: String?
-}
-
-private struct CredentialsRoot: Decodable {
- let claudeAiOauth: OAuthBlock?
-}
-
-private struct OAuthBlock: Decodable {
- let accessToken: String?
- let refreshToken: String?
- let expiresAt: Double?
- let rateLimitTier: String?
-}
-
-private struct TokenRefreshResponse: Decodable {
- let accessToken: String
- let refreshToken: String?
- let expiresIn: Int?
-
- enum CodingKeys: String, CodingKey {
- case accessToken = "access_token"
- case refreshToken = "refresh_token"
- case expiresIn = "expires_in"
- }
-}
-
-private struct UsageResponse: Decodable {
- let fiveHour: Window?
- let sevenDay: Window?
- let sevenDayOpus: Window?
- let sevenDaySonnet: Window?
-
- enum CodingKeys: String, CodingKey {
- case fiveHour = "five_hour"
- case sevenDay = "seven_day"
- case sevenDayOpus = "seven_day_opus"
- case sevenDaySonnet = "seven_day_sonnet"
- }
-}
-
-private struct Window: Decodable {
- let utilization: Double?
- let resetsAt: String?
-
- enum CodingKeys: String, CodingKey {
- case utilization
- case resetsAt = "resets_at"
- }
-}
diff --git a/mac/Sources/CodeBurnMenubar/Data/SubscriptionRefreshCadence.swift b/mac/Sources/CodeBurnMenubar/Data/SubscriptionRefreshCadence.swift
new file mode 100644
index 0000000..3701d25
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Data/SubscriptionRefreshCadence.swift
@@ -0,0 +1,42 @@
+import Foundation
+
+/// User-configurable cadence for /api/oauth/usage polling. Mirrors CodexBar's
+/// "manual / 1m / 2m / 5m / 15m" preset set so users on tight rate-limit
+/// budgets can dial it down and power users can dial it up. Stored as the raw
+/// number of seconds in UserDefaults; `manual = 0` means "never auto-refresh".
+enum SubscriptionRefreshCadence: Int, CaseIterable, Identifiable {
+ case manual = 0
+ case oneMinute = 60
+ case twoMinutes = 120
+ case fiveMinutes = 300
+ case fifteenMinutes = 900
+
+ var id: Int { rawValue }
+
+ var label: String {
+ switch self {
+ case .manual: return "Manual"
+ case .oneMinute: return "1 minute"
+ case .twoMinutes: return "2 minutes"
+ case .fiveMinutes: return "5 minutes"
+ case .fifteenMinutes: return "15 minutes"
+ }
+ }
+
+ static let defaultsKey = "codeburn.claude.refreshCadenceSeconds"
+ static let `default`: SubscriptionRefreshCadence = .twoMinutes
+
+ static var current: SubscriptionRefreshCadence {
+ get {
+ // UserDefaults.integer returns 0 when the key is missing — that
+ // happens to alias `manual`, which is wrong for a fresh install.
+ // Probe with object(forKey:) so we can distinguish "never set"
+ // from "set to manual" and seed the default on first run.
+ if UserDefaults.standard.object(forKey: defaultsKey) == nil {
+ return .default
+ }
+ return SubscriptionRefreshCadence(rawValue: UserDefaults.standard.integer(forKey: defaultsKey)) ?? .default
+ }
+ set { UserDefaults.standard.set(newValue.rawValue, forKey: defaultsKey) }
+ }
+}
diff --git a/mac/Sources/CodeBurnMenubar/Data/SubscriptionSnapshotStore.swift b/mac/Sources/CodeBurnMenubar/Data/SubscriptionSnapshotStore.swift
index 931154a..9357ee9 100644
--- a/mac/Sources/CodeBurnMenubar/Data/SubscriptionSnapshotStore.swift
+++ b/mac/Sources/CodeBurnMenubar/Data/SubscriptionSnapshotStore.swift
@@ -76,6 +76,13 @@ enum SubscriptionSnapshotStore {
/// Test seam: clear all snapshots.
static func resetForTesting() async {
+ await clearAll()
+ }
+
+ /// Wipe all snapshots from disk. Called when the user disconnects so the
+ /// "Based on last cycle" projections do not contaminate a reconnect under
+ /// a different account or tier.
+ static func clearAll() async {
await SnapshotLock.shared.run {
try? FileManager.default.removeItem(atPath: snapshotsPath())
}
diff --git a/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift b/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift
index ddf7dc4..5441794 100644
--- a/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift
+++ b/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift
@@ -1,10 +1,28 @@
import Foundation
import Observation
-private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/releases/latest"
+private let releasesAPI = "https://api.github.com/repos/getagentseal/codeburn/releases?per_page=20"
private let checkIntervalSeconds: TimeInterval = 2 * 24 * 60 * 60
private let lastCheckKey = "UpdateChecker.lastCheckDate"
private let cachedVersionKey = "UpdateChecker.latestVersion"
+private let updateTimeoutSeconds: UInt64 = 120
+private let maxUpdateStderrBytes = 64 * 1024
+
+private final class LockedDataBuffer: @unchecked Sendable {
+ private let lock = NSLock()
+ private var data = Data()
+
+ func append(_ chunk: Data, limit: Int) {
+ lock.withLock {
+ guard data.count < limit else { return }
+ data.append(Data(chunk.prefix(limit - data.count)))
+ }
+ }
+
+ func snapshot() -> Data {
+ lock.withLock { data }
+ }
+}
@MainActor
@Observable
@@ -16,14 +34,14 @@ final class UpdateChecker {
var updateAvailable: Bool {
guard let latest = latestVersion else { return false }
let current = currentVersion
- let normalizedLatest = latest.hasPrefix("v") ? String(latest.dropFirst()) : latest
- let normalizedCurrent = current.hasPrefix("v") ? String(current.dropFirst()) : current
+ let normalizedLatest = AppVersion.normalize(latest)
+ let normalizedCurrent = AppVersion.normalize(current)
guard !normalizedCurrent.isEmpty && normalizedCurrent != "dev" else { return false }
return normalizedLatest.compare(normalizedCurrent, options: .numeric) == .orderedDescending
}
var currentVersion: String {
- Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? ""
+ AppVersion.normalizedBundleShortVersion
}
func checkIfNeeded() async {
@@ -37,19 +55,24 @@ final class UpdateChecker {
}
func check() async {
+ updateError = nil
guard let url = URL(string: releasesAPI) else { return }
var request = URLRequest(url: url)
request.setValue("codeburn-menubar-updater", forHTTPHeaderField: "User-Agent")
request.setValue("application/vnd.github+json", forHTTPHeaderField: "Accept")
do {
- let (data, _) = try await URLSession.shared.data(for: request)
- let release = try JSONDecoder().decode(GitHubRelease.self, from: data)
- guard let asset = release.assets.first(where: {
- $0.name.hasPrefix("CodeBurnMenubar-") && $0.name.hasSuffix(".zip")
- }) else { return }
+ let (data, response) = try await URLSession.shared.data(for: request)
+ guard let http = response as? HTTPURLResponse, http.statusCode == 200 else {
+ let status = (response as? HTTPURLResponse)?.statusCode ?? -1
+ throw UpdateCheckError.http(status)
+ }
+ let releases = try JSONDecoder().decode([GitHubRelease].self, from: data)
+ guard let resolved = Self.resolveLatestMenubarRelease(in: releases) else {
+ throw UpdateCheckError.missingMenubarAsset
+ }
- let version = asset.name
+ let version = resolved.asset.name
.replacingOccurrences(of: "CodeBurnMenubar-", with: "")
.replacingOccurrences(of: ".zip", with: "")
@@ -57,28 +80,58 @@ final class UpdateChecker {
UserDefaults.standard.set(Date().timeIntervalSince1970, forKey: lastCheckKey)
UserDefaults.standard.set(version, forKey: cachedVersionKey)
} catch {
+ updateError = "Update check failed: \(error.localizedDescription)"
NSLog("CodeBurn: update check failed: \(error)")
}
}
+ nonisolated static func resolveLatestMenubarRelease(in releases: [GitHubRelease]) -> (release: GitHubRelease, asset: GitHubAsset)? {
+ for release in releases where release.tag_name.hasPrefix("mac-v") {
+ guard let asset = release.assets.first(where: {
+ $0.name.hasPrefix("CodeBurnMenubar-v") && $0.name.hasSuffix(".zip")
+ }) else { continue }
+ guard release.assets.contains(where: { $0.name == "\(asset.name).sha256" }) else { continue }
+ return (release, asset)
+ }
+ return nil
+ }
+
func performUpdate() {
isUpdating = true
updateError = nil
let process = CodeburnCLI.makeProcess(subcommand: ["menubar", "--force"])
let errPipe = Pipe()
+ let errBuffer = LockedDataBuffer()
process.standardOutput = FileHandle.nullDevice
process.standardError = errPipe
+ errPipe.fileHandleForReading.readabilityHandler = { handle in
+ let chunk = handle.availableData
+ guard !chunk.isEmpty else { return }
+ errBuffer.append(chunk, limit: maxUpdateStderrBytes)
+ }
+
+ let timeoutTask = Task.detached(priority: .utility) {
+ try? await Task.sleep(nanoseconds: updateTimeoutSeconds * 1_000_000_000)
+ if process.isRunning {
+ NSLog("CodeBurn: update subprocess timed out after %llus - terminating", updateTimeoutSeconds)
+ process.terminate()
+ }
+ }
process.terminationHandler = { [weak self] proc in
- let errData = errPipe.fileHandleForReading.readDataToEndOfFile()
- let stderr = String(data: errData, encoding: .utf8) ?? ""
+ timeoutTask.cancel()
+ errPipe.fileHandleForReading.readabilityHandler = nil
+ let stderrData = errBuffer.snapshot()
+ let stderr = Self.sanitizeForDisplay(String(data: stderrData, encoding: .utf8) ?? "")
Task { @MainActor in
guard let self else { return }
+ self.isUpdating = false
if proc.terminationStatus != 0 {
- self.isUpdating = false
self.updateError = stderr.isEmpty ? "Update failed (exit \(proc.terminationStatus))" : stderr
NSLog("CodeBurn: update failed (exit \(proc.terminationStatus)): \(stderr)")
+ } else {
+ self.latestVersion = nil
}
}
}
@@ -91,14 +144,41 @@ final class UpdateChecker {
NSLog("CodeBurn: update spawn failed: \(error)")
}
}
+
+ nonisolated private static func sanitizeForDisplay(_ value: String) -> String {
+ var cleaned = value.replacingOccurrences(of: "\u{0000}", with: "")
+ let patterns: [(String, String)] = [
+ (#"sk-ant-[A-Za-z0-9_-]+"#, "sk-ant-***"),
+ (#"sk-[A-Za-z0-9_-]{16,}"#, "sk-***"),
+ (#"eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+"#, "eyJ***"),
+ (#"(?i)Bearer\s+\S+"#, "Bearer ***"),
+ ]
+ for (pattern, replacement) in patterns {
+ cleaned = cleaned.replacingOccurrences(of: pattern, with: replacement, options: .regularExpression)
+ }
+ if cleaned.count > 1_000 { cleaned = String(cleaned.prefix(1_000)) + "..." }
+ return cleaned.trimmingCharacters(in: .whitespacesAndNewlines)
+ }
}
-private struct GitHubRelease: Decodable {
+enum UpdateCheckError: LocalizedError {
+ case http(Int)
+ case missingMenubarAsset
+
+ var errorDescription: String? {
+ switch self {
+ case let .http(status): "GitHub returned HTTP \(status)."
+ case .missingMenubarAsset: "No mac-v release with a menubar zip and checksum was found."
+ }
+ }
+}
+
+struct GitHubRelease: Decodable {
let tag_name: String
let assets: [GitHubAsset]
}
-private struct GitHubAsset: Decodable {
+struct GitHubAsset: Decodable {
let name: String
let browser_download_url: String
}
diff --git a/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift b/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift
index 4f4a5f8..83251de 100644
--- a/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift
+++ b/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift
@@ -13,20 +13,50 @@ enum CodeburnCLI {
/// PATH additions for GUI-launched apps, which otherwise get a minimal PATH that misses
/// Homebrew and npm global installs.
private static let additionalPathEntries = ["/opt/homebrew/bin", "/usr/local/bin"]
+ private static let persistedPathFilename = "codeburn-cli-path.v1"
/// Returns the argv that launches the CLI. Dev override via `CODEBURN_BIN` is honoured only
/// if every whitespace-delimited token passes `safeArgPattern`. Otherwise falls back to the
/// plain `codeburn` name (resolved via PATH).
static func baseArgv() -> [String] {
- guard let raw = ProcessInfo.processInfo.environment["CODEBURN_BIN"], !raw.isEmpty else {
- return ["codeburn"]
+ if ProcessInfo.processInfo.environment["CODEBURN_ALLOW_DEV_BIN"] == "1",
+ let raw = ProcessInfo.processInfo.environment["CODEBURN_BIN"],
+ !raw.isEmpty
+ {
+ let parts = raw.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
+ guard parts.allSatisfy(isSafe) else {
+ NSLog("CodeBurn: refusing unsafe CODEBURN_BIN; using installed codeburn")
+ return installedArgv()
+ }
+ return parts
}
- let parts = raw.split(separator: " ", omittingEmptySubsequences: true).map(String.init)
- guard parts.allSatisfy(isSafe) else {
- NSLog("CodeBurn: refusing unsafe CODEBURN_BIN; using default 'codeburn'")
- return ["codeburn"]
+
+ return installedArgv()
+ }
+
+ private static func installedArgv() -> [String] {
+ if let persisted = persistedCLIPath(), isSafe(persisted), FileManager.default.isExecutableFile(atPath: persisted) {
+ return [persisted]
}
- return parts
+ for candidate in additionalPathEntries.map({ "\($0)/codeburn" }) {
+ if FileManager.default.isExecutableFile(atPath: candidate) {
+ return [candidate]
+ }
+ }
+ return ["codeburn"]
+ }
+
+ private static func persistedCLIPath() -> String? {
+ let support = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first
+ ?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent("Library/Application Support")
+ let url = support
+ .appendingPathComponent("CodeBurn", isDirectory: true)
+ .appendingPathComponent(persistedPathFilename)
+ guard let value = try? String(contentsOf: url, encoding: .utf8).trimmingCharacters(in: .whitespacesAndNewlines),
+ !value.isEmpty,
+ value.hasPrefix("/")
+ else { return nil }
+ return value
}
/// Builds a `Process` that runs the CLI with the given subcommand args. Uses `/usr/bin/env`
diff --git a/mac/Sources/CodeBurnMenubar/Theme/ThemeState.swift b/mac/Sources/CodeBurnMenubar/Theme/ThemeState.swift
index a2cb7ee..9e0444e 100644
--- a/mac/Sources/CodeBurnMenubar/Theme/ThemeState.swift
+++ b/mac/Sources/CodeBurnMenubar/Theme/ThemeState.swift
@@ -1,4 +1,5 @@
import SwiftUI
+import Observation
enum AccentPreset: String, CaseIterable, Identifiable {
case ember = "Ember"
@@ -72,6 +73,7 @@ enum AccentPreset: String, CaseIterable, Identifiable {
}
@MainActor
+@Observable
final class ThemeState {
static let shared = ThemeState()
diff --git a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
index 77b6165..82f2ceb 100644
--- a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift
@@ -1,28 +1,111 @@
+import AppKit
import SwiftUI
+/// Shared state read by the NSEvent local monitor closure. The closure
+/// snapshots its captured environment at install time, so SwiftUI @State
+/// can't be used directly — a reference-type holder keeps the latest hover
+/// status visible to the monitor across SwiftUI updates.
+@MainActor
+final class AgentTabStripScrollState {
+ static let shared = AgentTabStripScrollState()
+ var isStripHovered: Bool = false
+}
+
struct AgentTabStrip: View {
@Environment(AppStore.self) private var store
+ @State private var stripViewportWidth: CGFloat = 0
+ @State private var stripContentWidth: CGFloat = 0
+ @State private var scrollWheelMonitor: Any?
var body: some View {
- ScrollView(.horizontal, showsIndicators: false) {
- HStack(spacing: 5) {
- ForEach(visibleFilters) { filter in
- Button {
- Task { await store.switchTo(provider: filter) }
- } label: {
- AgentTab(
- filter: filter,
- cost: cost(for: filter),
- isActive: store.selectedProvider == filter
+ GeometryReader { viewportGeo in
+ ScrollViewReader { proxy in
+ HStack(spacing: 4) {
+ if isOverflowing {
+ Button {
+ selectAdjacentProvider(direction: -1, proxy: proxy)
+ } label: {
+ Image(systemName: "chevron.left")
+ .font(.system(size: 10, weight: .semibold))
+ .frame(width: 18, height: 18)
+ }
+ .buttonStyle(.plain)
+ .foregroundStyle(canMoveBackward ? Color.primary : Color.secondary.opacity(0.35))
+ .disabled(!canMoveBackward)
+ .help("Show previous providers")
+ }
+
+ ScrollView(.horizontal, showsIndicators: false) {
+ HStack(spacing: 5) {
+ ForEach(visibleFilters) { filter in
+ AgentTab(
+ filter: filter,
+ cost: cost(for: filter),
+ isActive: store.selectedProvider == filter,
+ quota: store.quotaSummary(for: filter)
+ ) {
+ store.switchTo(provider: filter)
+ withAnimation(.easeInOut(duration: 0.18)) {
+ proxy.scrollTo(filter.id, anchor: .center)
+ }
+ }
+ .id(filter.id)
+ }
+ }
+ .background(
+ GeometryReader { contentGeo in
+ Color.clear
+ .onAppear {
+ stripContentWidth = contentGeo.size.width
+ }
+ .onChange(of: contentGeo.size.width) { _, newWidth in
+ stripContentWidth = newWidth
+ }
+ }
)
}
- .buttonStyle(.plain)
+ .padding(.horizontal, 12)
+ .padding(.top, 8)
+ .padding(.bottom, 4)
+ .onHover { hovering in
+ AgentTabStripScrollState.shared.isStripHovered = hovering
+ }
+
+ if isOverflowing {
+ Button {
+ selectAdjacentProvider(direction: 1, proxy: proxy)
+ } label: {
+ Image(systemName: "chevron.right")
+ .font(.system(size: 10, weight: .semibold))
+ .frame(width: 18, height: 18)
+ }
+ .buttonStyle(.plain)
+ .foregroundStyle(canMoveForward ? Color.primary : Color.secondary.opacity(0.35))
+ .disabled(!canMoveForward)
+ .help("Show next providers")
+ }
+ }
+ .onAppear {
+ stripViewportWidth = viewportGeo.size.width
+ installScrollWheelMonitorIfNeeded()
+ withAnimation(.easeInOut(duration: 0.18)) {
+ proxy.scrollTo(store.selectedProvider.id, anchor: .center)
+ }
+ }
+ .onChange(of: viewportGeo.size.width) { _, newWidth in
+ stripViewportWidth = newWidth
+ }
+ .onChange(of: store.selectedProvider) { _, newProvider in
+ withAnimation(.easeInOut(duration: 0.18)) {
+ proxy.scrollTo(newProvider.id, anchor: .center)
+ }
+ }
+ .onDisappear {
+ removeScrollWheelMonitorIfNeeded()
}
}
- .padding(.horizontal, 12)
- .padding(.top, 8)
- .padding(.bottom, 4)
}
+ .frame(height: 38)
}
private var todayAll: MenubarPayload {
@@ -46,6 +129,9 @@ struct AgentTabStrip: View {
private func cost(for filter: ProviderFilter) -> Double? {
let data = periodAll
if filter == .all { return data.current.cost }
+ if filter == store.selectedProvider, store.hasCachedData {
+ return store.payload.current.cost
+ }
let providers = Dictionary(
data.current.providers.map { ($0.key.lowercased(), $0.value) },
uniquingKeysWith: +
@@ -54,23 +140,100 @@ struct AgentTabStrip: View {
sum + (providers[key] ?? 0)
}
}
+
+ private var currentFilterIndex: Int {
+ visibleFilters.firstIndex(of: store.selectedProvider) ?? 0
+ }
+
+ private var canMoveBackward: Bool { currentFilterIndex > 0 }
+ private var canMoveForward: Bool { currentFilterIndex < visibleFilters.count - 1 }
+ private var isOverflowing: Bool { stripContentWidth > (stripViewportWidth - 30) }
+
+ private func selectAdjacentProvider(direction: Int, proxy: ScrollViewProxy) {
+ guard !visibleFilters.isEmpty else { return }
+ let targetIndex = min(max(currentFilterIndex + direction, 0), visibleFilters.count - 1)
+ let target = visibleFilters[targetIndex]
+ store.switchTo(provider: target)
+ withAnimation(.easeInOut(duration: 0.18)) {
+ proxy.scrollTo(target.id, anchor: .center)
+ }
+ }
+
+ /// Standard mouse wheels emit vertical-only scroll deltas, which a horizontal
+ /// `ScrollView` ignores. While the cursor is over the strip we transpose
+ /// vertical-axis scroll fields onto the horizontal axis so the underlying
+ /// NSScrollView receives a real horizontal delta. Trackpad events (precise
+ /// deltas, with native horizontal component) are passed through untouched
+ /// so vertical scrolling elsewhere in the popover is unaffected.
+ private func installScrollWheelMonitorIfNeeded() {
+ guard scrollWheelMonitor == nil else { return }
+ scrollWheelMonitor = NSEvent.addLocalMonitorForEvents(matching: .scrollWheel) { event in
+ guard AgentTabStripScrollState.shared.isStripHovered,
+ !event.hasPreciseScrollingDeltas,
+ abs(event.scrollingDeltaX) < 0.001,
+ abs(event.scrollingDeltaY) > 0,
+ let cg = event.cgEvent?.copy() else {
+ return event
+ }
+ let lineDeltaY = cg.getIntegerValueField(.scrollWheelEventDeltaAxis1)
+ let pointDeltaY = cg.getDoubleValueField(.scrollWheelEventPointDeltaAxis1)
+ let fixedDeltaY = cg.getDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1)
+ cg.setIntegerValueField(.scrollWheelEventDeltaAxis1, value: 0)
+ cg.setDoubleValueField(.scrollWheelEventPointDeltaAxis1, value: 0)
+ cg.setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis1, value: 0)
+ cg.setIntegerValueField(.scrollWheelEventDeltaAxis2, value: lineDeltaY)
+ cg.setDoubleValueField(.scrollWheelEventPointDeltaAxis2, value: pointDeltaY)
+ cg.setDoubleValueField(.scrollWheelEventFixedPtDeltaAxis2, value: fixedDeltaY)
+ return NSEvent(cgEvent: cg) ?? event
+ }
+ }
+
+ private func removeScrollWheelMonitorIfNeeded() {
+ if let monitor = scrollWheelMonitor {
+ NSEvent.removeMonitor(monitor)
+ scrollWheelMonitor = nil
+ }
+ }
}
private struct AgentTab: View {
let filter: ProviderFilter
let cost: Double?
let isActive: Bool
+ let quota: QuotaSummary?
+ let onTap: () -> Void
+
+ @State private var hoverPopoverShown = false
+ @State private var hoverEnterTask: DispatchWorkItem?
+ @State private var hoverExitTask: DispatchWorkItem?
+ @State private var clickDismissed = false
+
+ /// Providers whose AgentTab chip reserves a 3pt bar slot underneath the
+ /// label, even when not yet connected. Driven by which providers we
+ /// actually implement live-quota fetching for in AppStore.quotaSummary.
+ static func providerSupportsQuota(_ filter: ProviderFilter) -> Bool {
+ switch filter {
+ case .claude, .codex: return true
+ default: return false
+ }
+ }
var body: some View {
- HStack(spacing: 5) {
- Text(filter.rawValue)
- .font(.system(size: 11.5, weight: .medium))
- .tracking(-0.05)
- if let cost, cost > 0 {
- Text(cost.asCompactCurrency())
- .font(.codeMono(size: 10.5, weight: .medium))
- .foregroundStyle(isActive ? AnyShapeStyle(.white.opacity(0.8)) : AnyShapeStyle(.secondary))
- .tracking(-0.2)
+ VStack(spacing: 3) {
+ HStack(spacing: 5) {
+ Text(filter.rawValue)
+ .font(.system(size: 11.5, weight: .medium))
+ .tracking(-0.05)
+ if let cost, cost > 0 {
+ Text(cost.asCompactCurrency())
+ .font(.codeMono(size: 10.5, weight: .medium))
+ .foregroundStyle(isActive ? AnyShapeStyle(.white.opacity(0.8)) : AnyShapeStyle(.secondary))
+ .tracking(-0.2)
+ }
+ }
+ if quota != nil {
+ AgentTabQuotaBar(quota: quota, isActive: isActive)
+ .frame(height: 3)
}
}
.padding(.horizontal, 10)
@@ -81,6 +244,233 @@ private struct AgentTab: View {
)
.foregroundStyle(isActive ? AnyShapeStyle(.white) : AnyShapeStyle(.secondary))
.contentShape(Rectangle())
+ .onTapGesture {
+ hoverPopoverShown = false
+ hoverEnterTask?.cancel()
+ clickDismissed = true
+ onTap()
+ }
+ .onHover { hovering in
+ hoverEnterTask?.cancel()
+ hoverExitTask?.cancel()
+ if !hovering {
+ clickDismissed = false
+ let task = DispatchWorkItem { hoverPopoverShown = false }
+ hoverExitTask = task
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.15, execute: task)
+ } else if !clickDismissed, quota != nil {
+ let task = DispatchWorkItem { hoverPopoverShown = true }
+ hoverEnterTask = task
+ DispatchQueue.main.asyncAfter(deadline: .now() + 0.25, execute: task)
+ }
+ }
+ .popover(isPresented: $hoverPopoverShown) {
+ if let quota {
+ QuotaDetailPopover(quota: quota)
+ }
+ }
+ }
+}
+
+/// Thin progress bar drawn inside an AgentTab chip when that provider has a live quota
+/// source. Width matches the chip; color shifts green → amber → red at 70% / 90%.
+private struct AgentTabQuotaBar: View {
+ let quota: QuotaSummary?
+ let isActive: Bool
+
+ var body: some View {
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ Capsule()
+ .fill(trackColor)
+ if let percent = filledFraction {
+ Capsule()
+ .fill(barColor)
+ .frame(width: max(2, geo.size.width * CGFloat(percent)))
+ .animation(.easeOut(duration: 0.25), value: percent)
+ }
+ if case .terminalFailure = quota?.connection {
+ // Hatched/red strip to telegraph "broken; reconnect needed".
+ Capsule()
+ .fill(Color.red.opacity(0.7))
+ }
+ }
+ }
+ }
+
+ private var filledFraction: Double? {
+ guard let pct = quota?.primary?.percent else { return nil }
+ return min(max(pct, 0), 1)
+ }
+
+ private var barColor: Color {
+ guard let pct = quota?.primary?.percent else { return .clear }
+ switch QuotaSummary.severity(for: pct) {
+ case .normal: return isActive ? Color.white : Color.green.opacity(0.85)
+ case .warning: return Color.yellow
+ case .critical: return Color.orange
+ case .danger: return Color.red
+ }
+ }
+
+ private var trackColor: Color {
+ isActive ? Color.white.opacity(0.20) : Color.secondary.opacity(0.18)
+ }
+}
+
+private struct QuotaDetailPopover: View {
+ let quota: QuotaSummary
+
+ var body: some View {
+ VStack(alignment: .leading, spacing: 8) {
+ switch quota.connection {
+ case .terminalFailure(let reason):
+ terminalFailureCard(reason: reason)
+ case .disconnected:
+ Text(disconnectedMessage)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ case .loading where quota.details.isEmpty:
+ Text("Loading…")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ default:
+ rowsCard
+ }
+ }
+ .padding(12)
+ .frame(width: 260)
+ }
+
+ private var disconnectedMessage: String {
+ switch quota.providerFilter {
+ case .codex: return "Sign in with `codex` (ChatGPT mode) to track quota."
+ case .claude: return "Sign in to Claude Code to track quota."
+ default: return "Sign in to track quota."
+ }
+ }
+
+ private var rowsCard: some View {
+ VStack(alignment: .leading, spacing: 6) {
+ HStack(spacing: 6) {
+ Text("\(quota.providerFilter.rawValue) usage")
+ .font(.system(size: 11, weight: .semibold))
+ if case .stale = quota.connection {
+ Text("stale")
+ .font(.system(size: 9.5))
+ .foregroundStyle(.secondary)
+ } else if case .transientFailure = quota.connection {
+ Text("retrying")
+ .font(.system(size: 9.5))
+ .foregroundStyle(.orange)
+ }
+ Spacer()
+ if let plan = quota.planLabel, !plan.isEmpty {
+ Text(plan)
+ .font(.system(size: 9.5, weight: .medium))
+ .foregroundStyle(.secondary)
+ .lineLimit(1)
+ .truncationMode(.tail)
+ .padding(.horizontal, 6)
+ .padding(.vertical, 2)
+ .background(
+ RoundedRectangle(cornerRadius: 4)
+ .fill(Color.secondary.opacity(0.12))
+ )
+ // Size to content. Plan names are bounded short strings
+ // ("Max 20x", "Pro Lite", "Free Workspace"); a forced
+ // maxWidth was making short labels look stretched.
+ .fixedSize(horizontal: true, vertical: false)
+ }
+ }
+ ForEach(Array(quota.details.enumerated()), id: \.offset) { _, w in
+ QuotaDetailRow(window: w)
+ }
+ if !quota.footerLines.isEmpty {
+ Divider()
+ .padding(.top, 2)
+ ForEach(Array(quota.footerLines.enumerated()), id: \.offset) { _, line in
+ Text(line)
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ }
+ }
+ }
+ }
+
+ private func terminalFailureCard(reason: String?) -> some View {
+ VStack(alignment: .leading, spacing: 6) {
+ Text(reconnectTitle)
+ .font(.system(size: 11.5, weight: .semibold))
+ .foregroundStyle(.red)
+ Text(reason ?? defaultReconnectReason)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ Text(reconnectInstruction)
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ }
+ }
+
+ private var reconnectTitle: String {
+ switch quota.providerFilter {
+ case .codex: return "Reconnect Codex"
+ default: return "Reconnect Claude"
+ }
+ }
+
+ private var defaultReconnectReason: String {
+ switch quota.providerFilter {
+ case .codex: return "Refresh token rejected by OpenAI."
+ default: return "Refresh token rejected by Anthropic."
+ }
+ }
+
+ private var reconnectInstruction: String {
+ switch quota.providerFilter {
+ case .codex: return "Run `codex login` in your terminal, then click Reconnect."
+ default: return "Open Claude Code in your terminal and type `/login`, then click Reconnect."
+ }
+ }
+}
+
+private struct QuotaDetailRow: View {
+ let window: QuotaSummary.Window
+
+ var body: some View {
+ HStack(spacing: 8) {
+ Text(window.label)
+ .font(.system(size: 10.5))
+ .frame(width: 92, alignment: .leading)
+ GeometryReader { geo in
+ ZStack(alignment: .leading) {
+ Capsule().fill(Color.secondary.opacity(0.18))
+ Capsule()
+ .fill(barColor)
+ .frame(width: max(2, geo.size.width * CGFloat(min(max(window.percent, 0), 1))))
+ }
+ }
+ .frame(height: 4)
+ Text(window.percentLabel)
+ .font(.codeMono(size: 10.5, weight: .medium))
+ .frame(width: 36, alignment: .trailing)
+ if !window.resetsInLabel.isEmpty {
+ Text(window.resetsInLabel)
+ .font(.codeMono(size: 10))
+ .foregroundStyle(.secondary)
+ .frame(width: 50, alignment: .trailing)
+ }
+ }
+ }
+
+ private var barColor: Color {
+ switch QuotaSummary.severity(for: window.percent) {
+ case .normal: return Color.green.opacity(0.85)
+ case .warning: return Color.yellow
+ case .critical: return Color.orange
+ case .danger: return Color.red
+ }
}
}
@@ -89,19 +479,26 @@ extension ProviderFilter {
switch self {
case .all: return Theme.brandAccent
case .claude: return Theme.categoricalClaude
+ case .cline: return Color(red: 0x23/255.0, green: 0x8A/255.0, blue: 0x7E/255.0)
case .codex: return Theme.categoricalCodex
case .cursor: return Theme.categoricalCursor
+ case .cursorAgent: return Color(red: 0x4E/255.0, green: 0xC9/255.0, blue: 0xB0/255.0)
case .copilot: return Color(red: 0x6D/255.0, green: 0x8F/255.0, blue: 0xA6/255.0)
case .droid: return Color(red: 0x7C/255.0, green: 0x3A/255.0, blue: 0xED/255.0)
case .gemini: return Color(red: 0x44/255.0, green: 0x85/255.0, blue: 0xF4/255.0)
+ case .ibmBob: return Color(red: 0x0F/255.0, green: 0x62/255.0, blue: 0xFE/255.0)
case .kiloCode: return Color(red: 0x00/255.0, green: 0x96/255.0, blue: 0x88/255.0)
case .kiro: return Color(red: 0x4A/255.0, green: 0x9E/255.0, blue: 0xC4/255.0)
+ case .kimi: return Color(red: 0xA4/255.0, green: 0xC6/255.0, blue: 0x39/255.0)
case .openclaw: return Color(red: 0xDA/255.0, green: 0x70/255.0, blue: 0x56/255.0)
case .opencode: return Color(red: 0x5B/255.0, green: 0x83/255.0, blue: 0x5B/255.0)
case .pi: return Color(red: 0xB2/255.0, green: 0x6B/255.0, blue: 0x3D/255.0)
case .qwen: return Color(red: 0x61/255.0, green: 0x5E/255.0, blue: 0xEB/255.0)
case .omp: return Color(red: 0x8B/255.0, green: 0x5C/255.0, blue: 0xB0/255.0)
case .rooCode: return Color(red: 0x4C/255.0, green: 0xAF/255.0, blue: 0x50/255.0)
+ case .crush: return Color(red: 0xE0/255.0, green: 0x6C/255.0, blue: 0x9F/255.0)
+ case .antigravity: return Color(red: 0xFF/255.0, green: 0x7A/255.0, blue: 0x45/255.0)
+ case .goose: return Color(red: 0xB7/255.0, green: 0x8D/255.0, blue: 0x52/255.0)
}
}
}
diff --git a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift
index 86f174c..aff1e83 100644
--- a/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/FindingsSection.swift
@@ -213,11 +213,11 @@ private struct HistoryStats {
private func computeHistoryStats(history: [DailyHistoryEntry]) -> HistoryStats {
var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = TimeZone(identifier: "UTC")!
+ calendar.timeZone = .current
let formatter: DateFormatter = {
let f = DateFormatter()
f.dateFormat = "yyyy-MM-dd"
- f.timeZone = TimeZone(identifier: "UTC")
+ f.timeZone = .current
return f
}()
let now = Date()
diff --git a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift
index 5b143b2..3374bd9 100644
--- a/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/HeatmapSection.swift
@@ -5,6 +5,36 @@ private let trendBarWidth: CGFloat = 13
private let trendBarGap: CGFloat = 4
private let trendChartHeight: CGFloat = 90
+// Cached formatters and a calendar to avoid allocating fresh ones on every
+// SwiftUI body re-eval. Hover scrubbing on the trend bars triggers many
+// re-evals per second; a fresh DateFormatter / Calendar each time was a
+// measurable hot spot.
+private let yyyymmdd: DateFormatter = {
+ let f = DateFormatter()
+ f.dateFormat = "yyyy-MM-dd"
+ f.timeZone = .current
+ return f
+}()
+
+private let prettyDayFormat: DateFormatter = {
+ let f = DateFormatter()
+ f.dateFormat = "EEE MMM d"
+ return f
+}()
+
+private let mmmDayFormat: DateFormatter = {
+ let f = DateFormatter()
+ f.dateFormat = "MMM d"
+ f.timeZone = .current
+ return f
+}()
+
+private let gregorianCalendar: Calendar = {
+ var c = Calendar(identifier: .gregorian)
+ c.timeZone = .current
+ return c
+}()
+
/// Three switchable insight visualizations: Calendar (this month), Forecast (burn rate),
/// Pulse (efficiency KPIs). Pills at top toggle between them.
struct HeatmapSection: View {
@@ -25,10 +55,14 @@ struct HeatmapSection: View {
}
private var visibleModes: [InsightMode] {
- // Plan sources from Claude's OAuth usage endpoint, so it only makes sense when the
- // Claude provider tab is selected. Hidden on All/Cursor/Codex/etc.
+ // Plan sources from a provider's OAuth usage endpoint. Currently
+ // implemented for Claude (Anthropic) and Codex (ChatGPT). Hidden on
+ // All / Cursor / Droid / Gemini / Copilot until those providers ship
+ // their own quota data sources.
InsightMode.allCases.filter { mode in
- if mode == .plan { return store.selectedProvider == .claude }
+ if mode == .plan {
+ return store.selectedProvider == .claude || store.selectedProvider == .codex
+ }
return true
}
}
@@ -42,7 +76,12 @@ struct HeatmapSection: View {
@ViewBuilder
private var content: some View {
switch store.selectedInsight {
- case .plan: PlanInsight(usage: store.subscription)
+ case .plan:
+ if store.selectedProvider == .codex {
+ CodexPlanInsight()
+ } else {
+ PlanInsight(usage: store.subscription)
+ }
case .trend: TrendInsight(days: store.payload.history.daily)
case .forecast: ForecastInsight(days: store.payload.history.daily)
case .pulse: PulseInsight(payload: store.payload)
@@ -342,13 +381,8 @@ private struct BarTooltipCard: View {
}
private func prettyDate(_ ymd: String) -> String {
- let parser = DateFormatter()
- parser.dateFormat = "yyyy-MM-dd"
- parser.timeZone = .current
- guard let date = parser.date(from: ymd) else { return ymd }
- let display = DateFormatter()
- display.dateFormat = "EEE MMM d"
- return display.string(from: date)
+ guard let date = yyyymmdd.date(from: ymd) else { return ymd }
+ return prettyDayFormat.string(from: date)
}
private struct MiniStat: View {
@@ -370,7 +404,7 @@ private struct MiniStat: View {
}
private struct TrendBar: Identifiable {
- let id = UUID()
+ var id: String { date }
let date: String
let cost: Double
let inputTokens: Double
@@ -391,14 +425,8 @@ private struct TrendStats {
}
private func buildTrendBars(from days: [DailyHistoryEntry]) -> [TrendBar] {
- var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = .current
- let formatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "yyyy-MM-dd"
- f.timeZone = .current
- return f
- }()
+ let calendar = gregorianCalendar
+ let formatter = yyyymmdd
let entryByDate = Dictionary(days.map { ($0.date, $0) }, uniquingKeysWith: { _, new in new })
let today = calendar.startOfDay(for: Date())
let todayKey = formatter.string(from: today)
@@ -426,14 +454,8 @@ private func computeTrendStats(bars: [TrendBar], allDays: [DailyHistoryEntry]) -
let avg = bars.isEmpty ? 0 : total / Double(bars.count)
let peak = bars.filter { $0.cost > 0 }.max(by: { $0.cost < $1.cost })
- var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = .current
- let formatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "yyyy-MM-dd"
- f.timeZone = .current
- return f
- }()
+ let calendar = gregorianCalendar
+ let formatter = yyyymmdd
let today = calendar.startOfDay(for: Date())
let priorWindowStart = calendar.date(byAdding: .day, value: -(2 * trendDays - 1), to: today)
let thisWindowStart = calendar.date(byAdding: .day, value: -(trendDays - 1), to: today)
@@ -515,7 +537,7 @@ private struct ForecastInsight: View {
guard previous > 0 else { return "no prior month" }
let diff = ((projection - previous) / previous) * 100
let sign = diff >= 0 ? "+" : ""
- return "\(sign)\(String(format: "%.0f", diff))% vs last month ($\(String(format: "%.0f", previous)))"
+ return "\(sign)\(String(format: "%.0f", diff))% vs last month (\(previous.asCompactCurrency()))"
}
}
@@ -546,14 +568,8 @@ private struct ForecastStats {
}
private func computeForecast(days: [DailyHistoryEntry]) -> ForecastStats {
- var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = .current
- let formatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "yyyy-MM-dd"
- f.timeZone = .current
- return f
- }()
+ let calendar = gregorianCalendar
+ let formatter = yyyymmdd
let now = Date()
let comps = calendar.dateComponents([.year, .month, .day], from: now)
guard
@@ -793,24 +809,13 @@ private struct AllStats {
let historyDayCount: Int
}
-private func computeAllStats(payload: MenubarPayload) -> AllStats {
+@MainActor private func computeAllStats(payload: MenubarPayload) -> AllStats {
let history = payload.history.daily
let favoriteModel = payload.current.topModels.first?.name ?? "—"
- var calendar = Calendar(identifier: .gregorian)
- calendar.timeZone = .current
- let formatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "yyyy-MM-dd"
- f.timeZone = .current
- return f
- }()
- let displayFormatter: DateFormatter = {
- let f = DateFormatter()
- f.dateFormat = "MMM d"
- f.timeZone = .current
- return f
- }()
+ let calendar = gregorianCalendar
+ let formatter = yyyymmdd
+ let displayFormatter = mmmDayFormat
let now = Date()
let today = calendar.startOfDay(for: now)
@@ -848,13 +853,21 @@ private func computeAllStats(payload: MenubarPayload) -> AllStats {
var longestStreak = 0
var running = 0
- let sortedDates = history.map(\.date).sorted()
- for date in sortedDates {
- if (costByDate[date] ?? 0) > 0 {
- running += 1
- longestStreak = max(longestStreak, running)
- } else {
- running = 0
+ if let firstDate = history.map(\.date).min(),
+ let lastDate = history.map(\.date).max(),
+ let start = formatter.date(from: firstDate),
+ let end = formatter.date(from: lastDate) {
+ var cursor = start
+ while cursor <= end {
+ let key = formatter.string(from: cursor)
+ if (costByDate[key] ?? 0) > 0 {
+ running += 1
+ longestStreak = max(longestStreak, running)
+ } else {
+ running = 0
+ }
+ guard let next = calendar.date(byAdding: .day, value: 1, to: cursor) else { break }
+ cursor = next
}
}
@@ -887,28 +900,36 @@ private struct PlanInsight: View {
var body: some View {
Group {
switch store.subscriptionLoadState {
- case .idle:
- PlanIdleView()
- case .loading:
+ case .notBootstrapped:
+ PlanConnectView { Task { await store.bootstrapSubscription() } }
+ case .bootstrapping:
PlanLoadingView()
+ case .loading:
+ if let usage {
+ loadedBody(usage: usage)
+ } else {
+ PlanLoadingView()
+ }
case .noCredentials:
PlanNoCredentialsView()
case .failed:
PlanFailedView(error: store.subscriptionError)
+ case .transientFailure:
+ if let usage {
+ loadedBody(usage: usage)
+ } else {
+ PlanFailedView(error: store.subscriptionError ?? "Anthropic temporarily unreachable — retrying.")
+ }
+ case let .terminalFailure(reason):
+ PlanReconnectView(reason: reason) { Task { await store.bootstrapSubscription() } }
case .loaded:
if let usage {
loadedBody(usage: usage)
} else {
- PlanNoCredentialsView()
+ PlanLoadingView()
}
}
}
- .task {
- // Lazy-trigger fetch the first time Plan is opened.
- if store.subscriptionLoadState == .idle {
- await store.refreshSubscription()
- }
- }
}
@ViewBuilder
@@ -1006,26 +1027,6 @@ private struct PlanInsight: View {
// MARK: - Plan empty/loading/failure states
-private struct PlanIdleView: View {
- var body: some View {
- VStack(spacing: 8) {
- Image(systemName: "person.crop.circle.dashed")
- .font(.system(size: 22))
- .foregroundStyle(.tertiary)
- Text("Loading your plan...")
- .font(.system(size: 11.5, weight: .medium))
- .foregroundStyle(.secondary)
- Text("macOS may ask permission to read your Claude Code credentials.")
- .font(.system(size: 10))
- .foregroundStyle(.tertiary)
- .multilineTextAlignment(.center)
- .frame(maxWidth: 260)
- }
- .frame(maxWidth: .infinity)
- .padding(.vertical, 16)
- }
-}
-
private struct PlanLoadingView: View {
var body: some View {
VStack(spacing: 8) {
@@ -1043,27 +1044,27 @@ private struct PlanNoCredentialsView: View {
@Environment(AppStore.self) private var store
var body: some View {
- VStack(spacing: 8) {
+ VStack(spacing: 10) {
Image(systemName: "key.slash")
- .font(.system(size: 20))
+ .font(.system(size: 24))
.foregroundStyle(.tertiary)
- Text("No Claude subscription connected")
+ Text("No Claude credentials found")
.font(.system(size: 12, weight: .semibold))
.foregroundStyle(.primary)
- Text("Sign in with Claude Code, then click Retry.")
+ Text("Sign in with Claude Code first: open `claude` in your terminal and type `/login`. Then click Try Again.")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
- .frame(maxWidth: 260)
- Button("Retry") {
- Task { await store.refreshSubscription() }
+ .frame(maxWidth: 280)
+ Button("Try Again") {
+ Task { await store.bootstrapSubscription() }
}
.controlSize(.small)
.buttonStyle(.borderedProminent)
.tint(Theme.brandAccent)
}
.frame(maxWidth: .infinity)
- .padding(.vertical, 14)
+ .padding(.vertical, 16)
}
}
@@ -1099,6 +1100,175 @@ private struct PlanFailedView: View {
}
}
+/// Shown the very first time a user opens the Plan tab. Clicking Connect is the
+/// only path to triggering the macOS keychain prompt for Claude Code credentials —
+/// the menubar app does not touch the keychain at startup.
+private struct PlanConnectView: View {
+ let onConnect: () -> Void
+
+ var body: some View {
+ VStack(spacing: 10) {
+ Image(systemName: "link.circle")
+ .font(.system(size: 26))
+ .foregroundStyle(Theme.brandAccent)
+ Text("Connect Claude subscription")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundStyle(.primary)
+ Text("CodeBurn will read your Claude Code credentials once. macOS will ask permission. After that, the live quota bar shows next to the Claude tab and updates automatically.")
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 280)
+ Button("Connect", action: onConnect)
+ .controlSize(.small)
+ .buttonStyle(.borderedProminent)
+ .tint(Theme.brandAccent)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 18)
+ }
+}
+
+/// Shown when the refresh token has been invalidated (typically because the user
+/// re-authenticated on another device). Clicking the button re-runs bootstrap,
+/// which reads Claude's credentials source again and writes a fresh copy to our
+/// own keychain item.
+private struct PlanReconnectView: View {
+ let reason: String?
+ let onReconnect: () -> Void
+
+ var body: some View {
+ VStack(spacing: 10) {
+ Image(systemName: "arrow.triangle.2.circlepath.circle")
+ .font(.system(size: 24))
+ .foregroundStyle(.red)
+ Text("Reconnect Claude")
+ .font(.system(size: 12, weight: .semibold))
+ .foregroundStyle(.primary)
+ Text(reason ?? "Your Claude session has expired. Open Claude Code in your terminal and type `/login`, then click Reconnect.")
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 280)
+ .lineLimit(3)
+ Button("Reconnect", action: onReconnect)
+ .controlSize(.small)
+ .buttonStyle(.borderedProminent)
+ .tint(.red)
+ }
+ .frame(maxWidth: .infinity)
+ .padding(.vertical, 16)
+ }
+}
+
+/// Plan tab for Codex. Mirrors PlanInsight's layout but reads from
+/// store.codexUsage / store.codexLoadState. We deliberately skip the
+/// "On pace at reset" projection here — that math is fed by local
+/// per-message Claude spend extrapolated against the API quota windows;
+/// our local Codex spend isn't an apples-to-apples signal for the
+/// ChatGPT-subscription rate windows reported by wham/usage. Add when
+/// we wire a comparable extrapolator.
+private struct CodexPlanInsight: View {
+ @Environment(AppStore.self) private var store
+
+ var body: some View {
+ Group {
+ switch store.codexLoadState {
+ case .notBootstrapped:
+ PlanConnectView { Task { await store.bootstrapCodex() } }
+ case .bootstrapping:
+ PlanLoadingView()
+ case .loading:
+ if let usage = store.codexUsage {
+ loadedBody(usage: usage)
+ } else {
+ PlanLoadingView()
+ }
+ case .noCredentials:
+ PlanNoCredentialsView()
+ case .failed:
+ PlanFailedView(error: store.codexError)
+ case .transientFailure:
+ if let usage = store.codexUsage {
+ loadedBody(usage: usage)
+ } else {
+ PlanFailedView(error: store.codexError ?? "ChatGPT temporarily unreachable — retrying.")
+ }
+ case let .terminalFailure(reason):
+ PlanReconnectView(reason: reason) { Task { await store.bootstrapCodex() } }
+ case .loaded:
+ if let usage = store.codexUsage {
+ loadedBody(usage: usage)
+ } else {
+ PlanLoadingView()
+ }
+ }
+ }
+ }
+
+ @ViewBuilder
+ private func loadedBody(usage: CodexUsage) -> some View {
+ VStack(alignment: .leading, spacing: 10) {
+ HStack(alignment: .firstTextBaseline) {
+ Text(usage.plan.displayName)
+ .font(.system(size: 13, weight: .semibold))
+ .foregroundStyle(.primary)
+ Spacer()
+ if let resetsAt = (usage.primary ?? usage.secondary)?.resetsAt {
+ Text("Resets \(relativeReset(resetsAt))")
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ }
+ }
+ if let primary = usage.primary {
+ UtilizationRow(
+ label: "\(primary.windowLabel) window",
+ percent: primary.usedPercent,
+ resetsAt: primary.resetsAt,
+ projection: nil
+ )
+ }
+ if let secondary = usage.secondary {
+ UtilizationRow(
+ label: "\(secondary.windowLabel) window",
+ percent: secondary.usedPercent,
+ resetsAt: secondary.resetsAt,
+ projection: nil
+ )
+ }
+ // Surface non-zero per-model rate limits (Codex Spark, etc.) so
+ // power users see them; idle ones stay collapsed.
+ ForEach(Array(usage.additionalLimits.enumerated()), id: \.offset) { _, limit in
+ if let p = limit.primary, p.usedPercent > 0 {
+ UtilizationRow(
+ label: "\(limit.name) · \(p.windowLabel)",
+ percent: p.usedPercent,
+ resetsAt: p.resetsAt,
+ projection: nil
+ )
+ }
+ if let s = limit.secondary, s.usedPercent > 0 {
+ UtilizationRow(
+ label: "\(limit.name) · \(s.windowLabel)",
+ percent: s.usedPercent,
+ resetsAt: s.resetsAt,
+ projection: nil
+ )
+ }
+ }
+ }
+ .padding(.horizontal, 14)
+ .padding(.top, 4)
+ .padding(.bottom, 8)
+ }
+
+ private func relativeReset(_ date: Date) -> String {
+ let f = RelativeDateTimeFormatter()
+ f.unitsStyle = .short
+ return f.localizedString(for: date, relativeTo: Date())
+ }
+}
+
private struct WindowProjection {
enum Source { case linear, historicalBaseline }
let percent: Double
diff --git a/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift
index 44879d8..7bad14b 100644
--- a/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift
@@ -41,9 +41,30 @@ struct MenuBarContent: View {
}
}
- if store.isLoading {
- BurnLoadingOverlay(periodLabel: store.selectedPeriod.rawValue)
+ // Overlay fires only on cold cache for the current key. This
+ // avoids the 1-frame `$0.00` flash on first-time period/provider
+ // switches. When the fetch fails (CLI subprocess timeout, parse
+ // error, etc.), surface a retry card instead of leaving the
+ // user stuck on a perpetual "Loading..." spinner.
+ if !store.hasCachedData {
+ if store.isCurrentKeyLoading || !store.hasAttemptedCurrentKeyLoad {
+ BurnLoadingOverlay(periodLabel: store.selectedPeriod.rawValue)
+ .transition(.opacity)
+ } else if let err = store.lastError {
+ FetchErrorOverlay(
+ error: err,
+ periodLabel: store.selectedPeriod.rawValue,
+ retry: { Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) } }
+ )
.transition(.opacity)
+ } else {
+ FetchErrorOverlay(
+ error: "The last refresh stopped before returning data. CodeBurn will keep retrying, or you can retry now.",
+ periodLabel: store.selectedPeriod.rawValue,
+ retry: { Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) } }
+ )
+ .transition(.opacity)
+ }
}
}
.frame(height: 520)
@@ -55,20 +76,34 @@ struct MenuBarContent: View {
StarBanner()
}
- .id(store.accentPreset)
}
- /// True when a specific provider tab is selected and that provider has no spend in the
- /// currently selected period. The .all tab is exempt -- it always shows aggregated data.
private var isFilteredEmpty: Bool {
guard store.selectedProvider != .all else { return false }
- return store.payload.current.cost <= 0 && store.payload.current.calls == 0
+ if store.payload.current.cost > 0 || store.payload.current.calls > 0 { return false }
+ if providerHasCostInAllPayload { return false }
+ return true
+ }
+
+ private var providerHasCostInAllPayload: Bool {
+ guard let allPayload = store.periodAllPayload else { return false }
+ let providers = Dictionary(
+ allPayload.current.providers.map { ($0.key.lowercased(), $0.value) },
+ uniquingKeysWith: +
+ )
+ return store.selectedProvider.providerKeys.contains { key in
+ (providers[key] ?? 0) > 0
+ }
}
/// Show the tab row whenever the CLI detected at least one AI coding tool installed
/// on this machine. Hidden only when nothing is detected, which means there's
/// nothing to filter by anyway.
private var showAgentTabs: Bool {
+ // Sticky: once any cached payload has reported providers, keep the tab strip
+ // visible. Without this, the strip disappears for one frame on a period
+ // switch when the new key's payload is still empty.
+ if store.hasAnyProvidersInCache { return true }
let payload = store.todayPayload ?? store.payload
return !payload.current.providers.isEmpty
}
@@ -99,11 +134,54 @@ private struct EmptyProviderState: View {
case .sevenDays: "the last 7 days"
case .thirtyDays: "the last 30 days"
case .month: "this month"
- case .all: "all time"
+ case .all: "the last 6 months"
}
}
}
+/// Shown when a fetch failed and the cache is still empty for this key. The
+/// user previously sat on the "Loading…" spinner forever — the popover had
+/// no path to recover beyond the next 30s tick (which would just re-fail).
+/// Now they see what broke and can retry directly.
+private struct FetchErrorOverlay: View {
+ let error: String
+ let periodLabel: String
+ let retry: () -> Void
+
+ var body: some View {
+ ZStack {
+ Rectangle().fill(.ultraThinMaterial)
+ VStack(spacing: 12) {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 28))
+ .foregroundStyle(Theme.brandAccent)
+ Text("Couldn't load \(periodLabel)")
+ .font(.system(size: 12.5, weight: .semibold))
+ .foregroundStyle(.primary)
+ Text(displayError)
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ .multilineTextAlignment(.center)
+ .frame(maxWidth: 280)
+ .lineLimit(3)
+ Button("Retry", action: retry)
+ .buttonStyle(.borderedProminent)
+ .tint(Theme.brandAccent)
+ .controlSize(.small)
+ }
+ .padding(.horizontal, 20)
+ }
+ }
+
+ /// Strip the leading subprocess noise that creeps into NSError descriptions
+ /// so the visible message is the actual cause, not the framework wrapper.
+ private var displayError: String {
+ let trimmed = error.trimmingCharacters(in: .whitespacesAndNewlines)
+ if trimmed.count <= 240 { return trimmed }
+ return String(trimmed.prefix(240)) + "…"
+ }
+}
+
/// Translucent overlay that blurs whatever's behind it (the previous tab/period content)
/// and centers an animated burning flame -- the brand mark filling up bottom-to-top in
/// yellow→orange→red, looping.
@@ -185,24 +263,31 @@ private struct BurnFlame: View {
private struct Header: View {
@Environment(UpdateChecker.self) private var updateChecker
+ @Environment(AppStore.self) private var store
var body: some View {
- HStack {
- VStack(alignment: .leading, spacing: 1) {
- (
- Text("Code").foregroundStyle(.primary)
- + Text("Burn").foregroundStyle(Theme.brandEmber)
- )
- .font(.system(size: 13, weight: .semibold))
- .tracking(-0.15)
- Text("AI Coding Cost Tracker")
- .font(.system(size: 10.5))
- .foregroundStyle(.secondary)
+ VStack(alignment: .leading, spacing: 6) {
+ HStack {
+ VStack(alignment: .leading, spacing: 1) {
+ (
+ Text("Code").foregroundStyle(.primary)
+ + Text("Burn").foregroundStyle(Theme.brandEmber)
+ )
+ .font(.system(size: 13, weight: .semibold))
+ .tracking(-0.15)
+ Text("AI Coding Cost Tracker")
+ .font(.system(size: 10.5))
+ .foregroundStyle(.secondary)
+ }
+ Spacer()
+ if updateChecker.updateAvailable || updateChecker.updateError != nil {
+ UpdateBadge()
+ }
+ AccentPicker()
}
- Spacer()
- if updateChecker.updateAvailable {
- UpdateBadge()
- }
- AccentPicker()
+ // Compact warning row when any connected provider crosses 70%.
+ // Lists all warning providers with their worst-window percent so
+ // the user knows whether to slow down on Claude, Codex, or both.
+ QuotaWarningRow(status: store.aggregateQuotaStatus)
}
.padding(.horizontal, 14)
.padding(.top, 10)
@@ -210,6 +295,61 @@ private struct Header: View {
}
}
+private struct QuotaWarningRow: View {
+ let status: AppStore.AggregateQuotaStatus
+
+ var body: some View {
+ if !status.warnings.isEmpty {
+ HStack(spacing: 6) {
+ Image(systemName: severityIcon)
+ .font(.system(size: 10, weight: .semibold))
+ .foregroundStyle(severityColor)
+ Text(message)
+ .font(.system(size: 10.5, weight: .medium))
+ .foregroundStyle(severityColor)
+ Spacer(minLength: 0)
+ }
+ .padding(.horizontal, 8)
+ .padding(.vertical, 5)
+ .background(
+ RoundedRectangle(cornerRadius: 5)
+ .fill(severityColor.opacity(0.12))
+ )
+ }
+ }
+
+ private var message: String {
+ let parts = status.warnings.map { "\($0.name) \(Int($0.percent.rounded()))%" }
+ if parts.count == 1 {
+ // Reads "Claude over limit (105%)" when any provider exceeds the
+ // quota cap, instead of the awkward "Claude 105% of quota used".
+ if case .danger = status.severity {
+ return "\(status.warnings[0].name) over limit (\(Int(status.warnings[0].percent.rounded()))%)"
+ }
+ return "\(parts[0]) of quota used"
+ }
+ return parts.joined(separator: " · ")
+ }
+
+ private var severityColor: Color {
+ switch status.severity {
+ case .normal: return .secondary
+ case .warning: return .yellow
+ case .critical: return .orange
+ case .danger: return .red
+ }
+ }
+
+ private var severityIcon: String {
+ switch status.severity {
+ case .normal: return "info.circle"
+ case .warning: return "exclamationmark.circle"
+ case .critical: return "exclamationmark.triangle"
+ case .danger: return "octagon"
+ }
+ }
+}
+
private struct AccentPicker: View {
@Environment(AppStore.self) private var store
@@ -269,18 +409,25 @@ private struct UpdateBadge: View {
var body: some View {
Button {
- updateChecker.performUpdate()
+ if updateChecker.updateAvailable {
+ updateChecker.performUpdate()
+ } else {
+ Task { await updateChecker.check() }
+ }
} label: {
HStack(spacing: 4) {
if updateChecker.isUpdating {
ProgressView()
.controlSize(.mini)
.scaleEffect(0.7)
+ } else if updateChecker.updateError != nil {
+ Image(systemName: "exclamationmark.triangle.fill")
+ .font(.system(size: 10))
} else {
Image(systemName: "arrow.down.circle.fill")
.font(.system(size: 10))
}
- Text(updateChecker.isUpdating ? "Updating..." : "Update")
+ Text(updateChecker.isUpdating ? "Updating..." : (updateChecker.updateError == nil ? "Update" : "Failed"))
.font(.system(size: 10, weight: .medium))
}
.padding(.horizontal, 8)
@@ -290,6 +437,7 @@ private struct UpdateBadge: View {
.tint(Theme.brandAccent)
.controlSize(.mini)
.disabled(updateChecker.isUpdating)
+ .help(updateChecker.updateError ?? "Install the latest menubar build")
}
}
@@ -397,7 +545,7 @@ struct FooterBar: View {
.fixedSize()
Button {
- Task { await store.refresh(includeOptimize: true, force: true) }
+ refreshNow()
} label: {
Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise")
.font(.system(size: 11, weight: .medium))
@@ -422,7 +570,7 @@ struct FooterBar: View {
Spacer()
- Text("v\(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "?")")
+ Text(AppVersion.displayBundleShortVersion)
.font(.system(size: 10, weight: .regular, design: .monospaced))
.foregroundStyle(.tertiary)
@@ -443,6 +591,14 @@ struct FooterBar: View {
TerminalLauncher.open(subcommand: ["report"])
}
+ private func refreshNow() {
+ if let delegate = NSApp.delegate as? AppDelegate {
+ delegate.refreshSubscriptionNow()
+ } else {
+ Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) }
+ }
+ }
+
private enum ExportFormat {
case csv, json
var cliName: String { self == .csv ? "csv" : "json" }
@@ -457,7 +613,7 @@ struct FooterBar: View {
Task {
let downloads = (NSHomeDirectory() as NSString).appendingPathComponent("Downloads")
let formatter = DateFormatter()
- formatter.dateFormat = "yyyy-MM-dd"
+ formatter.dateFormat = "yyyy-MM-dd-HHmmss"
let base = "codeburn-\(formatter.string(from: Date()))"
let outputPath = (downloads as NSString).appendingPathComponent(base + format.suffix)
@@ -466,13 +622,17 @@ struct FooterBar: View {
])
do {
- try process.run()
- process.waitUntilExit()
- if process.terminationStatus == 0 {
- NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: outputPath)])
- } else {
- NSLog("CodeBurn: \(format.cliName.uppercased()) export exited with status \(process.terminationStatus)")
+ let fmt = format
+ process.terminationHandler = { proc in
+ Task { @MainActor in
+ if proc.terminationStatus == 0 {
+ NSWorkspace.shared.activateFileViewerSelecting([URL(fileURLWithPath: outputPath)])
+ } else {
+ NSLog("CodeBurn: \(fmt.cliName.uppercased()) export exited with status \(proc.terminationStatus)")
+ }
+ }
}
+ try process.run()
} catch {
NSLog("CodeBurn: \(format.cliName.uppercased()) export failed: \(error)")
}
@@ -483,21 +643,18 @@ struct FooterBar: View {
/// thread right away so the UI redraws the next frame, then fetches a fresh rate in the
/// background. CLI config is persisted so other codeburn commands stay in sync.
private func applyCurrency(code: String) {
- store.currency = code
let symbol = CurrencyState.symbolForCode(code)
Task {
let cached = await FXRateCache.shared.cachedRate(for: code)
- await MainActor.run {
+ if let cached {
+ store.currency = code
CurrencyState.shared.apply(code: code, rate: cached, symbol: symbol)
}
let fresh = await FXRateCache.shared.rate(for: code)
- if let fresh, fresh != cached {
- await MainActor.run {
- CurrencyState.shared.apply(code: code, rate: fresh, symbol: symbol)
- }
- }
+ store.currency = code
+ CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol)
}
CLICurrencyConfig.persist(code: code)
diff --git a/mac/Sources/CodeBurnMenubar/Views/PeriodSegmentedControl.swift b/mac/Sources/CodeBurnMenubar/Views/PeriodSegmentedControl.swift
index a636932..065e363 100644
--- a/mac/Sources/CodeBurnMenubar/Views/PeriodSegmentedControl.swift
+++ b/mac/Sources/CodeBurnMenubar/Views/PeriodSegmentedControl.swift
@@ -7,7 +7,7 @@ struct PeriodSegmentedControl: View {
HStack(spacing: 1) {
ForEach(Period.allCases) { period in
Button {
- Task { await store.switchTo(period: period) }
+ store.switchTo(period: period)
} label: {
Text(period.rawValue)
.font(.system(size: 11, weight: .medium))
diff --git a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift
new file mode 100644
index 0000000..a317380
--- /dev/null
+++ b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift
@@ -0,0 +1,365 @@
+import SwiftUI
+
+/// macOS-standard tabbed Settings window. New per-provider sections (Codex,
+/// Cursor, Copilot, etc.) plug in as additional tabs. Each tab owns its own
+/// concerns; this top-level view only hosts the TabView shell.
+struct SettingsView: View {
+ @Environment(AppStore.self) private var store
+
+ var body: some View {
+ TabView {
+ GeneralSettingsTab()
+ .tabItem { Label("General", systemImage: "gearshape") }
+
+ ClaudeSettingsTab()
+ .tabItem { Label("Claude", systemImage: "brain") }
+
+ CodexSettingsTab()
+ .tabItem { Label("Codex", systemImage: "chevron.left.forwardslash.chevron.right") }
+
+ AboutSettingsTab()
+ .tabItem { Label("About", systemImage: "info.circle") }
+ }
+ .frame(width: 520, height: 400)
+ }
+}
+
+// MARK: - General
+
+private struct GeneralSettingsTab: View {
+ @Environment(AppStore.self) private var store
+
+ var body: some View {
+ Form {
+ Section("Display") {
+ Picker("Currency", selection: Binding(
+ get: { store.currency },
+ set: { applyCurrency(code: $0) }
+ )) {
+ ForEach(["USD", "EUR", "GBP", "INR", "JPY", "AUD", "CAD"], id: \.self) { code in
+ Text(code).tag(code)
+ }
+ }
+ Picker("Accent", selection: Binding(
+ get: { store.accentPreset },
+ set: { store.accentPreset = $0 }
+ )) {
+ ForEach(AccentPreset.allCases) { preset in
+ Text(preset.rawValue).tag(preset)
+ }
+ }
+ }
+ }
+ .formStyle(.grouped)
+ .padding()
+ }
+
+ private func applyCurrency(code: String) {
+ let symbol = CurrencyState.symbolForCode(code)
+ Task {
+ let cached = await FXRateCache.shared.cachedRate(for: code)
+ if let cached {
+ store.currency = code
+ CurrencyState.shared.apply(code: code, rate: cached, symbol: symbol)
+ }
+ let fresh = await FXRateCache.shared.rate(for: code)
+ store.currency = code
+ CurrencyState.shared.apply(code: code, rate: fresh ?? cached, symbol: symbol)
+ }
+ CLICurrencyConfig.persist(code: code)
+ }
+}
+
+// MARK: - Claude
+
+private struct ClaudeSettingsTab: View {
+ @Environment(AppStore.self) private var store
+
+ var body: some View {
+ Form {
+ Section("Connection") {
+ ClaudeConnectionRow()
+ }
+ Section("Quota Refresh") {
+ Picker("Update every", selection: Binding(
+ get: { SubscriptionRefreshCadence.current },
+ set: { SubscriptionRefreshCadence.current = $0 }
+ )) {
+ ForEach(SubscriptionRefreshCadence.allCases) { cadence in
+ Text(cadence.label).tag(cadence)
+ }
+ }
+ .pickerStyle(.menu)
+ Text("Anthropic rate-limits this endpoint per account. 2 minutes is plenty for the 5-hour and weekly windows; pick Manual if you only want updates on demand.")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ Button("Refresh Now") {
+ Task { await store.refreshSubscription() }
+ }
+ }
+ }
+ .formStyle(.grouped)
+ .padding()
+ }
+}
+
+private struct ClaudeConnectionRow: View {
+ @Environment(AppStore.self) private var store
+ @State private var showDisconnectConfirm = false
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName: stateIcon)
+ .font(.system(size: 18))
+ .foregroundStyle(stateTint)
+ .frame(width: 22)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(stateTitle)
+ .font(.system(size: 12, weight: .semibold))
+ Text(stateDetail)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ }
+ Spacer()
+ actionButton
+ }
+ .padding(.vertical, 4)
+ }
+
+ private var stateIcon: String {
+ switch store.subscriptionLoadState {
+ case .loaded: return "checkmark.circle.fill"
+ case .terminalFailure: return "exclamationmark.triangle.fill"
+ case .transientFailure: return "clock.arrow.circlepath"
+ case .bootstrapping, .loading: return "ellipsis.circle"
+ case .notBootstrapped, .noCredentials: return "link.circle"
+ case .failed: return "xmark.circle"
+ }
+ }
+
+ private var stateTint: Color {
+ switch store.subscriptionLoadState {
+ case .loaded: return .green
+ case .terminalFailure, .failed: return .red
+ case .transientFailure: return .orange
+ default: return .secondary
+ }
+ }
+
+ private var stateTitle: String {
+ switch store.subscriptionLoadState {
+ case .loaded: return "Connected"
+ case let .terminalFailure(reason): return reason ?? "Reconnect required"
+ case .transientFailure: return "Backing off"
+ case .bootstrapping: return "Connecting…"
+ case .loading: return "Refreshing…"
+ case .notBootstrapped, .noCredentials: return "Not connected"
+ case .failed: return "Couldn't load plan data"
+ }
+ }
+
+ private var stateDetail: String {
+ switch store.subscriptionLoadState {
+ case .loaded:
+ if let tier = store.subscription?.tier.displayName {
+ return "Plan: \(tier)"
+ }
+ return "Live quota tracked from Anthropic."
+ case .terminalFailure: return "Open Claude Code in your terminal and type `/login`, then click Reconnect."
+ case .transientFailure: return store.subscriptionError ?? "Anthropic rate-limited; auto-retrying."
+ case .bootstrapping: return "macOS may ask permission to read your credentials."
+ case .loading: return "Background refresh in progress."
+ case .notBootstrapped, .noCredentials: return "Click Connect to read your Claude Code credentials and start tracking quota."
+ case .failed: return store.subscriptionError ?? ""
+ }
+ }
+
+ @ViewBuilder
+ private var actionButton: some View {
+ switch store.subscriptionLoadState {
+ case .loaded, .transientFailure, .loading:
+ Button("Disconnect") { showDisconnectConfirm = true }
+ .confirmationDialog(
+ "Disconnect Claude?",
+ isPresented: $showDisconnectConfirm
+ ) {
+ Button("Disconnect", role: .destructive) {
+ store.disconnectSubscription()
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("CodeBurn will stop tracking quota and delete its local copy of your Claude credentials. Your Claude Code keychain entry is untouched — Claude Code keeps working.")
+ }
+ case .terminalFailure, .noCredentials, .failed:
+ Button("Reconnect") { Task { await store.bootstrapSubscription() } }
+ .buttonStyle(.borderedProminent)
+ case .notBootstrapped:
+ Button("Connect") { Task { await store.bootstrapSubscription() } }
+ .buttonStyle(.borderedProminent)
+ case .bootstrapping:
+ ProgressView().controlSize(.small)
+ }
+ }
+}
+
+// MARK: - Codex
+
+private struct CodexSettingsTab: View {
+ @Environment(AppStore.self) private var store
+
+ var body: some View {
+ Form {
+ Section("Connection") {
+ CodexConnectionRow()
+ }
+ Section {
+ Text("Codex live-quota tracking reads `~/.codex/auth.json` once on Connect, then keeps a local copy under Application Support so subsequent quota fetches don't re-read the original. Only ChatGPT-mode auth (Plus / Pro / Team / Business) is supported — API-key users are billed per request and have a different reporting surface.")
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ } header: {
+ Text("How it works")
+ }
+ }
+ .formStyle(.grouped)
+ .padding()
+ }
+}
+
+private struct CodexConnectionRow: View {
+ @Environment(AppStore.self) private var store
+ @State private var showDisconnectConfirm = false
+
+ var body: some View {
+ HStack(alignment: .top, spacing: 12) {
+ Image(systemName: stateIcon)
+ .font(.system(size: 18))
+ .foregroundStyle(stateTint)
+ .frame(width: 22)
+ VStack(alignment: .leading, spacing: 2) {
+ Text(stateTitle)
+ .font(.system(size: 12, weight: .semibold))
+ Text(stateDetail)
+ .font(.system(size: 11))
+ .foregroundStyle(.secondary)
+ .lineLimit(2)
+ }
+ Spacer()
+ actionButton
+ }
+ .padding(.vertical, 4)
+ }
+
+ private var stateIcon: String {
+ switch store.codexLoadState {
+ case .loaded: return "checkmark.circle.fill"
+ case .terminalFailure: return "exclamationmark.triangle.fill"
+ case .transientFailure: return "clock.arrow.circlepath"
+ case .bootstrapping, .loading: return "ellipsis.circle"
+ case .notBootstrapped, .noCredentials: return "link.circle"
+ case .failed: return "xmark.circle"
+ }
+ }
+
+ private var stateTint: Color {
+ switch store.codexLoadState {
+ case .loaded: return .green
+ case .terminalFailure, .failed: return .red
+ case .transientFailure: return .orange
+ default: return .secondary
+ }
+ }
+
+ private var stateTitle: String {
+ switch store.codexLoadState {
+ case .loaded: return "Connected"
+ case let .terminalFailure(reason): return reason ?? "Reconnect required"
+ case .transientFailure: return "Backing off"
+ case .bootstrapping: return "Connecting…"
+ case .loading: return "Refreshing…"
+ case .notBootstrapped, .noCredentials: return "Not connected"
+ case .failed: return "Couldn't load Codex quota"
+ }
+ }
+
+ private var stateDetail: String {
+ switch store.codexLoadState {
+ case .loaded:
+ if let plan = store.codexUsage?.plan.displayName {
+ return "Plan: \(plan)"
+ }
+ return "Live quota tracked from chatgpt.com."
+ case .terminalFailure:
+ // Be specific about the cause: the message we already surface in
+ // codexError will say "API-key mode" if that's the situation, so
+ // the generic "run codex login" hint covers both cases.
+ if let err = store.codexError, err.lowercased().contains("api-key") {
+ return "Codex is in API-key mode. Run `codex login` and choose a ChatGPT plan to enable quota tracking."
+ }
+ return "Run `codex login` in your terminal to sign in again, then click Reconnect."
+ case .transientFailure: return store.codexError ?? "ChatGPT rate-limited; auto-retrying."
+ case .bootstrapping: return "Reading ~/.codex/auth.json."
+ case .loading: return "Background refresh in progress."
+ case .notBootstrapped, .noCredentials:
+ return "Click Connect to read your Codex CLI credentials. If Connect fails, run `codex login` in your terminal first to create ~/.codex/auth.json."
+ case .failed: return store.codexError ?? ""
+ }
+ }
+
+ @ViewBuilder
+ private var actionButton: some View {
+ switch store.codexLoadState {
+ case .loaded, .transientFailure, .loading:
+ Button("Disconnect") { showDisconnectConfirm = true }
+ .confirmationDialog(
+ "Disconnect Codex?",
+ isPresented: $showDisconnectConfirm
+ ) {
+ Button("Disconnect", role: .destructive) {
+ store.disconnectCodex()
+ }
+ Button("Cancel", role: .cancel) {}
+ } message: {
+ Text("CodeBurn will stop tracking quota and delete its local copy of your Codex credentials. Your ~/.codex/auth.json is untouched — Codex CLI keeps working.")
+ }
+ case .terminalFailure, .noCredentials, .failed:
+ Button("Reconnect") { Task { await store.bootstrapCodex() } }
+ .buttonStyle(.borderedProminent)
+ case .notBootstrapped:
+ Button("Connect") { Task { await store.bootstrapCodex() } }
+ .buttonStyle(.borderedProminent)
+ case .bootstrapping:
+ ProgressView().controlSize(.small)
+ }
+ }
+}
+
+// MARK: - About
+
+private struct AboutSettingsTab: View {
+ private let appVersion: String = AppVersion.normalizedBundleShortVersion
+ private let buildVersion: String = AppVersion.normalizedBundleBuildVersion
+
+ var body: some View {
+ VStack(spacing: 14) {
+ Image(systemName: "flame.fill")
+ .font(.system(size: 40))
+ .foregroundStyle(Theme.brandAccent)
+ Text("CodeBurn")
+ .font(.system(size: 18, weight: .semibold))
+ Text("AI Coding Cost Tracker")
+ .font(.system(size: 12))
+ .foregroundStyle(.secondary)
+ Text("Version \(appVersion) (\(buildVersion))")
+ .font(.codeMono(size: 11))
+ .foregroundStyle(.secondary)
+ HStack(spacing: 10) {
+ Link("GitHub", destination: URL(string: "https://github.com/getagentseal/codeburn")!)
+ Link("Issues", destination: URL(string: "https://github.com/getagentseal/codeburn/issues")!)
+ }
+ .font(.system(size: 12))
+ }
+ .frame(maxWidth: .infinity, maxHeight: .infinity)
+ .padding()
+ }
+}
diff --git a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift
new file mode 100644
index 0000000..fd75fec
--- /dev/null
+++ b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift
@@ -0,0 +1,84 @@
+import Foundation
+import Testing
+@testable import CodeBurnMenubar
+
+private func menubarPayload(cost: Double) -> MenubarPayload {
+ MenubarPayload(
+ generated: "test",
+ current: CurrentBlock(
+ label: "Today",
+ cost: cost,
+ calls: 1,
+ sessions: 1,
+ oneShotRate: nil,
+ inputTokens: 1,
+ outputTokens: 1,
+ cacheHitPercent: 0,
+ topActivities: [],
+ topModels: [],
+ providers: ["claude": cost]
+ ),
+ optimize: OptimizeBlock(findingCount: 0, savingsUSD: 0, topFindings: []),
+ history: HistoryBlock(daily: [])
+ )
+}
+
+@Suite("AppStore refresh recovery")
+@MainActor
+struct AppStoreRefreshRecoveryTests {
+ @Test("stale visible payload triggers hard recovery without clearing cache")
+ func stalePayloadTriggersHardRecoveryWithoutClearingCache() {
+ let store = AppStore()
+ store.setCachedPayloadForTesting(
+ menubarPayload(cost: 92.33),
+ period: .today,
+ provider: .all,
+ fetchedAt: Date().addingTimeInterval(-180)
+ )
+
+ #expect(store.todayPayload?.current.cost == 92.33)
+ #expect(store.needsInteractivePayloadRefresh)
+ #expect(store.needsStatusPayloadRefresh)
+ #expect(store.hasStaleInteractivePayload)
+ #expect(store.shouldResetInteractiveRefreshPipeline)
+
+ store.resetRefreshState(clearCache: false)
+
+ #expect(store.todayPayload?.current.cost == 92.33)
+ }
+
+ @Test("fresh visible payload does not trigger hard recovery")
+ func freshPayloadDoesNotTriggerHardRecovery() {
+ let store = AppStore()
+ store.setCachedPayloadForTesting(
+ menubarPayload(cost: 164.06),
+ period: .today,
+ provider: .all,
+ fetchedAt: Date()
+ )
+
+ #expect(!store.needsInteractivePayloadRefresh)
+ #expect(!store.needsStatusPayloadRefresh)
+ #expect(!store.hasStaleInteractivePayload)
+ #expect(!store.shouldResetInteractiveRefreshPipeline)
+ }
+
+ @Test("missing today status payload needs status refresh")
+ func missingTodayStatusPayloadNeedsStatusRefresh() {
+ let store = AppStore()
+
+ #expect(store.todayPayload == nil)
+ #expect(store.needsStatusPayloadRefresh)
+ }
+
+ @Test("missing unattempted payload triggers hard recovery")
+ func missingUnattemptedPayloadTriggersHardRecovery() {
+ let store = AppStore()
+
+ #expect(!store.hasCachedData)
+ #expect(!store.hasAttemptedCurrentKeyLoad)
+ #expect(store.needsInteractivePayloadRefresh)
+ #expect(store.hasMissingInteractivePayloadWithoutAttempt)
+ #expect(store.shouldResetInteractiveRefreshPipeline)
+ }
+}
diff --git a/mac/Tests/CodeBurnMenubarTests/AppVersionTests.swift b/mac/Tests/CodeBurnMenubarTests/AppVersionTests.swift
new file mode 100644
index 0000000..898f5e0
--- /dev/null
+++ b/mac/Tests/CodeBurnMenubarTests/AppVersionTests.swift
@@ -0,0 +1,19 @@
+import Testing
+@testable import CodeBurnMenubar
+
+@Suite("AppVersion")
+struct AppVersionTests {
+ @Test("display avoids duplicate v prefix")
+ func displayAvoidsDuplicatePrefix() {
+ #expect(AppVersion.display("0.9.8") == "v0.9.8")
+ #expect(AppVersion.display("v0.9.8") == "v0.9.8")
+ #expect(AppVersion.display("mac-v0.9.8") == "v0.9.8")
+ }
+
+ @Test("bundle metadata stores unprefixed semver")
+ func normalizeBundleVersion() {
+ #expect(AppVersion.normalize("v0.9.8") == "0.9.8")
+ #expect(AppVersion.normalize("mac-v0.9.8") == "0.9.8")
+ #expect(AppVersion.normalize("dev") == "dev")
+ }
+}
diff --git a/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift b/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift
new file mode 100644
index 0000000..44f52b5
--- /dev/null
+++ b/mac/Tests/CodeBurnMenubarTests/UpdateCheckerTests.swift
@@ -0,0 +1,39 @@
+import Testing
+@testable import CodeBurnMenubar
+
+@Suite("UpdateChecker")
+struct UpdateCheckerTests {
+ @Test("selects newest mac release with zip and checksum")
+ func selectsNewestMacReleaseWithChecksum() {
+ let releases = [
+ GitHubRelease(
+ tag_name: "v0.9.9",
+ assets: [GitHubAsset(name: "codeburn-0.9.9.tgz", browser_download_url: "https://example.test/cli")]
+ ),
+ GitHubRelease(
+ tag_name: "mac-v0.9.8",
+ assets: [
+ GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip", browser_download_url: "https://example.test/app"),
+ GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip.sha256", browser_download_url: "https://example.test/app.sha256"),
+ ]
+ ),
+ ]
+
+ let resolved = UpdateChecker.resolveLatestMenubarRelease(in: releases)
+
+ #expect(resolved?.release.tag_name == "mac-v0.9.8")
+ #expect(resolved?.asset.name == "CodeBurnMenubar-v0.9.8.zip")
+ }
+
+ @Test("ignores mac release missing checksum")
+ func ignoresMacReleaseMissingChecksum() {
+ let releases = [
+ GitHubRelease(
+ tag_name: "mac-v0.9.8",
+ assets: [GitHubAsset(name: "CodeBurnMenubar-v0.9.8.zip", browser_download_url: "https://example.test/app")]
+ ),
+ ]
+
+ #expect(UpdateChecker.resolveLatestMenubarRelease(in: releases) == nil)
+ }
+}
diff --git a/package-lock.json b/package-lock.json
index 7077b1b..ad40db2 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,18 +1,19 @@
{
"name": "codeburn",
- "version": "0.9.4",
+ "version": "0.9.9",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "codeburn",
- "version": "0.9.4",
+ "version": "0.9.9",
"license": "MIT",
"dependencies": {
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
- "react": "^19.2.5"
+ "react": "^19.2.5",
+ "strip-ansi": "^7.2.0"
},
"bin": {
"codeburn": "dist/cli.js"
@@ -26,7 +27,7 @@
"vitest": "^3.1.0"
},
"engines": {
- "node": ">=22"
+ "node": ">=22.13.0"
}
},
"node_modules/@alcalzone/ansi-tokenize": {
diff --git a/package.json b/package.json
index 73a7c38..243c768 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "codeburn",
- "version": "0.9.4",
+ "version": "0.9.9",
"description": "See where your AI coding tokens go - by task, tool, model, and project",
"type": "module",
"main": "./dist/cli.js",
@@ -12,7 +12,7 @@
],
"scripts": {
"bundle-litellm": "node scripts/bundle-litellm.mjs",
- "build": "node scripts/bundle-litellm.mjs && tsup",
+ "build": "node scripts/bundle-litellm.mjs && tsup && node -e \"const fs=require('fs'); fs.copyFileSync('src/cli.ts','dist/cli.js'); fs.chmodSync('dist/cli.js',0o755)\"",
"dev": "tsx src/cli.ts",
"test": "vitest",
"prepublishOnly": "npm run build"
@@ -21,6 +21,8 @@
"claude-code",
"cursor",
"codex",
+ "kimi",
+ "ibm-bob",
"opencode",
"pi",
"codebuff",
@@ -31,7 +33,7 @@
"developer-tools"
],
"engines": {
- "node": ">=22"
+ "node": ">=22.13.0"
},
"author": "AgentSeal ",
"license": "MIT",
@@ -47,7 +49,8 @@
"chalk": "^5.4.1",
"commander": "^13.1.0",
"ink": "^7.0.0",
- "react": "^19.2.5"
+ "react": "^19.2.5",
+ "strip-ansi": "^7.2.0"
},
"devDependencies": {
"@types/node": "^22.19.17",
diff --git a/src/bash-utils.ts b/src/bash-utils.ts
index c578972..2e5fe0d 100644
--- a/src/bash-utils.ts
+++ b/src/bash-utils.ts
@@ -1,12 +1,14 @@
import { basename } from 'path'
+import stripAnsi from 'strip-ansi'
function stripQuotedStrings(command: string): string {
return command.replace(/"[^"]*"|'[^']*'/g, match => ' '.repeat(match.length))
}
-export function extractBashCommands(command: string): string[] {
- if (!command || !command.trim()) return []
+export function extractBashCommands(rawCommand: string): string[] {
+ if (!rawCommand || !rawCommand.trim()) return []
+ const command = stripAnsi(rawCommand)
const stripped = stripQuotedStrings(command)
const separatorRegex = /\s*(?:&&|;|\|)\s*/g
diff --git a/src/classifier.ts b/src/classifier.ts
index 33b52b2..9a5de49 100644
--- a/src/classifier.ts
+++ b/src/classifier.ts
@@ -53,6 +53,10 @@ function getAllTools(turn: ParsedTurn): string[] {
return turn.assistantCalls.flatMap(c => c.tools)
}
+function getAllSkills(turn: ParsedTurn): string[] {
+ return turn.assistantCalls.flatMap(c => c.skills ?? [])
+}
+
function classifyByToolPattern(turn: ParsedTurn): TaskCategory | null {
const tools = getAllTools(turn)
if (tools.length === 0) return null
@@ -89,12 +93,38 @@ function classifyByToolPattern(turn: ParsedTurn): TaskCategory | null {
return null
}
+/// Picks the category whose keyword pattern matches earliest in the message.
+/// On a tie (same start index) the candidate listed first in `candidates` wins,
+/// so callers control tie-break priority by ordering. Returns null when no
+/// pattern matches. The first-match heuristic fixes the long-standing problem
+/// where "add error handling" was tagged Debugging because the DEBUG regex was
+/// checked before FEATURE; now FEATURE wins because "add" appears before
+/// "error". Issue #196.
+function firstMatchingCategory(
+ text: string,
+ candidates: ReadonlyArray<{ regex: RegExp; category: TaskCategory }>,
+): TaskCategory | null {
+ let best: { index: number; order: number; category: TaskCategory } | null = null
+ for (let i = 0; i < candidates.length; i++) {
+ const c = candidates[i]!
+ const m = c.regex.exec(text)
+ if (!m) continue
+ if (!best || m.index < best.index || (m.index === best.index && i < best.order)) {
+ best = { index: m.index, order: i, category: c.category }
+ }
+ }
+ return best?.category ?? null
+}
+
function refineByKeywords(category: TaskCategory, userMessage: string): TaskCategory {
if (category === 'coding') {
- if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
- if (REFACTOR_KEYWORDS.test(userMessage)) return 'refactoring'
- if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
- return 'coding'
+ // Tie-break order (when two keywords match at the same index): refactoring
+ // first because its words are the most specific, then feature, then debug.
+ return firstMatchingCategory(userMessage, [
+ { regex: REFACTOR_KEYWORDS, category: 'refactoring' },
+ { regex: FEATURE_KEYWORDS, category: 'feature' },
+ { regex: DEBUG_KEYWORDS, category: 'debugging' },
+ ]) ?? 'coding'
}
if (category === 'exploration') {
@@ -109,8 +139,14 @@ function refineByKeywords(category: TaskCategory, userMessage: string): TaskCate
function classifyConversation(userMessage: string): TaskCategory {
if (BRAINSTORM_KEYWORDS.test(userMessage)) return 'brainstorming'
if (RESEARCH_KEYWORDS.test(userMessage)) return 'exploration'
- if (DEBUG_KEYWORDS.test(userMessage)) return 'debugging'
- if (FEATURE_KEYWORDS.test(userMessage)) return 'feature'
+ // Same first-match-wins logic as refineByKeywords so a chat-only message
+ // starting with a feature verb does not flip to debugging because of an
+ // incidental "error" or "fix" word later in the same sentence.
+ const debugOrFeature = firstMatchingCategory(userMessage, [
+ { regex: FEATURE_KEYWORDS, category: 'feature' },
+ { regex: DEBUG_KEYWORDS, category: 'debugging' },
+ ])
+ if (debugOrFeature) return debugOrFeature
if (FILE_PATTERNS.test(userMessage)) return 'coding'
if (SCRIPT_PATTERNS.test(userMessage)) return 'coding'
if (URL_PATTERN.test(userMessage)) return 'exploration'
@@ -159,5 +195,12 @@ export function classifyTurn(turn: ParsedTurn): ClassifiedTurn {
}
}
- return { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) }
+ const result: ClassifiedTurn = { ...turn, category, retries: countRetries(turn), hasEdits: turnHasEdits(turn) }
+
+ if (category === 'general') {
+ const skills = getAllSkills(turn)
+ if (skills.length > 0) result.subCategory = skills[0]
+ }
+
+ return result
}
diff --git a/src/cli-date.ts b/src/cli-date.ts
index 66831b9..250884b 100644
--- a/src/cli-date.ts
+++ b/src/cli-date.ts
@@ -1,4 +1,5 @@
import type { DateRange } from './types.js'
+import { toDateString } from './daily-cache.js'
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/
@@ -7,19 +8,68 @@ const END_OF_DAY_MINUTES = 59
const END_OF_DAY_SECONDS = 59
const END_OF_DAY_MS = 999
+// "All Time" is intentionally bounded to the last 6 months. Older data is
+// rarely actionable for a cost tracker, and capping the range keeps the parse
+// path bounded so providers like Codex/Cursor with sparse multi-year history
+// still load in seconds. Users who need an unbounded window can use
+// `--from` / `--to`.
+const ALL_TIME_MONTHS = 6
+
+export type Period = 'today' | 'week' | '30days' | 'month' | 'all'
+
+export const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
+
+// Short labels suitable for the dashboard tab strip. Long-form labels for
+// header text come from `getDateRange().label`.
+export const PERIOD_LABELS: Record = {
+ today: 'Today',
+ week: '7 Days',
+ '30days': '30 Days',
+ month: 'This Month',
+ all: '6 Months',
+}
+
+const VALID_PERIODS: ReadonlyArray = ['today', 'week', '30days', 'month', 'all']
+
+export function toPeriod(s: string): Period {
+ if ((VALID_PERIODS as readonly string[]).includes(s)) return s as Period
+ // Fail loudly instead of silently coercing to 'week'. Previously a typo
+ // like `-p mounth` produced a quiet 7-day report and the user thought
+ // they were viewing the month.
+ process.stderr.write(
+ `codeburn: unknown period "${s}". Valid values: ${VALID_PERIODS.join(', ')}.\n`
+ )
+ process.exit(1)
+}
+
function parseLocalDate(s: string): Date {
if (!ISO_DATE_RE.test(s)) {
throw new Error(`Invalid date format "${s}": expected YYYY-MM-DD`)
}
const [y, m, d] = s.split('-').map(Number) as [number, number, number]
- return new Date(y, m - 1, d)
+ const date = new Date(y, m - 1, d)
+ // JS Date silently rolls overflow forward (Feb 31 → Mar 3). That makes a
+ // typo like `--from 2026-02-31 --to 2026-03-15` quietly drop sessions
+ // dated Feb 28 - Mar 2. Reject overflow so the user gets a loud error
+ // instead of an off-by-N-days date range.
+ if (date.getFullYear() !== y || date.getMonth() !== m - 1 || date.getDate() !== d) {
+ throw new Error(`Invalid date "${s}": ${m}/${d}/${y} is not a real calendar date`)
+ }
+ return date
}
export function parseDateRangeFlags(from: string | undefined, to: string | undefined): DateRange | null {
if (from === undefined && to === undefined) return null
const now = new Date()
- const start = from !== undefined ? parseLocalDate(from) : new Date(0)
+ // When --from is omitted, default to 6 months back (the same window the
+ // dashboard's "all" period uses) instead of epoch. Previously a bare
+ // `--to 2026-01-01` opened a 55-year scan from 1970 which is rarely what
+ // the user meant and is expensive on machines with many session files.
+ const ALL_TIME_FALLBACK_MS = 6 * 31 * 24 * 60 * 60 * 1000
+ const start = from !== undefined
+ ? parseLocalDate(from)
+ : new Date(now.getTime() - ALL_TIME_FALLBACK_MS)
const endDate = to !== undefined ? parseLocalDate(to) : new Date(now.getFullYear(), now.getMonth(), now.getDate())
const end = new Date(
@@ -37,3 +87,64 @@ export function parseDateRangeFlags(from: string | undefined, to: string | undef
}
return { start, end }
}
+
+/**
+ * Returns the date range and a human-readable label for a named period.
+ *
+ * Accepts a string (rather than the strict `Period` type) because the CLI
+ * surfaces a few extra inputs not exposed in the dashboard tab strip
+ * (e.g. `'yesterday'`). Unknown values fall back to `'week'`.
+ *
+ * Note: `'all'` is bounded to the last 6 months. Use `--from`/`--to` for
+ * an unbounded historical window.
+ */
+export function getDateRange(period: string): { range: DateRange; label: string } {
+ const now = new Date()
+ const end = new Date(
+ now.getFullYear(),
+ now.getMonth(),
+ now.getDate(),
+ END_OF_DAY_HOURS,
+ END_OF_DAY_MINUTES,
+ END_OF_DAY_SECONDS,
+ END_OF_DAY_MS,
+ )
+
+ switch (period) {
+ case 'today': {
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
+ return { range: { start, end }, label: `Today (${toDateString(start)})` }
+ }
+ case 'yesterday': {
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
+ const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, END_OF_DAY_HOURS, END_OF_DAY_MINUTES, END_OF_DAY_SECONDS, END_OF_DAY_MS)
+ return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` }
+ }
+ case 'week': {
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
+ return { range: { start, end }, label: 'Last 7 Days' }
+ }
+ case 'month': {
+ const start = new Date(now.getFullYear(), now.getMonth(), 1)
+ return { range: { start, end }, label: `${now.toLocaleString('default', { month: 'long' })} ${now.getFullYear()}` }
+ }
+ case '30days': {
+ const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
+ return { range: { start, end }, label: 'Last 30 Days' }
+ }
+ case 'all': {
+ const start = new Date(now.getFullYear(), now.getMonth() - ALL_TIME_MONTHS, 1)
+ return { range: { start, end }, label: 'Last 6 months' }
+ }
+ default: {
+ process.stderr.write(
+ `codeburn: unknown period "${period}". Valid values: today, week, 30days, month, all.\n`
+ )
+ process.exit(1)
+ }
+ }
+}
+
+export function formatDateRangeLabel(from: string | undefined, to: string | undefined): string {
+ return `${from ?? 'all'} to ${to ?? 'today'}`
+}
diff --git a/src/cli.ts b/src/cli.ts
index 116866c..dec3d49 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -1,888 +1,15 @@
-import { Command } from 'commander'
-import { installMenubarApp } from './menubar-installer.js'
-import { exportCsv, exportJson, type PeriodExport } from './export.js'
-import { loadPricing, setModelAliases } from './models.js'
-import { parseAllSessions, filterProjectsByName } from './parser.js'
-import { convertCost } from './currency.js'
-import { renderStatusBar } from './format.js'
-import { type PeriodData, type ProviderCost } from './menubar-json.js'
-import { buildMenubarPayload } from './menubar-json.js'
-import { getDaysInRange, ensureCacheHydrated, emptyCache, MS_PER_DAY, BACKFILL_DAYS, toDateString } from './daily-cache.js'
-import { aggregateProjectsIntoDays, buildPeriodDataFromDays, dateKey } from './day-aggregator.js'
-import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
-import { renderDashboard } from './dashboard.js'
-import { parseDateRangeFlags } from './cli-date.js'
-import { runOptimize, scanAndDetect } from './optimize.js'
-import { renderCompare } from './compare.js'
-import { getAllProviders } from './providers/index.js'
-import { clearPlan, readConfig, readPlan, saveConfig, savePlan, getConfigFilePath, type PlanId } from './config.js'
-import { clampResetDay, getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
-import { getPresetPlan, isPlanId, isPlanProvider, planDisplayName } from './plans.js'
-import { createRequire } from 'node:module'
-
-const require = createRequire(import.meta.url)
-const { version } = require('../package.json')
-import { loadCurrency, getCurrency, isValidCurrencyCode } from './currency.js'
-
-async function hydrateCache() {
- try {
- return await ensureCacheHydrated(
- (range) => parseAllSessions(range, 'all'),
- aggregateProjectsIntoDays,
- )
- } catch {
- return emptyCache()
- }
+#!/usr/bin/env node
+// This launcher must stay parseable by Node 18. Do NOT add static imports.
+const [major, minor] = process.versions.node.split('.').map(Number)
+if (major < 22 || (major === 22 && minor < 13)) {
+ process.stderr.write(
+ `codeburn requires Node.js >= 22.13.0 (current: ${process.version})\n` +
+ 'Upgrade at https://nodejs.org/\n',
+ )
+ process.exit(1)
}
-function getDateRange(period: string): { range: DateRange; label: string } {
- const now = new Date()
- const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
-
- switch (period) {
- case 'today': {
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- return { range: { start, end }, label: `Today (${toDateString(start)})` }
- }
- case 'yesterday': {
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)
- const yesterdayEnd = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 23, 59, 59, 999)
- return { range: { start, end: yesterdayEnd }, label: `Yesterday (${toDateString(start)})` }
- }
- case 'week': {
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
- return { range: { start, end }, label: 'Last 7 Days' }
- }
- case 'month': {
- const start = new Date(now.getFullYear(), now.getMonth(), 1)
- return { range: { start, end }, label: `${now.toLocaleString('default', { month: 'long' })} ${now.getFullYear()}` }
- }
- case '30days': {
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30)
- return { range: { start, end }, label: 'Last 30 Days' }
- }
- case 'all': {
- // Cap "All Time" to the last 6 months. Older data is rarely actionable for a cost
- // tracker and keeps the parse path bounded so providers like Codex/Cursor with sparse
- // data still load in seconds.
- const start = new Date(now.getFullYear(), now.getMonth() - 6, now.getDate())
- return { range: { start, end }, label: 'Last 6 months' }
- }
- default: {
- const start = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7)
- return { range: { start, end }, label: 'Last 7 Days' }
- }
- }
-}
-
-type Period = 'today' | 'week' | '30days' | 'month' | 'all'
-
-function toPeriod(s: string): Period {
- if (s === 'today') return 'today'
- if (s === 'month') return 'month'
- if (s === '30days') return '30days'
- if (s === 'all') return 'all'
- return 'week'
-}
-
-function collect(val: string, acc: string[]): string[] {
- acc.push(val)
- return acc
-}
-
-function parseNumber(value: string): number {
- return Number(value)
-}
-
-function parseInteger(value: string): number {
- return parseInt(value, 10)
-}
-
-type JsonPlanSummary = {
- id: PlanId
- budget: number
- spent: number
- percentUsed: number
- status: 'under' | 'near' | 'over'
- projectedMonthEnd: number
- daysUntilReset: number
- periodStart: string
- periodEnd: string
-}
-
-function toJsonPlanSummary(planUsage: PlanUsage): JsonPlanSummary {
- return {
- id: planUsage.plan.id,
- budget: convertCost(planUsage.budgetUsd),
- spent: convertCost(planUsage.spentApiEquivalentUsd),
- percentUsed: Math.round(planUsage.percentUsed * 10) / 10,
- status: planUsage.status,
- projectedMonthEnd: convertCost(planUsage.projectedMonthUsd),
- daysUntilReset: planUsage.daysUntilReset,
- periodStart: planUsage.periodStart.toISOString(),
- periodEnd: planUsage.periodEnd.toISOString(),
- }
-}
-
-async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise {
- await loadPricing()
- const { range, label } = getDateRange(period)
- const projects = filterProjectsByName(await parseAllSessions(range, provider), project, exclude)
- const report: ReturnType & { plan?: JsonPlanSummary } = buildJsonReport(projects, label, period)
- const planUsage = await getPlanUsageOrNull()
- if (planUsage) {
- report.plan = toJsonPlanSummary(planUsage)
- }
- console.log(JSON.stringify(report, null, 2))
-}
-
-const program = new Command()
- .name('codeburn')
- .description('See where your AI coding tokens go - by task, tool, model, and project')
- .version(version)
- .option('--verbose', 'print warnings to stderr on read failures and skipped files')
-
-program.hook('preAction', async (thisCommand) => {
- const config = await readConfig()
- setModelAliases(config.modelAliases ?? {})
- if (thisCommand.opts<{ verbose?: boolean }>().verbose) {
- process.env['CODEBURN_VERBOSE'] = '1'
- }
- await loadCurrency()
+import('./main.js').catch((err) => {
+ process.stderr.write(String(err?.message ?? err) + '\n')
+ process.exit(1)
})
-
-function buildJsonReport(projects: ProjectSummary[], period: string, periodKey: string) {
- const sessions = projects.flatMap(p => p.sessions)
- const { code } = getCurrency()
-
- const totalCostUSD = projects.reduce((s, p) => s + p.totalCostUSD, 0)
- const totalCalls = projects.reduce((s, p) => s + p.totalApiCalls, 0)
- const totalSessions = projects.reduce((s, p) => s + p.sessions.length, 0)
- const totalInput = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0)
- const totalOutput = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0)
- const totalCacheRead = sessions.reduce((s, sess) => s + sess.totalCacheReadTokens, 0)
- const totalCacheWrite = sessions.reduce((s, sess) => s + sess.totalCacheWriteTokens, 0)
- // Match src/menubar-json.ts:cacheHitPercent: reads over reads+fresh-input. cache_write
- // counts tokens being stored, not served, so it doesn't belong in the denominator.
- const cacheHitDenom = totalInput + totalCacheRead
- const cacheHitPercent = cacheHitDenom > 0 ? Math.round((totalCacheRead / cacheHitDenom) * 1000) / 10 : 0
-
- const dailyMap: Record = {}
- for (const sess of sessions) {
- for (const turn of sess.turns) {
- if (!turn.timestamp) { continue }
- const day = dateKey(turn.timestamp)
- if (!dailyMap[day]) { dailyMap[day] = { cost: 0, calls: 0 } }
- for (const call of turn.assistantCalls) {
- dailyMap[day].cost += call.costUSD
- dailyMap[day].calls += 1
- }
- }
- }
- const daily = Object.entries(dailyMap).sort().map(([date, d]) => ({
- date,
- cost: convertCost(d.cost),
- calls: d.calls,
- }))
-
- const projectList = projects.map(p => ({
- name: p.project,
- path: p.projectPath,
- cost: convertCost(p.totalCostUSD),
- avgCostPerSession: p.sessions.length > 0
- ? convertCost(p.totalCostUSD / p.sessions.length)
- : null,
- calls: p.totalApiCalls,
- sessions: p.sessions.length,
- }))
-
- const modelMap: Record = {}
- for (const sess of sessions) {
- for (const [model, d] of Object.entries(sess.modelBreakdown)) {
- if (!modelMap[model]) { modelMap[model] = { calls: 0, cost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } }
- modelMap[model].calls += d.calls
- modelMap[model].cost += d.costUSD
- modelMap[model].inputTokens += d.tokens.inputTokens
- modelMap[model].outputTokens += d.tokens.outputTokens
- modelMap[model].cacheReadTokens += d.tokens.cacheReadInputTokens
- modelMap[model].cacheWriteTokens += d.tokens.cacheCreationInputTokens
- }
- }
- const models = Object.entries(modelMap)
- .sort(([, a], [, b]) => b.cost - a.cost)
- .map(([name, { cost, ...rest }]) => ({ name, ...rest, cost: convertCost(cost) }))
-
- const catMap: Record = {}
- for (const sess of sessions) {
- for (const [cat, d] of Object.entries(sess.categoryBreakdown)) {
- if (!catMap[cat]) { catMap[cat] = { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 } }
- catMap[cat].turns += d.turns
- catMap[cat].cost += d.costUSD
- catMap[cat].editTurns += d.editTurns
- catMap[cat].oneShotTurns += d.oneShotTurns
- }
- }
- const activities = Object.entries(catMap)
- .sort(([, a], [, b]) => b.cost - a.cost)
- .map(([cat, d]) => ({
- category: CATEGORY_LABELS[cat as TaskCategory] ?? cat,
- cost: convertCost(d.cost),
- turns: d.turns,
- editTurns: d.editTurns,
- oneShotTurns: d.oneShotTurns,
- oneShotRate: d.editTurns > 0 ? Math.round((d.oneShotTurns / d.editTurns) * 1000) / 10 : null,
- }))
-
- const toolMap: Record = {}
- const mcpMap: Record = {}
- const bashMap: Record = {}
- for (const sess of sessions) {
- for (const [tool, d] of Object.entries(sess.toolBreakdown)) {
- toolMap[tool] = (toolMap[tool] ?? 0) + d.calls
- }
- for (const [server, d] of Object.entries(sess.mcpBreakdown)) {
- mcpMap[server] = (mcpMap[server] ?? 0) + d.calls
- }
- for (const [cmd, d] of Object.entries(sess.bashBreakdown)) {
- bashMap[cmd] = (bashMap[cmd] ?? 0) + d.calls
- }
- }
-
- const sortedMap = (m: Record) =>
- Object.entries(m).sort(([, a], [, b]) => b - a).map(([name, calls]) => ({ name, calls }))
-
- const topSessions = projects
- .flatMap(p => p.sessions.map(s => ({ project: p.project, sessionId: s.sessionId, date: s.firstTimestamp ? dateKey(s.firstTimestamp) : null, cost: convertCost(s.totalCostUSD), calls: s.apiCalls })))
- .sort((a, b) => b.cost - a.cost)
- .slice(0, 5)
-
- return {
- generated: new Date().toISOString(),
- currency: code,
- period,
- periodKey,
- overview: {
- cost: convertCost(totalCostUSD),
- calls: totalCalls,
- sessions: totalSessions,
- cacheHitPercent,
- tokens: {
- input: totalInput,
- output: totalOutput,
- cacheRead: totalCacheRead,
- cacheWrite: totalCacheWrite,
- },
- },
- daily,
- projects: projectList,
- models,
- activities,
- tools: sortedMap(toolMap),
- mcpServers: sortedMap(mcpMap),
- shellCommands: sortedMap(bashMap),
- topSessions,
- }
-}
-
-program
- .command('report', { isDefault: true })
- .description('Interactive usage dashboard')
- .option('-p, --period ', 'Starting period: today, week, 30days, month, all', 'week')
- .option('--from ', 'Start date (YYYY-MM-DD). Overrides --period when set')
- .option('--to ', 'End date (YYYY-MM-DD). Overrides --period when set')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .option('--format ', 'Output format: tui, json', 'tui')
- .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
- .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
- .action(async (opts) => {
- let customRange: DateRange | null = null
- try {
- customRange = parseDateRangeFlags(opts.from, opts.to)
- } catch (err) {
- const message = err instanceof Error ? err.message : String(err)
- console.error(`\n Error: ${message}\n`)
- process.exit(1)
- }
-
- const period = toPeriod(opts.period)
- if (opts.format === 'json') {
- await loadPricing()
- await hydrateCache()
- if (customRange) {
- const label = `${opts.from ?? 'all'} to ${opts.to ?? 'today'}`
- const projects = filterProjectsByName(
- await parseAllSessions(customRange, opts.provider),
- opts.project,
- opts.exclude,
- )
- console.log(JSON.stringify(buildJsonReport(projects, label, 'custom'), null, 2))
- } else {
- await runJsonReport(period, opts.provider, opts.project, opts.exclude)
- }
- return
- }
- await hydrateCache()
- await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude, customRange)
- })
-
-function buildPeriodData(label: string, projects: ProjectSummary[]): PeriodData {
- const sessions = projects.flatMap(p => p.sessions)
- const catTotals: Record = {}
- const modelTotals: Record = {}
- let inputTokens = 0, outputTokens = 0, cacheReadTokens = 0, cacheWriteTokens = 0
-
- for (const sess of sessions) {
- inputTokens += sess.totalInputTokens
- outputTokens += sess.totalOutputTokens
- cacheReadTokens += sess.totalCacheReadTokens
- cacheWriteTokens += sess.totalCacheWriteTokens
- for (const [cat, d] of Object.entries(sess.categoryBreakdown)) {
- if (!catTotals[cat]) catTotals[cat] = { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 }
- catTotals[cat].turns += d.turns
- catTotals[cat].cost += d.costUSD
- catTotals[cat].editTurns += d.editTurns
- catTotals[cat].oneShotTurns += d.oneShotTurns
- }
- for (const [model, d] of Object.entries(sess.modelBreakdown)) {
- if (!modelTotals[model]) modelTotals[model] = { calls: 0, cost: 0 }
- modelTotals[model].calls += d.calls
- modelTotals[model].cost += d.costUSD
- }
- }
-
- return {
- label,
- cost: projects.reduce((s, p) => s + p.totalCostUSD, 0),
- calls: projects.reduce((s, p) => s + p.totalApiCalls, 0),
- sessions: projects.reduce((s, p) => s + p.sessions.length, 0),
- inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens,
- categories: Object.entries(catTotals)
- .sort(([, a], [, b]) => b.cost - a.cost)
- .map(([cat, d]) => ({ name: CATEGORY_LABELS[cat as TaskCategory] ?? cat, ...d })),
- models: Object.entries(modelTotals)
- .sort(([, a], [, b]) => b.cost - a.cost)
- .map(([name, d]) => ({ name, ...d })),
- }
-}
-
-program
- .command('status')
- .description('Compact status output (today + week + month)')
- .option('--format ', 'Output format: terminal, menubar-json, json', 'terminal')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
- .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .option('--period ', 'Primary period for menubar-json: today, week, 30days, month, all', 'today')
- .option('--no-optimize', 'Skip optimize findings (menubar-json only, faster)')
- .action(async (opts) => {
- await loadPricing()
- const pf = opts.provider
- const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
- if (opts.format === 'menubar-json') {
- const periodInfo = getDateRange(opts.period)
- const now = new Date()
- const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
- const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY))
- const isAllProviders = pf === 'all'
-
- const cache = await hydrateCache()
-
- // CURRENT PERIOD DATA
- // - .all provider: assemble from cache + today (fast)
- // - specific provider: parse the period range with provider filter (correct, but slower)
- let currentData: PeriodData
- let scanProjects: ProjectSummary[]
- let scanRange: DateRange
-
- if (isAllProviders) {
- // Parse only today's sessions; historical data comes from cache to avoid double-counting
- const todayRange: DateRange = { start: todayStart, end: new Date() }
- const todayProjects = fp(await parseAllSessions(todayRange, 'all'))
- const todayDays = aggregateProjectsIntoDays(todayProjects)
- const rangeStartStr = toDateString(periodInfo.range.start)
- const rangeEndStr = toDateString(periodInfo.range.end)
- const historicalDays = getDaysInRange(cache, rangeStartStr, yesterdayStr)
- const todayInRange = todayDays.filter(d => d.date >= rangeStartStr && d.date <= rangeEndStr)
- const allDays = [...historicalDays, ...todayInRange].sort((a, b) => a.date.localeCompare(b.date))
- currentData = buildPeriodDataFromDays(allDays, periodInfo.label)
- scanProjects = todayProjects
- scanRange = periodInfo.range
- } else {
- const projects = fp(await parseAllSessions(periodInfo.range, pf))
- currentData = buildPeriodData(periodInfo.label, projects)
- scanProjects = projects
- scanRange = periodInfo.range
- }
-
- // PROVIDERS
- // For .all: enumerate every provider with cost across the period (from cache) + installed-but-zero.
- // For specific: just this single provider with its scoped cost.
- const allProviders = await getAllProviders()
- const displayNameByName = new Map(allProviders.map(p => [p.name, p.displayName]))
- const providers: ProviderCost[] = []
- if (isAllProviders) {
- // Parse only today; historical provider costs come from cache
- const todayRangeForProviders: DateRange = { start: todayStart, end: new Date() }
- const todayDaysForProviders = aggregateProjectsIntoDays(fp(await parseAllSessions(todayRangeForProviders, 'all')))
- const rangeStartStr = toDateString(periodInfo.range.start)
- const todayStr = toDateString(todayStart)
- const allDaysForProviders = [
- ...getDaysInRange(cache, rangeStartStr, yesterdayStr),
- ...todayDaysForProviders.filter(d => d.date === todayStr),
- ]
- const providerTotals: Record = {}
- for (const d of allDaysForProviders) {
- for (const [name, p] of Object.entries(d.providers)) {
- providerTotals[name] = (providerTotals[name] ?? 0) + p.cost
- }
- }
- for (const [name, cost] of Object.entries(providerTotals)) {
- providers.push({ name: displayNameByName.get(name) ?? name, cost })
- }
- for (const p of allProviders) {
- if (providers.some(pc => pc.name === p.displayName)) continue
- const sources = await p.discoverSessions()
- if (sources.length > 0) providers.push({ name: p.displayName, cost: 0 })
- }
- } else {
- const display = displayNameByName.get(pf) ?? pf
- providers.push({ name: display, cost: currentData.cost })
- }
-
- // DAILY HISTORY (last 365 days)
- // Cache stores per-provider cost+calls per day in DailyEntry.providers, so we can derive
- // a provider-filtered history without re-parsing. Tokens aren't broken down per provider
- // in the cache, so the filtered view shows zero tokens (heatmap/trend still works on cost).
- const historyStartStr = toDateString(new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY))
- const allCacheDays = getDaysInRange(cache, historyStartStr, yesterdayStr)
- // Parse only today for history; historical days come from cache
- const todayRangeForHistory: DateRange = { start: todayStart, end: new Date() }
- const allTodayDaysForHistory = aggregateProjectsIntoDays(fp(await parseAllSessions(todayRangeForHistory, 'all')))
- const todayStrForHistory = toDateString(todayStart)
- const fullHistory = [...allCacheDays, ...allTodayDaysForHistory.filter(d => d.date === todayStrForHistory)]
- const dailyHistory = fullHistory.map(d => {
- if (isAllProviders) {
- const topModels = Object.entries(d.models)
- .filter(([name]) => name !== '')
- .sort(([, a], [, b]) => b.cost - a.cost)
- .slice(0, 5)
- .map(([name, m]) => ({
- name,
- cost: m.cost,
- calls: m.calls,
- inputTokens: m.inputTokens,
- outputTokens: m.outputTokens,
- }))
- return {
- date: d.date,
- cost: d.cost,
- calls: d.calls,
- inputTokens: d.inputTokens,
- outputTokens: d.outputTokens,
- cacheReadTokens: d.cacheReadTokens,
- cacheWriteTokens: d.cacheWriteTokens,
- topModels,
- }
- }
- const prov = d.providers[pf] ?? { calls: 0, cost: 0 }
- return {
- date: d.date,
- cost: prov.cost,
- calls: prov.calls,
- inputTokens: 0,
- outputTokens: 0,
- cacheReadTokens: 0,
- cacheWriteTokens: 0,
- topModels: [],
- }
- })
-
- const optimize = opts.optimize === false ? null : await scanAndDetect(scanProjects, scanRange)
- console.log(JSON.stringify(buildMenubarPayload(currentData, providers, optimize, dailyHistory)))
- return
- }
-
- if (opts.format === 'json') {
- await hydrateCache()
- const todayData = buildPeriodData('today', fp(await parseAllSessions(getDateRange('today').range, pf)))
- const monthData = buildPeriodData('month', fp(await parseAllSessions(getDateRange('month').range, pf)))
- const { code, rate } = getCurrency()
- const payload: {
- currency: string
- today: { cost: number; calls: number }
- month: { cost: number; calls: number }
- plan?: JsonPlanSummary
- } = {
- currency: code,
- today: { cost: Math.round(todayData.cost * rate * 100) / 100, calls: todayData.calls },
- month: { cost: Math.round(monthData.cost * rate * 100) / 100, calls: monthData.calls },
- }
- const planUsage = await getPlanUsageOrNull()
- if (planUsage) {
- payload.plan = toJsonPlanSummary(planUsage)
- }
- console.log(JSON.stringify(payload))
- return
- }
-
- await hydrateCache()
- const monthProjects = fp(await parseAllSessions(getDateRange('month').range, pf))
- console.log(renderStatusBar(monthProjects))
- })
-
-program
- .command('today')
- .description('Today\'s usage dashboard')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .option('--format ', 'Output format: tui, json', 'tui')
- .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
- .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
- .action(async (opts) => {
- if (opts.format === 'json') {
- await runJsonReport('today', opts.provider, opts.project, opts.exclude)
- return
- }
- await hydrateCache()
- await renderDashboard('today', opts.provider, opts.refresh, opts.project, opts.exclude)
- })
-
-program
- .command('month')
- .description('This month\'s usage dashboard')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .option('--format ', 'Output format: tui, json', 'tui')
- .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
- .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .option('--refresh ', 'Auto-refresh interval in seconds (0 to disable)', parseInt, 30)
- .action(async (opts) => {
- if (opts.format === 'json') {
- await runJsonReport('month', opts.provider, opts.project, opts.exclude)
- return
- }
- await hydrateCache()
- await renderDashboard('month', opts.provider, opts.refresh, opts.project, opts.exclude)
- })
-
-program
- .command('export')
- .description('Export usage data to CSV or JSON (includes 1 day, 7 days, 30 days)')
- .option('-f, --format ', 'Export format: csv, json', 'csv')
- .option('-o, --output ', 'Output file path')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
- .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
- .action(async (opts) => {
- await loadPricing()
- await hydrateCache()
- const pf = opts.provider
- const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude)
- const periods: PeriodExport[] = [
- { label: 'Today', projects: fp(await parseAllSessions(getDateRange('today').range, pf)) },
- { label: '7 Days', projects: fp(await parseAllSessions(getDateRange('week').range, pf)) },
- { label: '30 Days', projects: fp(await parseAllSessions(getDateRange('30days').range, pf)) },
- ]
-
- if (periods.every(p => p.projects.length === 0)) {
- console.log('\n No usage data found.\n')
- return
- }
-
- const defaultName = `codeburn-${toDateString(new Date())}`
- const outputPath = opts.output ?? `${defaultName}.${opts.format}`
-
- let savedPath: string
- try {
- if (opts.format === 'json') {
- savedPath = await exportJson(periods, outputPath)
- } else {
- savedPath = await exportCsv(periods, outputPath)
- }
- } catch (err) {
- // Protection guards in export.ts (symlink refusal, non-codeburn folder refusal, etc.)
- // throw with a user-readable message. Print just the message, not the stack, so the CLI
- // doesn't spray its internals at the user.
- const message = err instanceof Error ? err.message : String(err)
- console.error(`\n Export failed: ${message}\n`)
- process.exit(1)
- }
-
- console.log(`\n Exported (Today + 7 Days + 30 Days) to: ${savedPath}\n`)
- })
-
-program
- .command('menubar')
- .description('Install and launch the macOS menubar app (one command, no clone)')
- .option('--force', 'Reinstall even if an older copy is already in ~/Applications')
- .action(async (opts: { force?: boolean }) => {
- try {
- const result = await installMenubarApp({ force: opts.force })
- console.log(`\n Ready. ${result.installedPath}\n`)
- } catch (err) {
- const message = err instanceof Error ? err.message : String(err)
- console.error(`\n Menubar install failed: ${message}\n`)
- process.exit(1)
- }
- })
-
-program
- .command('currency [code]')
- .description('Set display currency (e.g. codeburn currency GBP)')
- .option('--symbol ', 'Override the currency symbol')
- .option('--reset', 'Reset to USD (removes currency config)')
- .action(async (code?: string, opts?: { symbol?: string; reset?: boolean }) => {
- if (opts?.reset) {
- const config = await readConfig()
- delete config.currency
- await saveConfig(config)
- console.log('\n Currency reset to USD.\n')
- return
- }
-
- if (!code) {
- const { code: activeCode, rate, symbol } = getCurrency()
- if (activeCode === 'USD' && rate === 1) {
- console.log('\n Currency: USD (default)')
- console.log(` Config: ${getConfigFilePath()}\n`)
- } else {
- console.log(`\n Currency: ${activeCode}`)
- console.log(` Symbol: ${symbol}`)
- console.log(` Rate: 1 USD = ${rate} ${activeCode}`)
- console.log(` Config: ${getConfigFilePath()}\n`)
- }
- return
- }
-
- const upperCode = code.toUpperCase()
- if (!isValidCurrencyCode(upperCode)) {
- console.error(`\n "${code}" is not a valid ISO 4217 currency code.\n`)
- process.exitCode = 1
- return
- }
-
- const config = await readConfig()
- config.currency = {
- code: upperCode,
- ...(opts?.symbol ? { symbol: opts.symbol } : {}),
- }
- await saveConfig(config)
-
- await loadCurrency()
- const { rate, symbol } = getCurrency()
-
- console.log(`\n Currency set to ${upperCode}.`)
- console.log(` Symbol: ${symbol}`)
- console.log(` Rate: 1 USD = ${rate} ${upperCode}`)
- console.log(` Config saved to ${getConfigFilePath()}\n`)
- })
-
-program
- .command('model-alias [from] [to]')
- .description('Map a provider model name to a canonical one for pricing (e.g. codeburn model-alias my-model claude-opus-4-6)')
- .option('--remove ', 'Remove an alias')
- .option('--list', 'List configured aliases')
- .action(async (from?: string, to?: string, opts?: { remove?: string; list?: boolean }) => {
- const config = await readConfig()
- const aliases = config.modelAliases ?? {}
-
- if (opts?.list || (!from && !opts?.remove)) {
- const entries = Object.entries(aliases)
- if (entries.length === 0) {
- console.log('\n No model aliases configured.')
- console.log(` Config: ${getConfigFilePath()}\n`)
- } else {
- console.log('\n Model aliases:')
- for (const [src, dst] of entries) {
- console.log(` ${src} -> ${dst}`)
- }
- console.log(` Config: ${getConfigFilePath()}\n`)
- }
- return
- }
-
- if (opts?.remove) {
- if (!(opts.remove in aliases)) {
- console.error(`\n Alias not found: ${opts.remove}\n`)
- process.exitCode = 1
- return
- }
- delete aliases[opts.remove]
- config.modelAliases = Object.keys(aliases).length > 0 ? aliases : undefined
- await saveConfig(config)
- console.log(`\n Removed alias: ${opts.remove}\n`)
- return
- }
-
- if (!from || !to) {
- console.error('\n Usage: codeburn model-alias \n')
- process.exitCode = 1
- return
- }
-
- aliases[from] = to
- config.modelAliases = aliases
- await saveConfig(config)
- console.log(`\n Alias saved: ${from} -> ${to}`)
- console.log(` Config: ${getConfigFilePath()}\n`)
- })
-
-program
- .command('plan [action] [id]')
- .description('Show or configure a subscription plan for overage tracking')
- .option('--format ', 'Output format: text or json', 'text')
- .option('--monthly-usd ', 'Monthly plan price in USD (for custom)', parseNumber)
- .option('--provider ', 'Provider scope: all, claude, codex, cursor', 'all')
- .option('--reset-day ', 'Day of month plan resets (1-28)', parseInteger, 1)
- .action(async (action?: string, id?: string, opts?: { format?: string; monthlyUsd?: number; provider?: string; resetDay?: number }) => {
- const mode = action ?? 'show'
-
- if (mode === 'show') {
- const plan = await readPlan()
- const displayPlan = !plan || plan.id === 'none'
- ? { id: 'none', monthlyUsd: 0, provider: 'all', resetDay: 1, setAt: null }
- : {
- id: plan.id,
- monthlyUsd: plan.monthlyUsd,
- provider: plan.provider,
- resetDay: clampResetDay(plan.resetDay),
- setAt: plan.setAt,
- }
- if (opts?.format === 'json') {
- console.log(JSON.stringify(displayPlan))
- return
- }
- if (!plan || plan.id === 'none') {
- console.log('\n Plan: none')
- console.log(' API-pricing view is active.')
- console.log(` Config: ${getConfigFilePath()}\n`)
- return
- }
- console.log(`\n Plan: ${planDisplayName(plan.id)} (${plan.id})`)
- console.log(` Budget: $${plan.monthlyUsd}/month`)
- console.log(` Provider: ${plan.provider}`)
- console.log(` Reset day: ${clampResetDay(plan.resetDay)}`)
- console.log(` Set at: ${plan.setAt}`)
- console.log(` Config: ${getConfigFilePath()}\n`)
- return
- }
-
- if (mode === 'reset') {
- await clearPlan()
- console.log('\n Plan reset. API-pricing view is active.\n')
- return
- }
-
- if (mode !== 'set') {
- console.error('\n Usage: codeburn plan [set | reset]\n')
- process.exitCode = 1
- return
- }
-
- if (!id || !isPlanId(id)) {
- console.error(`\n Plan id must be one of: claude-pro, claude-max, cursor-pro, custom, none; got "${id ?? ''}".\n`)
- process.exitCode = 1
- return
- }
-
- const resetDay = opts?.resetDay ?? 1
- if (!Number.isInteger(resetDay) || resetDay < 1 || resetDay > 28) {
- console.error(`\n --reset-day must be an integer from 1 to 28; got ${resetDay}.\n`)
- process.exitCode = 1
- return
- }
-
- if (id === 'none') {
- await clearPlan()
- console.log('\n Plan reset. API-pricing view is active.\n')
- return
- }
-
- if (id === 'custom') {
- if (opts?.monthlyUsd === undefined) {
- console.error('\n Custom plans require --monthly-usd .\n')
- process.exitCode = 1
- return
- }
- const monthlyUsd = opts.monthlyUsd
- if (!Number.isFinite(monthlyUsd) || monthlyUsd <= 0) {
- console.error(`\n --monthly-usd must be a positive number; got ${opts.monthlyUsd}.\n`)
- process.exitCode = 1
- return
- }
- const provider = opts?.provider ?? 'all'
- if (!isPlanProvider(provider)) {
- console.error(`\n --provider must be one of: all, claude, codex, cursor; got "${provider}".\n`)
- process.exitCode = 1
- return
- }
- await savePlan({
- id: 'custom',
- monthlyUsd,
- provider,
- resetDay,
- setAt: new Date().toISOString(),
- })
- console.log(`\n Plan set to custom ($${monthlyUsd}/month, ${provider}, reset day ${resetDay}).`)
- console.log(` Config saved to ${getConfigFilePath()}\n`)
- return
- }
-
- const preset = getPresetPlan(id)
- if (!preset) {
- console.error(`\n Unknown preset "${id}".\n`)
- process.exitCode = 1
- return
- }
-
- await savePlan({
- ...preset,
- resetDay,
- setAt: new Date().toISOString(),
- })
- console.log(`\n Plan set to ${planDisplayName(preset.id)} ($${preset.monthlyUsd}/month).`)
- console.log(` Provider: ${preset.provider}`)
- console.log(` Reset day: ${resetDay}`)
- console.log(` Config saved to ${getConfigFilePath()}\n`)
- })
-
-program
- .command('optimize')
- .description('Find token waste and get exact fixes')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .action(async (opts) => {
- await loadPricing()
- await hydrateCache()
- const { range, label } = getDateRange(opts.period)
- const projects = await parseAllSessions(range, opts.provider)
- await runOptimize(projects, label, range)
- })
-
-program
- .command('compare')
- .description('Compare two AI models side-by-side')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', 'all')
- .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
- .action(async (opts) => {
- await loadPricing()
- await hydrateCache()
- const { range } = getDateRange(opts.period)
- await renderCompare(range, opts.provider)
- })
-
-program
- .command('yield')
- .description('Track which AI spend shipped to main vs reverted/abandoned (experimental)')
- .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', 'week')
- .action(async (opts) => {
- const { computeYield, formatYieldSummary } = await import('./yield.js')
- await loadPricing()
- await hydrateCache()
- const { range, label } = getDateRange(opts.period)
- console.log(`\n Analyzing yield for ${label}...\n`)
- const summary = await computeYield(range, process.cwd())
- console.log(formatYieldSummary(summary))
- })
-
-program.parse()
diff --git a/src/codex-cache.ts b/src/codex-cache.ts
new file mode 100644
index 0000000..d408cb5
--- /dev/null
+++ b/src/codex-cache.ts
@@ -0,0 +1,143 @@
+import { readFile, mkdir, stat, open, rename, unlink } from 'fs/promises'
+import { existsSync } from 'fs'
+import { randomBytes } from 'crypto'
+import { join } from 'path'
+import { homedir } from 'os'
+
+import type { ParsedProviderCall } from './providers/types.js'
+
+const CODEX_CACHE_VERSION = 1
+const CACHE_FILE = 'codex-results.json'
+
+type FileFingerprint = { mtimeMs: number; sizeBytes: number }
+
+type FileEntry = {
+ mtimeMs: number
+ sizeBytes: number
+ project: string
+ calls: ParsedProviderCall[]
+}
+
+type ResultCache = {
+ version: number
+ files: Record
+}
+
+function getCacheDir(): string {
+ return process.env['CODEBURN_CACHE_DIR'] ?? join(homedir(), '.cache', 'codeburn')
+}
+
+function getCachePath(): string {
+ return join(getCacheDir(), CACHE_FILE)
+}
+
+let memCache: ResultCache | null = null
+
+async function loadCache(): Promise {
+ if (memCache) return memCache
+ try {
+ const raw = await readFile(getCachePath(), 'utf-8')
+ const cache = JSON.parse(raw) as ResultCache
+ if (cache.version === CODEX_CACHE_VERSION && cache.files && typeof cache.files === 'object') {
+ memCache = cache
+ return cache
+ }
+ } catch {}
+ memCache = { version: CODEX_CACHE_VERSION, files: {} }
+ return memCache
+}
+
+function getEntry(cache: ResultCache, filePath: string, fp: FileFingerprint): FileEntry | null {
+ if (!Object.hasOwn(cache.files, filePath)) return null
+ const entry = cache.files[filePath]
+ if (entry && entry.mtimeMs === fp.mtimeMs && entry.sizeBytes === fp.sizeBytes) {
+ return entry
+ }
+ return null
+}
+
+export async function readCachedCodexResults(
+ filePath: string,
+): Promise {
+ try {
+ const s = await stat(filePath)
+ const cache = await loadCache()
+ const entry = getEntry(cache, filePath, { mtimeMs: s.mtimeMs, sizeBytes: s.size })
+ return entry?.calls ?? null
+ } catch {}
+ return null
+}
+
+export async function getCachedCodexProject(
+ filePath: string,
+): Promise {
+ try {
+ const s = await stat(filePath)
+ const cache = await loadCache()
+ const entry = getEntry(cache, filePath, { mtimeMs: s.mtimeMs, sizeBytes: s.size })
+ return entry?.project ?? null
+ } catch {}
+ return null
+}
+
+export async function fingerprintFile(
+ filePath: string,
+): Promise {
+ try {
+ const s = await stat(filePath)
+ return { mtimeMs: s.mtimeMs, sizeBytes: s.size }
+ } catch {
+ return null
+ }
+}
+
+export async function writeCachedCodexResults(
+ filePath: string,
+ project: string,
+ calls: ParsedProviderCall[],
+ fingerprint: FileFingerprint,
+): Promise {
+ try {
+ const cache = await loadCache()
+ cache.files[filePath] = {
+ mtimeMs: fingerprint.mtimeMs,
+ sizeBytes: fingerprint.sizeBytes,
+ project,
+ calls,
+ }
+ } catch {}
+}
+
+export async function flushCodexCache(): Promise {
+ if (!memCache) return
+ try {
+ // Evict entries for files that no longer exist on disk
+ const paths = Object.keys(memCache.files)
+ for (const p of paths) {
+ try {
+ await stat(p)
+ } catch {
+ delete memCache.files[p]
+ }
+ }
+
+ const dir = getCacheDir()
+ if (!existsSync(dir)) await mkdir(dir, { recursive: true })
+ const finalPath = getCachePath()
+ const tempPath = `${finalPath}.${randomBytes(8).toString('hex')}.tmp`
+ const payload = JSON.stringify(memCache)
+ const handle = await open(tempPath, 'w', 0o600)
+ try {
+ await handle.writeFile(payload, { encoding: 'utf-8' })
+ await handle.sync()
+ } finally {
+ await handle.close()
+ }
+ try {
+ await rename(tempPath, finalPath)
+ } catch (err) {
+ try { await unlink(tempPath) } catch {}
+ throw err
+ }
+ } catch {}
+}
diff --git a/src/compare.tsx b/src/compare.tsx
index af1d147..2fd71d4 100644
--- a/src/compare.tsx
+++ b/src/compare.tsx
@@ -7,6 +7,7 @@ import { formatCost } from './format.js'
import { parseAllSessions } from './parser.js'
import { getAllProviders } from './providers/index.js'
import type { ProjectSummary, DateRange } from './types.js'
+import { patchStdoutForWindows } from './ink-win.js'
const ORANGE = '#FF8C42'
const GREEN = '#5BF5A0'
@@ -330,16 +331,40 @@ export function CompareView({ projects, onBack }: CompareViewProps) {
const newModels = aggregateModelStats(projects)
setModels(newModels)
- if (pickedNames) {
- const hasA = newModels.some(m => m.model === pickedNames[0])
- const hasB = newModels.some(m => m.model === pickedNames[1])
- if (hasA && hasB) {
- setLoadTrigger(t => t + 1)
- } else {
- setPickedNames(null)
- setPhase('select')
- }
+ if (!pickedNames) return
+ const hasA = newModels.some(m => m.model === pickedNames[0])
+ const hasB = newModels.some(m => m.model === pickedNames[1])
+ if (!hasA || !hasB) {
+ setPickedNames(null)
+ setPhase('select')
+ return
}
+
+ // When the periodic CLI refresh updates `projects` while the user is
+ // reading the results page, recompute the comparison rows IN PLACE rather
+ // than flipping to a loading screen. Previously every 30s tick bounced the
+ // user to a loading flash and reset their scroll position; the slow part
+ // (scanSelfCorrections, which walks every provider's session dir) is
+ // skipped on these refreshes — corrections drift slowly enough that
+ // staying with the existing values until the user re-enters compare from
+ // scratch is fine.
+ if (phase === 'results') {
+ const a = newModels.find(m => m.model === pickedNames[0])
+ const b = newModels.find(m => m.model === pickedNames[1])
+ if (!a || !b) return
+ const aCopy = { ...a, selfCorrections: selectedA?.selfCorrections ?? 0 }
+ const bCopy = { ...b, selfCorrections: selectedB?.selfCorrections ?? 0 }
+ setSelectedA(aCopy)
+ setSelectedB(bCopy)
+ setRows(computeComparison(aCopy, bCopy))
+ setCategories(computeCategoryComparison(projects, a.model, b.model))
+ setStyle(computeWorkingStyle(projects, a.model, b.model))
+ return
+ }
+
+ // Initial load (or returning from select after picking) — full pipeline,
+ // including scanSelfCorrections.
+ setLoadTrigger(t => t + 1)
}, [projects])
useEffect(() => {
@@ -448,6 +473,7 @@ export async function renderCompare(range: DateRange, provider: string): Promise
return
}
+ patchStdoutForWindows()
const projects = await parseAllSessions(range, provider)
const { waitUntilExit } = render(
process.exit(0)} />
diff --git a/src/config.ts b/src/config.ts
index 47a2b50..12fec8f 100644
--- a/src/config.ts
+++ b/src/config.ts
@@ -1,6 +1,7 @@
import { readFile, writeFile, mkdir, rename } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
+import { randomBytes } from 'crypto'
export type PlanId = 'claude-pro' | 'claude-max' | 'claude-max-5x' | 'cursor-pro' | 'custom' | 'none'
export type PlanProvider = 'claude' | 'codex' | 'cursor' | 'all'
@@ -42,7 +43,11 @@ export async function readConfig(): Promise {
export async function saveConfig(config: CodeburnConfig): Promise {
await mkdir(getConfigDir(), { recursive: true })
const configPath = getConfigPath()
- const tmpPath = `${configPath}.tmp`
+ // Randomize the temp path so two simultaneous saveConfig calls (from
+ // overlapping menubar + CLI runs, for example) do not race on the same
+ // staging file. The previous fixed `.tmp` suffix could leave one
+ // process reading partial bytes the other was mid-writing.
+ const tmpPath = `${configPath}.${randomBytes(8).toString('hex')}.tmp`
await writeFile(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf-8')
await rename(tmpPath, configPath)
}
diff --git a/src/currency.ts b/src/currency.ts
index 8788e07..92f0364 100644
--- a/src/currency.ts
+++ b/src/currency.ts
@@ -47,13 +47,24 @@ function resolveSymbol(code: string): string {
return parts.find(p => p.type === 'currency')?.value ?? code
}
-function getFractionDigits(code: string): number {
+export function getFractionDigits(code: string): number {
return new Intl.NumberFormat('en', {
style: 'currency',
currency: code,
}).resolvedOptions().maximumFractionDigits ?? 2
}
+/// Round a converted cost to the currency's natural decimal places. JPY/KRW/CLP
+/// resolve to 0 fraction digits — exporting those with `round2` produced rows
+/// like `¥412.37` while the dashboard rendered `¥412`, breaking finance reports
+/// that compare the two surfaces.
+export function roundForActiveCurrency(value: number): number {
+ const code = getCurrency().code
+ const digits = getFractionDigits(code)
+ const factor = Math.pow(10, digits)
+ return Math.round(value * factor) / factor
+}
+
function getCacheDir(): string {
return join(homedir(), '.cache', 'codeburn')
}
@@ -98,13 +109,19 @@ async function getExchangeRate(code: string): Promise {
const cached = await loadCachedRate(code)
if (cached) return cached
+ let rate: number
try {
- const rate = await fetchRate(code)
- await cacheRate(code, rate)
- return rate
+ rate = await fetchRate(code)
} catch {
return 1
}
+ // Persist the rate, but never let a cache-write failure (disk full, no
+ // permissions, etc.) cause us to return the USD-equivalent fallback.
+ // The original code wrapped fetch + cacheRate in one try/catch, so a
+ // disk-full at write time would discard a perfectly good rate and silently
+ // make every cost render as if the user had selected USD.
+ cacheRate(code, rate).catch(() => {})
+ return rate
}
export async function loadCurrency(): Promise {
@@ -137,9 +154,13 @@ export function getCostColumnHeader(): string {
}
export function convertCost(costUSD: number): number {
- const digits = getFractionDigits(active.code)
- const factor = 10 ** digits
- return Math.round(costUSD * active.rate * factor) / factor
+ // Return the unrounded converted cost. Rounding here meant zero-fraction
+ // currencies (JPY, KRW, CLP) clamped every per-session cost to the nearest
+ // whole unit before aggregation; a project with 1000 sessions averaging
+ // ¥0.4 each would aggregate to ¥0 instead of ¥400 because each row was
+ // rounded independently. formatCost (and the export rowsToCsv path) round
+ // at the display boundary instead.
+ return costUSD * active.rate
}
export function formatCost(costUSD: number): string {
@@ -151,5 +172,6 @@ export function formatCost(costUSD: number): string {
if (cost >= 1) return `${symbol}${cost.toFixed(2)}`
if (cost >= 0.01) return `${symbol}${cost.toFixed(3)}`
- return `${symbol}${cost.toFixed(4)}`
+ if (cost >= 0.0001) return `${symbol}${cost.toFixed(4)}`
+ return `${symbol}${cost.toFixed(2)}`
}
diff --git a/src/cursor-cache.ts b/src/cursor-cache.ts
index 62cc394..390dcfa 100644
--- a/src/cursor-cache.ts
+++ b/src/cursor-cache.ts
@@ -1,10 +1,17 @@
-import { readFile, writeFile, mkdir, stat } from 'fs/promises'
+import { readFile, writeFile, mkdir, rename, stat, unlink } from 'fs/promises'
import { join } from 'path'
import { homedir } from 'os'
+import { randomBytes } from 'crypto'
import type { ParsedProviderCall } from './providers/types.js'
-const CURSOR_CACHE_VERSION = 2
+// Bumped to 3 for the workspace-aware breakdown change: the cursor parser
+// now derives `sessionId` from the bubble row key (the real composer id)
+// rather than the empty `conversationId` JSON field, and the workspace
+// router relies on those composer ids to bucket calls per project.
+// Version 2 caches contain `sessionId: 'unknown'` for every call and would
+// route everything to the orphan project, so we invalidate them.
+const CURSOR_CACHE_VERSION = 3
type ResultCache = {
version?: number
@@ -50,18 +57,30 @@ export async function readCachedResults(dbPath: string): Promise {
- try {
- const fp = await getDbFingerprint(dbPath)
- if (!fp) return
+ const fp = await getDbFingerprint(dbPath)
+ if (!fp) return
- const dir = getCacheDir()
- await mkdir(dir, { recursive: true })
- const cache: ResultCache = {
- version: CURSOR_CACHE_VERSION,
- dbMtimeMs: fp.mtimeMs,
- dbSizeBytes: fp.size,
- calls,
- }
- await writeFile(getCachePath(), JSON.stringify(cache), 'utf-8')
- } catch {}
+ const dir = getCacheDir()
+ await mkdir(dir, { recursive: true }).catch(() => {})
+ const cache: ResultCache = {
+ version: CURSOR_CACHE_VERSION,
+ dbMtimeMs: fp.mtimeMs,
+ dbSizeBytes: fp.size,
+ calls,
+ }
+
+ // Atomic write: stage to a randomized temp file in the same directory,
+ // then rename onto the final path. rename() is atomic on POSIX, so a
+ // crash mid-write never leaves a half-written cache, and concurrent
+ // CLI invocations using their own random temp names cannot interleave
+ // bytes in the destination file (they only race on the final rename,
+ // last-writer-wins, both with valid content).
+ const target = getCachePath()
+ const tempPath = `${target}.${randomBytes(8).toString('hex')}.tmp`
+ try {
+ await writeFile(tempPath, JSON.stringify(cache), 'utf-8')
+ await rename(tempPath, target)
+ } catch {
+ await unlink(tempPath).catch(() => {})
+ }
}
diff --git a/src/daily-cache.ts b/src/daily-cache.ts
index 6e30727..ab43017 100644
--- a/src/daily-cache.ts
+++ b/src/daily-cache.ts
@@ -5,8 +5,19 @@ import { homedir } from 'os'
import { join } from 'path'
import type { DateRange, ProjectSummary } from './types.js'
-export const DAILY_CACHE_VERSION = 4
-const MIN_SUPPORTED_VERSION = 2
+// Bumped to 6 alongside the Claude 1-hour cache-write pricing fix: prior
+// daily entries priced all Claude cache writes at the 5-minute rate, so
+// cached historical cost/model/provider/category totals would remain
+// under-reported unless discarded and recomputed from raw sessions.
+export const DAILY_CACHE_VERSION = 6
+// MIN_SUPPORTED_VERSION bumped to 6 too. The migration path
+// (isMigratableCache + migrateDays) only fills in missing default fields;
+// it does NOT recompute the providers / categories / models rollups from
+// session data, because those raw sessions are not stored in the cache.
+// So a migrated v5 cache would carry forward stale pricing totals for
+// the full cache retention window. Setting the floor to 6 forces older
+// caches to be discarded and recomputed cleanly.
+const MIN_SUPPORTED_VERSION = 6
const DAILY_CACHE_FILENAME = 'daily-cache.json'
export type DailyEntry = {
@@ -133,10 +144,24 @@ export function addNewDays(cache: DailyCache, incoming: DailyEntry[], newestDate
byDate.set(day.date, day)
}
const merged = Array.from(byDate.values()).sort((a, b) => a.date.localeCompare(b.date))
+ // Prune entries older than the BACKFILL window so the cache file does not
+ // grow unbounded over years of daily use. The "all time" / 6-month period
+ // and the BACKFILL_DAYS bootstrap both fit comfortably inside this cap.
+ // Anchor the cap on the newestDate boundary so a stale or stuck clock
+ // can't accidentally evict everything. Skip the prune entirely if
+ // newestDate is malformed — an invalid Date would produce a NaN cutoff
+ // and `d.date >= "Invalid Date"` would silently drop every entry.
+ const cutoffDate = new Date(`${newestDate}T00:00:00Z`)
+ let pruned = merged
+ if (!isNaN(cutoffDate.getTime())) {
+ cutoffDate.setUTCDate(cutoffDate.getUTCDate() - DAILY_CACHE_RETENTION_DAYS)
+ const cutoff = toDateString(cutoffDate)
+ pruned = merged.filter(d => d.date >= cutoff)
+ }
const nextLast = cache.lastComputedDate && cache.lastComputedDate > newestDate
? cache.lastComputedDate
: newestDate
- return { version: DAILY_CACHE_VERSION, lastComputedDate: nextLast, days: merged }
+ return { version: DAILY_CACHE_VERSION, lastComputedDate: nextLast, days: pruned }
}
export function getDaysInRange(cache: DailyCache, start: string, end: string): DailyEntry[] {
@@ -153,6 +178,10 @@ export function withDailyCacheLock(fn: () => Promise): Promise {
export const MS_PER_DAY = 24 * 60 * 60 * 1000
export const BACKFILL_DAYS = 365
+// Keep 2 years of history so the longest UI-exposed period (6 months
+// today, with headroom for future longer windows) always reads from
+// cache while old entries get pruned.
+export const DAILY_CACHE_RETENTION_DAYS = 730
export function toDateString(date: Date): string {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`
@@ -165,7 +194,7 @@ export async function ensureCacheHydrated(
const now = new Date()
const todayStart = new Date(now.getFullYear(), now.getMonth(), now.getDate())
const yesterdayEnd = new Date(todayStart.getTime() - 1)
- const yesterdayStr = toDateString(new Date(todayStart.getTime() - MS_PER_DAY))
+ const yesterdayStr = toDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1))
return withDailyCacheLock(async () => {
let c = await loadDailyCache()
@@ -183,7 +212,7 @@ export async function ensureCacheHydrated(
parseInt(c.lastComputedDate.slice(5, 7)) - 1,
parseInt(c.lastComputedDate.slice(8, 10)) + 1
)
- : new Date(todayStart.getTime() - BACKFILL_DAYS * MS_PER_DAY)
+ : new Date(now.getFullYear(), now.getMonth(), now.getDate() - BACKFILL_DAYS)
if (gapStart.getTime() <= yesterdayEnd.getTime()) {
const gapRange: DateRange = { start: gapStart, end: yesterdayEnd }
diff --git a/src/dashboard.tsx b/src/dashboard.tsx
index f84254d..4add882 100644
--- a/src/dashboard.tsx
+++ b/src/dashboard.tsx
@@ -4,34 +4,27 @@ import React, { useState, useCallback, useEffect, useRef } from 'react'
import { render, Box, Text, useInput, useApp, useWindowSize } from 'ink'
import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
import { formatCost, formatTokens } from './format.js'
+import { aggregateModelEfficiency } from './model-efficiency.js'
import { parseAllSessions, filterProjectsByName } from './parser.js'
import { loadPricing } from './models.js'
import { getAllProviders } from './providers/index.js'
import { scanAndDetect, type WasteFinding, type WasteAction, type OptimizeResult } from './optimize.js'
-import { estimateContextBudget, discoverProjectCwd, type ContextBudget } from './context-budget.js'
+import { estimateContextBudget, type ContextBudget } from './context-budget.js'
import { dateKey } from './day-aggregator.js'
import { CompareView } from './compare.js'
import { getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
import { planDisplayName } from './plans.js'
-import { join } from 'path'
+import { getDateRange, PERIODS, PERIOD_LABELS, type Period, formatDateRangeLabel } from './cli-date.js'
+import { patchStdoutForWindows } from './ink-win.js'
-type Period = 'today' | 'week' | '30days' | 'month' | 'all'
type View = 'dashboard' | 'optimize' | 'compare'
-const PERIODS: Period[] = ['today', 'week', '30days', 'month', 'all']
-const PERIOD_LABELS: Record = {
- today: 'Today',
- week: '7 Days',
- '30days': '30 Days',
- month: 'This Month',
- all: 'All Time',
-}
-
const MIN_WIDE = 90
const ORANGE = '#FF8C42'
const DIM = '#555555'
const GOLD = '#FFD700'
const PLAN_BAR_WIDTH = 10
+const HEAVY_PERIODS = new Set(['30days', 'month', 'all'])
const LANG_DISPLAY_NAMES: Record = {
javascript: 'JavaScript', typescript: 'TypeScript', python: 'Python',
@@ -59,8 +52,10 @@ const PROVIDER_COLORS: Record = {
claude: '#FF8C42',
codex: '#5BF5A0',
cursor: '#00B4D8',
+ 'ibm-bob': '#0F62FE',
opencode: '#A78BFA',
pi: '#F472B6',
+ kimi: '#B6E34A',
all: '#FF8C42',
}
@@ -103,16 +98,16 @@ function gradientColor(pct: number): string {
return toHex(lerp(255, 245, t), lerp(140, 91, t), lerp(66, 91, t))
}
-function getDateRange(period: Period): { start: Date; end: Date } {
- const now = new Date()
- const end = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 23, 59, 59, 999)
- switch (period) {
- case 'today': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate()), end }
- case 'week': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7), end }
- case '30days': return { start: new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30), end }
- case 'month': return { start: new Date(now.getFullYear(), now.getMonth(), 1), end }
- case 'all': return { start: new Date(0), end }
- }
+function getPeriodRange(period: Period): { start: Date; end: Date } {
+ return getDateRange(period).range
+}
+
+function isHeavyPeriod(period: Period): boolean {
+ return HEAVY_PERIODS.has(period)
+}
+
+function nextTick(): Promise {
+ return new Promise(resolve => setImmediate(resolve))
}
type Layout = { dashWidth: number; wide: boolean; halfWidth: number; barWidth: number }
@@ -262,16 +257,19 @@ function DailyActivity({ projects, days = 14, pw, bw }: { projects: ProjectSumma
)
}
-const _homeEncoded = homedir().replace(/\//g, '-')
+const _home = homedir()
+const _homePrefix = _home.endsWith('/') ? _home : _home + '/'
-function shortProject(encoded: string): string {
- let path = encoded.replace(/^-/, '')
- if (path.startsWith(_homeEncoded.replace(/^-/, ''))) {
- path = path.slice(_homeEncoded.replace(/^-/, '').length).replace(/^-/, '')
- }
- path = path.replace(/^private-tmp-[^-]+-[^-]+-/, '').replace(/^private-tmp-/, '').replace(/^tmp-/, '')
+export function shortProject(absPath: string): string {
+ const normalized = absPath.replace(/\\/g, '/')
+ let path: string
+ if (normalized === _home) path = ''
+ else if (normalized.startsWith(_homePrefix)) path = normalized.slice(_homePrefix.length)
+ else path = normalized
+ path = path.replace(/^\/+/, '')
+ path = path.replace(/^private\/tmp\/[^/]+\/[^/]+\//, '').replace(/^private\/tmp\//, '').replace(/^tmp\//, '')
if (!path) return 'home'
- const parts = path.split('-').filter(Boolean)
+ const parts = path.split('/').filter(Boolean)
if (parts.length <= 3) return parts.join('/')
return parts.slice(-3).join('/')
}
@@ -297,7 +295,7 @@ function ProjectBreakdown({ projects, pw, bw, budgets }: { projects: ProjectSumm
return (
- {fit(shortProject(project.project), nw)}
+ {fit(shortProject(project.projectPath), nw)}
{formatCost(project.totalCostUSD).padStart(8)}
{avgCost.padStart(PROJECT_COL_AVG)}
{String(project.sessions.length).padStart(6)}
@@ -312,10 +310,13 @@ function ProjectBreakdown({ projects, pw, bw, budgets }: { projects: ProjectSumm
const MODEL_COL_COST = 8
const MODEL_COL_CACHE = 7
const MODEL_COL_CALLS = 7
+const MODEL_COL_ONESHOT = 7
const MODEL_NAME_WIDTH = 14
+const MIN_EDIT_TURNS_FOR_RATE = 5
function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const modelTotals: Record = {}
+ const modelEfficiency = aggregateModelEfficiency(projects)
for (const project of projects) {
for (const session of project.sessions) {
for (const [model, data] of Object.entries(session.modelBreakdown)) {
@@ -333,11 +334,15 @@ function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw:
return (
- {''.padEnd(bw + 1 + MODEL_NAME_WIDTH)}{'cost'.padStart(MODEL_COL_COST)}{'cache'.padStart(MODEL_COL_CACHE)}{'calls'.padStart(MODEL_COL_CALLS)}
+ {''.padEnd(bw + 1 + MODEL_NAME_WIDTH)}{'cost'.padStart(MODEL_COL_COST)}{'cache'.padStart(MODEL_COL_CACHE)}{'calls'.padStart(MODEL_COL_CALLS)}{'1-shot'.padStart(MODEL_COL_ONESHOT)}
{sorted.map(([model, data], i) => {
const totalInput = data.freshInput + data.cacheRead + data.cacheWrite
const cacheHit = totalInput > 0 ? (data.cacheRead / totalInput) * 100 : 0
const cacheLabel = totalInput > 0 ? `${cacheHit.toFixed(1)}%` : '-'
+ const efficiency = modelEfficiency.get(model)
+ const oneShotLabel = efficiency && efficiency.editTurns >= MIN_EDIT_TURNS_FOR_RATE && efficiency.oneShotRate !== null
+ ? `${efficiency.oneShotRate.toFixed(1)}%`
+ : '-'
return (
@@ -345,6 +350,7 @@ function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw:
{formatCost(data.costUSD).padStart(MODEL_COL_COST)}
{cacheLabel.padStart(MODEL_COL_CACHE)}
{String(data.calls).padStart(MODEL_COL_CALLS)}
+ {oneShotLabel.padStart(MODEL_COL_ONESHOT)}
)
})}
@@ -352,8 +358,11 @@ function ModelBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw:
)
}
+const SKILL_SUB_ROWS_LIMIT = 5
+
function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const categoryTotals: Record = {}
+ const skillTotals: Record = {}
for (const project of projects) {
for (const session of project.sessions) {
for (const [cat, data] of Object.entries(session.categoryBreakdown)) {
@@ -363,24 +372,47 @@ function ActivityBreakdown({ projects, pw, bw }: { projects: ProjectSummary[]; p
categoryTotals[cat].editTurns += data.editTurns
categoryTotals[cat].oneShotTurns += data.oneShotTurns
}
+ for (const [skill, data] of Object.entries(session.skillBreakdown ?? {})) {
+ if (!skillTotals[skill]) skillTotals[skill] = { turns: 0, costUSD: 0, editTurns: 0, oneShotTurns: 0 }
+ skillTotals[skill].turns += data.turns
+ skillTotals[skill].costUSD += data.costUSD
+ skillTotals[skill].editTurns += data.editTurns
+ skillTotals[skill].oneShotTurns += data.oneShotTurns
+ }
}
}
const sorted = Object.entries(categoryTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD)
+ const sortedSkills = Object.entries(skillTotals).sort(([, a], [, b]) => b.costUSD - a.costUSD).slice(0, SKILL_SUB_ROWS_LIMIT)
const maxCost = sorted[0]?.[1]?.costUSD ?? 0
return (
{''.padEnd(bw + 14)}{'cost'.padStart(8)}{'turns'.padStart(6)}{'1-shot'.padStart(7)}
- {sorted.map(([cat, data]) => {
+ {sorted.flatMap(([cat, data]) => {
const oneShotPct = data.editTurns > 0 ? Math.round((data.oneShotTurns / data.editTurns) * 100) + '%' : '-'
- return (
+ const rows = [
{fit(CATEGORY_LABELS[cat as TaskCategory] ?? cat, 13)}
{formatCost(data.costUSD).padStart(8)}
{String(data.turns).padStart(6)}
{String(oneShotPct).padStart(7)}
-
- )
+ ,
+ ]
+ if (cat === 'general' && sortedSkills.length > 0) {
+ for (const [skill, sd] of sortedSkills) {
+ const subPct = sd.editTurns > 0 ? Math.round((sd.oneShotTurns / sd.editTurns) * 100) + '%' : '-'
+ rows.push(
+
+
+ {fit(` /${skill}`, 13)}
+ {formatCost(sd.costUSD).padStart(8)}
+ {String(sd.turns).padStart(6)}
+ {String(subPct).padStart(7)}
+ ,
+ )
+ }
+ }
+ return rows
})}
)
@@ -423,7 +455,7 @@ const TOP_SESSIONS_CALLS_COL = 6
function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: number; bw: number }) {
const allSessions = projects.flatMap(p =>
- p.sessions.map(s => ({ ...s, projectName: p.project }))
+ p.sessions.map(s => ({ ...s, projectPath: p.projectPath }))
)
const top = [...allSessions].sort((a, b) => b.totalCostUSD - a.totalCostUSD).slice(0, 5)
@@ -441,7 +473,7 @@ function TopSessions({ projects, pw, bw }: { projects: ProjectSummary[]; pw: num
const date = session.firstTimestamp
? session.firstTimestamp.slice(0, TOP_SESSIONS_DATE_LEN)
: '----------'
- const label = `${date} ${shortProject(session.projectName)}`
+ const label = `${date} ${shortProject(session.projectPath)}`
return (
@@ -494,8 +526,10 @@ const PROVIDER_DISPLAY_NAMES: Record = {
claude: 'Claude',
codex: 'Codex',
cursor: 'Cursor',
+ 'ibm-bob': 'IBM Bob',
opencode: 'OpenCode',
pi: 'Pi',
+ kimi: 'Kimi',
}
function getProviderDisplayName(name: string): string { return PROVIDER_DISPLAY_NAMES[name] ?? name }
@@ -516,9 +550,43 @@ function PeriodTabs({ active, providerName, showProvider }: { active: Period; pr
)
}
+/// Header for an action's intended destination. Helps users distinguish a
+/// permanent CLAUDE.md rule from a one-time session opener so they don't
+/// accidentally bake a single-run constraint into their project's permanent
+/// instructions. Issue #277.
+function actionDestinationHeader(action: WasteAction): string {
+ switch (action.type) {
+ case 'file-content':
+ return `── Suggested ${action.path} addition `.padEnd(64, '─')
+ case 'command':
+ return '── Run this command '.padEnd(64, '─')
+ case 'paste': {
+ switch (action.destination) {
+ case 'claude-md':
+ return '── Suggested CLAUDE.md addition (permanent rule) '.padEnd(64, '─')
+ case 'session-opener':
+ return '── One-time session opener (do not add to CLAUDE.md) '.padEnd(64, '─')
+ case 'prompt':
+ return '── Ask Claude in the current session '.padEnd(64, '─')
+ case 'shell-config':
+ return '── Add to your shell config '.padEnd(64, '─')
+ default:
+ return '── Suggested action '.padEnd(64, '─')
+ }
+ }
+ }
+}
+
function FindingAction({ action }: { action: WasteAction }) {
const lines = action.type === 'file-content' ? action.content.split('\n') : action.type === 'command' ? action.text.split('\n') : [action.text]
- return (<>{action.label} {lines.map((line, i) => {line} )}>)
+ const header = actionDestinationHeader(action)
+ return (
+ <>
+ {header}
+ {action.label}
+ {lines.map((line, i) => {line} )}
+ >
+ )
}
function FindingPanel({ index, finding, costRate, width }: { index: number; finding: WasteFinding; costRate: number; width: number }) {
@@ -544,13 +612,23 @@ function FindingPanel({ index, finding, costRate, width }: { index: number; find
const GRADE_COLORS: Record = { A: '#5BF5A0', B: '#5BF5A0', C: GOLD, D: ORANGE, F: '#F55B5B' }
-function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade }: { findings: WasteFinding[]; costRate: number; projects: ProjectSummary[]; label: string; width: number; healthScore: number; healthGrade: string }) {
+// Each finding panel takes ~6-8 lines. Show 3 at a time so the window fits a
+// 30-line terminal alongside the optimize header + status bar; users page
+// with j/k. Without this cap, 4 new detectors + 7 originals scrolled findings
+// off the alt-buffer top and the user couldn't see the StatusBar at all.
+const FINDINGS_WINDOW_SIZE = 3
+
+function OptimizeView({ findings, costRate, projects, label, width, healthScore, healthGrade, cursor }: { findings: WasteFinding[]; costRate: number; projects: ProjectSummary[]; label: string; width: number; healthScore: number; healthGrade: string; cursor: number }) {
const periodCost = projects.reduce((s, p) => s + p.totalCostUSD, 0)
const totalTokens = findings.reduce((s, f) => s + f.tokensSaved, 0)
const totalCost = totalTokens * costRate
const pctRaw = periodCost > 0 ? (totalCost / periodCost) * 100 : 0
const pct = pctRaw >= 1 ? pctRaw.toFixed(0) : pctRaw.toFixed(1)
const gradeColor = GRADE_COLORS[healthGrade] ?? DIM
+ const total = findings.length
+ const start = total === 0 ? 0 : Math.min(cursor, Math.max(0, total - FINDINGS_WINDOW_SIZE))
+ const end = Math.min(start + FINDINGS_WINDOW_SIZE, total)
+ const visible = findings.slice(start, end)
return (
@@ -561,29 +639,38 @@ function OptimizeView({ findings, costRate, projects, label, width, healthScore,
({healthScore}/100)
Savings: ~{formatTokens(totalTokens)} tokens (~{formatCost(totalCost)}, ~{pct}% of spend)
+ {total > FINDINGS_WINDOW_SIZE && (
+ Showing {start + 1}–{end} of {total} · j/k to scroll
+ )}
- {findings.map((f, i) => )}
+ {visible.map((f, i) => )}
Token estimates are approximate.
)
}
-function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable, compareAvailable }: { width: number; showProvider?: boolean; view?: View; findingCount?: number; optimizeAvailable?: boolean; compareAvailable?: boolean }) {
+function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable, compareAvailable, customRange }: { width: number; showProvider?: boolean; view?: View; findingCount?: number; optimizeAvailable?: boolean; compareAvailable?: boolean; customRange?: boolean }) {
const isOptimize = view === 'optimize'
return (
{isOptimize
- ? <>b back >
- : <>{'<'} {'>'} switch >}
- q quit
- 1 today
- 2 week
- 3 30 days
- 4 month
- 5 all time
- {!isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && (
- <> o optimize ({findingCount}) >
+ ? <>b back j / k scroll >
+ : !customRange
+ ? <>{'<'} {'>'} switch >
+ : null}
+ q quit
+ {!customRange && !isOptimize && (
+ <>
+ 1 today
+ 2 week
+ 3 30 days
+ 4 month
+ 5 6 months
+ >
+ )}
+ {!isOptimize && optimizeAvailable && (
+ <> o optimize {findingCount != null && findingCount > 0 ? ({findingCount}) : null}>
)}
{!isOptimize && compareAvailable && (
<> c compare >
@@ -620,7 +707,7 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets,
)
}
-function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsage, refreshSeconds, projectFilter, excludeFilter }: {
+function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsage, refreshSeconds, projectFilter, excludeFilter, customRange, customRangeLabel }: {
initialProjects: ProjectSummary[]
initialPeriod: Period
initialProvider: string
@@ -628,6 +715,8 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
refreshSeconds?: number
projectFilter?: string[]
excludeFilter?: string[]
+ customRange?: DateRange | null
+ customRangeLabel?: string
}) {
const { exit } = useApp()
const [period, setPeriod] = useState(initialPeriod)
@@ -637,18 +726,27 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const [detectedProviders, setDetectedProviders] = useState([])
const [view, setView] = useState('dashboard')
const [optimizeResult, setOptimizeResult] = useState(null)
+ const [optimizeLoading, setOptimizeLoading] = useState(false)
const [projectBudgets, setProjectBudgets] = useState>(new Map())
const [planUsage, setPlanUsage] = useState(initialPlanUsage)
+ // Cursor for the OptimizeView's findings window. Reset whenever the user
+ // leaves the optimize view OR the underlying findings change so a long
+ // findings list never strands the user past the new array length.
+ const [findingsCursor, setFindingsCursor] = useState(0)
+ const isCustomRange = customRange != null
const { columns } = useWindowSize()
const { dashWidth } = getLayout(columns)
const multipleProviders = detectedProviders.length > 1
- const optimizeAvailable = activeProvider === 'all' || activeProvider === 'claude'
+ const optimizeAvailable = !isCustomRange && (activeProvider === 'all' || activeProvider === 'claude')
const modelCount = new Set(
projects.flatMap(p => p.sessions.flatMap(s => Object.keys(s.modelBreakdown)))
).size
const compareAvailable = modelCount >= 2
const debounceRef = useRef | null>(null)
const reloadGenerationRef = useRef(0)
+ const reloadInFlightRef = useRef(false)
+ const currentReloadRef = useRef<{ period: Period; provider: string } | null>(null)
+ const pendingReloadRef = useRef<{ period: Period; provider: string } | null>(null)
const findingCount = optimizeResult?.findings.length ?? 0
useEffect(() => {
@@ -665,13 +763,11 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
useEffect(() => {
let cancelled = false
async function loadBudgets() {
- const claudeDir = join(homedir(), '.claude', 'projects')
const budgets = new Map()
for (const project of projects.slice(0, 8)) {
if (cancelled) return
- const cwd = await discoverProjectCwd(join(claudeDir, project.project))
- if (!cwd) continue
- budgets.set(project.project, await estimateContextBudget(cwd))
+ if (!project.projectPath.startsWith('/')) continue
+ budgets.set(project.project, await estimateContextBudget(project.projectPath))
}
if (!cancelled) setProjectBudgets(budgets)
}
@@ -679,24 +775,31 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
return () => { cancelled = true }
}, [projects])
- useEffect(() => {
- if (!optimizeAvailable) { setOptimizeResult(null); return }
- let cancelled = false
- async function scan() {
- if (projects.length === 0) { setOptimizeResult(null); return }
- const result = await scanAndDetect(projects, getDateRange(period))
- if (!cancelled) setOptimizeResult(result)
- }
- scan()
- return () => { cancelled = true }
- }, [projects, period, optimizeAvailable])
-
const reloadData = useCallback(async (p: Period, prov: string) => {
+ if (reloadInFlightRef.current) {
+ const current = currentReloadRef.current
+ if (current?.period === p && current.provider === prov) {
+ pendingReloadRef.current = null
+ return
+ }
+ reloadGenerationRef.current++
+ pendingReloadRef.current = { period: p, provider: prov }
+ return
+ }
+ reloadInFlightRef.current = true
+ currentReloadRef.current = { period: p, provider: prov }
const generation = ++reloadGenerationRef.current
setLoading(true)
+ setOptimizeLoading(false)
setOptimizeResult(null)
try {
- const range = getDateRange(p)
+ if (isHeavyPeriod(p)) {
+ setProjects([])
+ setProjectBudgets(new Map())
+ await nextTick()
+ if (reloadGenerationRef.current !== generation) return
+ }
+ const range = getPeriodRange(p)
const data = await parseAllSessions(range, prov)
if (reloadGenerationRef.current !== generation) return
@@ -713,18 +816,51 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
if (reloadGenerationRef.current === generation) {
setLoading(false)
}
+ reloadInFlightRef.current = false
+ currentReloadRef.current = null
+ const pending = pendingReloadRef.current
+ pendingReloadRef.current = null
+ if (pending) {
+ void reloadData(pending.period, pending.provider)
+ }
}
}, [projectFilter, excludeFilter])
+ const loadOptimizeResult = useCallback(async () => {
+ if (!optimizeAvailable || projects.length === 0 || optimizeLoading) return
+ setView('optimize')
+ setFindingsCursor(0)
+ if (optimizeResult) return
+
+ const generation = reloadGenerationRef.current
+ setOptimizeLoading(true)
+ try {
+ const result = await scanAndDetect(projects, getPeriodRange(period))
+ if (reloadGenerationRef.current === generation) setOptimizeResult(result)
+ } catch (error) {
+ console.error(error)
+ } finally {
+ if (reloadGenerationRef.current === generation) setOptimizeLoading(false)
+ }
+ }, [optimizeAvailable, projects, period, optimizeLoading, optimizeResult])
+
useEffect(() => {
if (!refreshSeconds || refreshSeconds <= 0) return
+ if (isHeavyPeriod(period)) return
const id = setInterval(() => { reloadData(period, activeProvider) }, refreshSeconds * 1000)
return () => clearInterval(id)
}, [refreshSeconds, period, activeProvider, reloadData])
const switchPeriod = useCallback((np: Period) => {
if (np === period) return
+ // Clear projects + flip loading synchronously so the dashboard never
+ // renders the new period label over the old period's numbers between
+ // setPeriod() and the reloadData() promise resolving. Without this,
+ // there's a frame-to-hundreds-of-ms window where users saw wrong
+ // figures captioned with the new period.
setPeriod(np)
+ setProjects([])
+ setLoading(true)
if (debounceRef.current) clearTimeout(debounceRef.current)
debounceRef.current = setTimeout(() => { reloadData(np, activeProvider) }, 600)
}, [period, activeProvider, reloadData])
@@ -732,21 +868,40 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
const switchPeriodImmediate = useCallback(async (np: Period) => {
if (np === period) return
setPeriod(np)
+ setProjects([])
+ setLoading(true)
if (debounceRef.current) clearTimeout(debounceRef.current)
await reloadData(np, activeProvider)
}, [period, activeProvider, reloadData])
useInput((input, key) => {
if (input === 'q') { exit(); return }
- if (input === 'o' && findingCount > 0 && view === 'dashboard' && optimizeAvailable) { setView('optimize'); return }
- if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); return }
+ if (input === 'o' && view === 'dashboard' && optimizeAvailable) { void loadOptimizeResult(); return }
+ if ((input === 'b' || key.escape) && view === 'optimize') { setView('dashboard'); setFindingsCursor(0); return }
+ if (view === 'optimize') {
+ const total = optimizeResult?.findings.length ?? 0
+ const maxStart = Math.max(0, total - FINDINGS_WINDOW_SIZE)
+ if (input === 'j' || key.downArrow) { setFindingsCursor(c => Math.min(c + 1, maxStart)); return }
+ if (input === 'k' || key.upArrow) { setFindingsCursor(c => Math.max(c - 1, 0)); return }
+ }
if (input === 'c' && compareAvailable && view === 'dashboard') { setView('compare'); return }
+ if ((input === 'b' || key.escape) && view === 'compare') { setView('dashboard'); return }
if (input === 'p' && multipleProviders && view !== 'compare') {
const opts = ['all', ...detectedProviders]; const next = opts[(opts.indexOf(activeProvider) + 1) % opts.length]
setActiveProvider(next); setView('dashboard')
if (debounceRef.current) clearTimeout(debounceRef.current)
reloadData(period, next); return
}
+ // Period switches reload the underlying data. Disable them while the
+ // compare view is mounted; the compare view re-aggregates from
+ // `projects` and would visibly change underneath the user without any
+ // affordance back to the dashboard. Press `b` or Esc to return first.
+ if (view === 'compare') return
+ // Also disable while a custom --from/--to range is in effect. Switching
+ // period would silently abandon the user's explicit range and reload
+ // standard period data; the period tab strip is hidden in this mode so
+ // users have no expectation that 1-5 should do anything.
+ if (isCustomRange) return
const idx = PERIODS.indexOf(period)
if (key.leftArrow) switchPeriod(PERIODS[(idx - 1 + PERIODS.length) % PERIODS.length]!)
else if (key.rightArrow || key.tab) switchPeriod(PERIODS[(idx + 1) % PERIODS.length]!)
@@ -757,33 +912,48 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider,
else if (input === '5') switchPeriodImmediate('all')
})
- if (loading) {
+ const headerLabel = customRangeLabel ?? PERIOD_LABELS[period]
+
+ if (loading || optimizeLoading) {
return (
-
+ {!isCustomRange && }
+ {isCustomRange && }
{view === 'compare'
?
Model Comparison
- Loading {PERIOD_LABELS[period]} model data...
+ Loading {headerLabel} model data...
- : Loading {PERIOD_LABELS[period]}... }
- {view !== 'compare' && }
+ : view === 'optimize'
+ ? Scanning {headerLabel}...
+ : Loading {headerLabel}... }
+ {view !== 'compare' && }
)
}
return (
-
+ {!isCustomRange && }
+ {isCustomRange && }
{view === 'compare'
? setView('dashboard')} />
: view === 'optimize' && optimizeResult
- ?
+ ?
: }
- {view !== 'compare' && }
+ {view !== 'compare' && }
+
+ )
+}
+
+function CustomRangeBanner({ label, width }: { label: string; width: number }) {
+ return (
+
+ Custom range:
+ {label}
)
}
@@ -799,15 +969,16 @@ function StaticDashboard({ projects, period, activeProvider, planUsage }: { proj
)
}
-export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[], customRange?: DateRange | null): Promise {
+export async function renderDashboard(period: Period = 'week', provider: string = 'all', refreshSeconds?: number, projectFilter?: string[], excludeFilter?: string[], customRange?: DateRange | null, customRangeLabel?: string): Promise {
await loadPricing()
- const range = customRange ?? getDateRange(period)
+ const range = customRange ?? getPeriodRange(period)
const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter)
const planUsage = await getPlanUsageOrNull()
const isTTY = process.stdin.isTTY && process.stdout.isTTY
+ patchStdoutForWindows()
if (isTTY) {
const { waitUntilExit } = render(
-
+
)
await waitUntilExit()
} else {
diff --git a/src/data/litellm-snapshot.json b/src/data/litellm-snapshot.json
index 2ec14bd..7a7ec4a 100644
--- a/src/data/litellm-snapshot.json
+++ b/src/data/litellm-snapshot.json
@@ -1 +1 @@
-{"ai21.j2-mid-v1":[0.0000125,0.0000125,null,null],"ai21.j2-ultra-v1":[0.0000188,0.0000188,null,null],"ai21.jamba-1-5-large-v1:0":[0.000002,0.000008,null,null],"ai21.jamba-1-5-mini-v1:0":[2e-7,4e-7,null,null],"ai21.jamba-instruct-v1:0":[5e-7,7e-7,null,null],"us.writer.palmyra-x4-v1:0":[0.0000025,0.00001,null,null],"us.writer.palmyra-x5-v1:0":[6e-7,0.000006,null,null],"writer.palmyra-x4-v1:0":[0.0000025,0.00001,null,null],"writer.palmyra-x5-v1:0":[6e-7,0.000006,null,null],"amazon.nova-lite-v1:0":[6e-8,2.4e-7,null,null],"amazon.nova-2-lite-v1:0":[3e-7,0.0000025,null,7.5e-8],"amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"apac.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"apac.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"eu.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"eu.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"us.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"us.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"amazon.nova-2-multimodal-embeddings-v1:0":[1.35e-7,0,null,null],"amazon.nova-micro-v1:0":[3.5e-8,1.4e-7,null,null],"amazon.nova-pro-v1:0":[8e-7,0.0000032,null,null],"amazon.rerank-v1:0":[0,0,null,null],"amazon.titan-embed-image-v1":[8e-7,0,null,null],"amazon.titan-embed-text-v1":[1e-7,0,null,null],"amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"us.twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"eu.twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"anthropic.claude-haiku-4-5-20251001-v1:0":[0.000001,0.000005,0.00000125,1e-7],"anthropic.claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-7-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"anthropic.claude-opus-4-6-v1":[0.000005,0.000025,0.00000625,5e-7],"global.anthropic.claude-opus-4-6-v1":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"eu.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"au.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"anthropic.claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"anthropic.claude-mythos-preview":[0,0,null,null],"global.anthropic.claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"eu.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"au.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"anthropic.claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"eu.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"au.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-v1":[0.000008,0.000024,null,null],"anthropic.claude-v2:1":[0.000008,0.000024,null,null],"apac.amazon.nova-lite-v1:0":[6.3e-8,2.52e-7,null,null],"apac.amazon.nova-micro-v1:0":[3.7e-8,1.48e-7,null,null],"apac.amazon.nova-pro-v1:0":[8.4e-7,0.00000336,null,null],"apac.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"apac.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"apac.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"au.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"babbage-002":[4e-7,4e-7,null,null],"chatdolphin":[5e-7,5e-7,null,null],"chatgpt-4o-latest":[0.000005,0.000015,null,null],"gpt-4o-transcribe-diarize":[0.0000025,0.00001,null,null],"claude-haiku-4-5-20251001":[0.000001,0.000005,0.00000125,1e-7],"claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"claude-3-7-sonnet-20250219":[0.000003,0.000015,0.00000375,3e-7],"claude-3-haiku-20240307":[2.5e-7,0.00000125,3e-7,3e-8],"claude-3-opus-20240229":[0.000015,0.000075,0.00001875,0.0000015],"claude-4-opus-20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-4-sonnet-20250514":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5-20250929":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-1-20250805":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-5-20251101":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6-20260205":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7-20260416":[0.000005,0.000025,0.00000625,5e-7],"claude-sonnet-4-20250514":[0.000003,0.000015,0.00000375,3e-7],"codex-mini-latest":[0.0000015,0.000006,null,3.75e-7],"cohere.command-light-text-v14":[3e-7,6e-7,null,null],"cohere.command-r-plus-v1:0":[0.000003,0.000015,null,null],"cohere.command-r-v1:0":[5e-7,0.0000015,null,null],"cohere.command-text-v14":[0.0000015,0.000002,null,null],"cohere.embed-english-v3":[1e-7,0,null,null],"cohere.embed-multilingual-v3":[1e-7,0,null,null],"cohere.embed-v4:0":[1.2e-7,0,null,null],"cohere.rerank-v3-5:0":[0,0,null,null],"command":[0.000001,0.000002,null,null],"command-a-03-2025":[0.0000025,0.00001,null,null],"command-light":[3e-7,6e-7,null,null],"command-nightly":[0.000001,0.000002,null,null],"command-r":[1.5e-7,6e-7,null,null],"command-r-08-2024":[1.5e-7,6e-7,null,null],"command-r-plus":[0.0000025,0.00001,null,null],"command-r-plus-08-2024":[0.0000025,0.00001,null,null],"command-r7b-12-2024":[1.5e-7,3.75e-8,null,null],"computer-use-preview":[0.000003,0.000012,null,null],"deepseek-chat":[2.8e-7,4.2e-7,null,2.8e-8],"deepseek-reasoner":[2.8e-7,4.2e-7,null,2.8e-8],"davinci-002":[0.000002,0.000002,null,null],"deepseek.v3-v1:0":[5.8e-7,0.00000168,null,null],"deepseek.v3.2":[6.2e-7,0.00000185,null,null],"dolphin":[5e-7,5e-7,null,null],"deepseek-v3-2-251201":[0,0,null,null],"glm-4-7-251222":[0,0,null,null],"kimi-k2-thinking-251104":[0,0,null,null],"doubao-embedding":[0,0,null,null],"doubao-embedding-large":[0,0,null,null],"doubao-embedding-large-text-240915":[0,0,null,null],"doubao-embedding-large-text-250515":[0,0,null,null],"doubao-embedding-text-240715":[0,0,null,null],"embed-english-light-v2.0":[1e-7,0,null,null],"embed-english-light-v3.0":[1e-7,0,null,null],"embed-english-v2.0":[1e-7,0,null,null],"embed-english-v3.0":[1e-7,0,null,null],"embed-multilingual-v2.0":[1e-7,0,null,null],"embed-multilingual-v3.0":[1e-7,0,null,null],"embed-multilingual-light-v3.0":[0.0001,0,null,null],"eu.amazon.nova-lite-v1:0":[7.8e-8,3.12e-7,null,null],"eu.amazon.nova-micro-v1:0":[4.6e-8,1.84e-7,null,null],"eu.amazon.nova-pro-v1:0":[0.00000105,0.0000042,null,null],"eu.anthropic.claude-3-5-haiku-20241022-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"eu.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"eu.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"eu.anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"eu.meta.llama3-2-1b-instruct-v1:0":[1.3e-7,1.3e-7,null,null],"eu.meta.llama3-2-3b-instruct-v1:0":[1.9e-7,1.9e-7,null,null],"eu.mistral.pixtral-large-2502-v1:0":[0.000002,0.000006,null,null],"fireworks-ai-4.1b-to-16b":[2e-7,2e-7,null,null],"fireworks-ai-56b-to-176b":[0.0000012,0.0000012,null,null],"fireworks-ai-above-16b":[9e-7,9e-7,null,null],"fireworks-ai-default":[0,0,null,null],"fireworks-ai-embedding-150m-to-350m":[1.6e-8,0,null,null],"fireworks-ai-embedding-up-to-150m":[8e-9,0,null,null],"fireworks-ai-moe-up-to-56b":[5e-7,5e-7,null,null],"fireworks-ai-up-to-4b":[2e-7,2e-7,null,null],"ft:babbage-002":[0.0000016,0.0000016,null,null],"ft:davinci-002":[0.000012,0.000012,null,null],"ft:gpt-3.5-turbo":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-0125":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-0613":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-1106":[0.000003,0.000006,null,null],"ft:gpt-4-0613":[0.00003,0.00006,null,null],"ft:gpt-4o-2024-08-06":[0.00000375,0.000015,null,0.000001875],"ft:gpt-4o-2024-11-20":[0.00000375,0.000015,0.000001875,null],"ft:gpt-4o-mini-2024-07-18":[3e-7,0.0000012,null,1.5e-7],"ft:gpt-4.1-2025-04-14":[0.000003,0.000012,null,7.5e-7],"ft:gpt-4.1-mini-2025-04-14":[8e-7,0.0000032,null,2e-7],"ft:gpt-4.1-nano-2025-04-14":[2e-7,8e-7,null,5e-8],"ft:o4-mini-2025-04-16":[0.000004,0.000016,null,0.000001],"gemini-2.0-flash":[1e-7,4e-7,null,2.5e-8],"gemini-2.0-flash-001":[1.5e-7,6e-7,null,3.75e-8],"gemini-2.0-flash-lite":[7.5e-8,3e-7,null,1.875e-8],"gemini-2.0-flash-lite-001":[7.5e-8,3e-7,null,1.875e-8],"gemini-2.5-flash":[3e-7,0.0000025,null,3e-8],"gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"gemini-3.1-flash-image-preview":[5e-7,0.000003,null,null],"gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"gemini-2.5-flash-lite":[1e-7,4e-7,null,1e-8],"gemini-2.5-flash-lite-preview-09-2025":[1e-7,4e-7,null,1e-8],"gemini-2.5-flash-preview-09-2025":[3e-7,0.0000025,null,7.5e-8],"gemini-live-2.5-flash-preview-native-audio-09-2025":[3e-7,0.000002,null,7.5e-8],"gemini-2.5-flash-lite-preview-06-17":[1e-7,4e-7,null,2.5e-8],"gemini-2.5-pro":[0.00000125,0.00001,null,1.25e-7],"gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini-2.5-pro-preview-tts":[0.00000125,0.00001,null,1.25e-7],"gemini-robotics-er-1.5-preview":[3e-7,0.0000025,null,0],"gemini-2.5-computer-use-preview-10-2025":[0.00000125,0.00001,null,null],"gemini-embedding-001":[1.5e-7,0,null,null],"gemini-embedding-2-preview":[2e-7,0,null,null],"gemini-embedding-2":[2e-7,0,null,null],"gemini-flash-experimental":[0,0,null,null],"gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"google.gemma-3-12b-it":[9e-8,2.9e-7,null,null],"google.gemma-3-27b-it":[2.3e-7,3.8e-7,null,null],"google.gemma-3-4b-it":[4e-8,8e-8,null,null],"global.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-haiku-4-5-20251001-v1:0":[0.000001,0.000005,0.00000125,1e-7],"global.amazon.nova-2-lite-v1:0":[3e-7,0.0000025,null,7.5e-8],"gpt-3.5-turbo":[5e-7,0.0000015,null,null],"gpt-3.5-turbo-0125":[5e-7,0.0000015,null,null],"gpt-3.5-turbo-1106":[0.000001,0.000002,null,null],"gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"gpt-3.5-turbo-instruct-0914":[0.0000015,0.000002,null,null],"gpt-4":[0.00003,0.00006,null,null],"gpt-4-0125-preview":[0.00001,0.00003,null,null],"gpt-4-0314":[0.00003,0.00006,null,null],"gpt-4-0613":[0.00003,0.00006,null,null],"gpt-4-1106-preview":[0.00001,0.00003,null,null],"gpt-4-turbo":[0.00001,0.00003,null,null],"gpt-4-turbo-2024-04-09":[0.00001,0.00003,null,null],"gpt-4-turbo-preview":[0.00001,0.00003,null,null],"gpt-4.1":[0.000002,0.000008,null,5e-7],"gpt-4.1-2025-04-14":[0.000002,0.000008,null,5e-7],"gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"gpt-4.1-mini-2025-04-14":[4e-7,0.0000016,null,1e-7],"gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"gpt-4.1-nano-2025-04-14":[1e-7,4e-7,null,2.5e-8],"gpt-4o":[0.0000025,0.00001,null,0.00000125],"gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"gpt-4o-audio-preview":[0.0000025,0.00001,null,null],"gpt-4o-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"gpt-4o-audio-preview-2025-06-03":[0.0000025,0.00001,null,null],"gpt-audio":[0.0000025,0.00001,null,null],"gpt-audio-1.5":[0.0000025,0.00001,null,null],"gpt-audio-2025-08-28":[0.0000025,0.00001,null,null],"gpt-audio-mini":[6e-7,0.0000024,null,null],"gpt-audio-mini-2025-10-06":[6e-7,0.0000024,null,null],"gpt-audio-mini-2025-12-15":[6e-7,0.0000024,null,null],"gpt-4o-mini":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-2024-07-18":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-audio-preview":[1.5e-7,6e-7,null,null],"gpt-4o-mini-audio-preview-2024-12-17":[1.5e-7,6e-7,null,null],"gpt-4o-mini-realtime-preview":[6e-7,0.0000024,null,3e-7],"gpt-4o-mini-realtime-preview-2024-12-17":[6e-7,0.0000024,null,3e-7],"gpt-4o-mini-search-preview":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-search-preview-2025-03-11":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-transcribe":[0.00000125,0.000005,null,null],"gpt-4o-mini-tts":[0.0000025,0.00001,null,null],"gpt-4o-realtime-preview":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2024-12-17":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2025-06-03":[0.000005,0.00002,null,0.0000025],"gpt-4o-search-preview":[0.0000025,0.00001,null,0.00000125],"gpt-4o-search-preview-2025-03-11":[0.0000025,0.00001,null,0.00000125],"gpt-4o-transcribe":[0.0000025,0.00001,null,null],"gpt-image-1.5":[0.000005,0.00001,null,0.00000125],"gpt-image-1.5-2025-12-16":[0.000005,0.00001,null,0.00000125],"gpt-5":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat-latest":[0.00000125,0.00001,null,1.25e-7],"gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat-latest":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-chat-latest":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-pro":[0.000021,0.000168,null,null],"gpt-5.2-pro-2025-12-11":[0.000021,0.000168,null,null],"gpt-5.5":[0.000005,0.00003,null,5e-7],"gpt-5.5-2026-04-23":[0.000005,0.00003,null,5e-7],"gpt-5.5-pro":[0.00006,0.00036,null,0.000006],"gpt-5.5-pro-2026-04-23":[0.00006,0.00036,null,0.000006],"gpt-5.4":[0.0000025,0.000015,null,2.5e-7],"gpt-5.4-2026-03-05":[0.0000025,0.000015,null,2.5e-7],"gpt-5.4-pro":[0.00003,0.00018,null,0.000003],"gpt-5.4-pro-2026-03-05":[0.00003,0.00018,null,0.000003],"gpt-5.4-mini":[7.5e-7,0.0000045,null,7.5e-8],"gpt-5.4-mini-2026-03-17":[7.5e-7,0.0000045,null,7.5e-8],"gpt-5.4-nano":[2e-7,0.00000125,null,2e-8],"gpt-5.4-nano-2026-03-17":[2e-7,0.00000125,null,2e-8],"gpt-5-pro":[0.000015,0.00012,null,null],"gpt-5-pro-2025-10-06":[0.000015,0.00012,null,null],"gpt-5-2025-08-07":[0.00000125,0.00001,null,1.25e-7],"gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"gpt-5-chat-latest":[0.00000125,0.00001,null,1.25e-7],"gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-codex":[0.00000175,0.000014,null,1.75e-7],"gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"gpt-5-mini-2025-08-07":[2.5e-7,0.000002,null,2.5e-8],"gpt-5-nano":[5e-8,4e-7,null,5e-9],"gpt-5-nano-2025-08-07":[5e-8,4e-7,null,5e-9],"gpt-realtime":[0.000004,0.000016,null,4e-7],"gpt-realtime-1.5":[0.000004,0.000016,null,4e-7],"gpt-realtime-mini":[6e-7,0.0000024,null,null],"gpt-realtime-2025-08-28":[0.000004,0.000016,null,4e-7],"j2-light":[0.000003,0.000003,null,null],"j2-mid":[0.00001,0.00001,null,null],"j2-ultra":[0.000015,0.000015,null,null],"jamba-1.5":[2e-7,4e-7,null,null],"jamba-1.5-large":[0.000002,0.000008,null,null],"jamba-1.5-large@001":[0.000002,0.000008,null,null],"jamba-1.5-mini":[2e-7,4e-7,null,null],"jamba-1.5-mini@001":[2e-7,4e-7,null,null],"jamba-large-1.6":[0.000002,0.000008,null,null],"jamba-large-1.7":[0.000002,0.000008,null,null],"jamba-mini-1.6":[2e-7,4e-7,null,null],"jamba-mini-1.7":[2e-7,4e-7,null,null],"jina-reranker-v2-base-multilingual":[1.8e-8,1.8e-8,null,null],"jp.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"jp.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"meta.llama2-13b-chat-v1":[7.5e-7,0.000001,null,null],"meta.llama2-70b-chat-v1":[0.00000195,0.00000256,null,null],"meta.llama3-1-405b-instruct-v1:0":[0.00000532,0.000016,null,null],"meta.llama3-1-70b-instruct-v1:0":[9.9e-7,9.9e-7,null,null],"meta.llama3-1-8b-instruct-v1:0":[2.2e-7,2.2e-7,null,null],"meta.llama3-2-11b-instruct-v1:0":[3.5e-7,3.5e-7,null,null],"meta.llama3-2-1b-instruct-v1:0":[1e-7,1e-7,null,null],"meta.llama3-2-3b-instruct-v1:0":[1.5e-7,1.5e-7,null,null],"meta.llama3-2-90b-instruct-v1:0":[0.000002,0.000002,null,null],"meta.llama3-3-70b-instruct-v1:0":[7.2e-7,7.2e-7,null,null],"meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"meta.llama4-maverick-17b-instruct-v1:0":[2.4e-7,9.7e-7,null,null],"meta.llama4-scout-17b-instruct-v1:0":[1.7e-7,6.6e-7,null,null],"minimax.minimax-m2":[3e-7,0.0000012,null,null],"minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"mistral.devstral-2-123b":[4e-7,0.000002,null,null],"mistral.magistral-small-2509":[5e-7,0.0000015,null,null],"mistral.ministral-3-14b-instruct":[2e-7,2e-7,null,null],"mistral.ministral-3-3b-instruct":[1e-7,1e-7,null,null],"mistral.ministral-3-8b-instruct":[1.5e-7,1.5e-7,null,null],"mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"mistral.mistral-large-2407-v1:0":[0.000003,0.000009,null,null],"mistral.mistral-large-3-675b-instruct":[5e-7,0.0000015,null,null],"mistral.mistral-small-2402-v1:0":[0.000001,0.000003,null,null],"mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"mistral.voxtral-mini-3b-2507":[4e-8,4e-8,null,null],"mistral.voxtral-small-24b-2507":[1e-7,3e-7,null,null],"moonshot.kimi-k2-thinking":[6e-7,0.0000025,null,null],"moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"multimodalembedding":[8e-7,0,null,null],"multimodalembedding@001":[8e-7,0,null,null],"nvidia.nemotron-nano-12b-v2":[2e-7,6e-7,null,null],"nvidia.nemotron-nano-9b-v2":[6e-8,2.3e-7,null,null],"nvidia.nemotron-nano-3-30b":[6e-8,2.4e-7,null,null],"nvidia.nemotron-super-3-120b":[1.5e-7,6.5e-7,null,null],"o1":[0.000015,0.00006,null,0.0000075],"o1-2024-12-17":[0.000015,0.00006,null,0.0000075],"o1-pro":[0.00015,0.0006,null,null],"o1-pro-2025-03-19":[0.00015,0.0006,null,null],"o3":[0.000002,0.000008,null,5e-7],"o3-2025-04-16":[0.000002,0.000008,null,5e-7],"o3-deep-research":[0.00001,0.00004,null,0.0000025],"o3-deep-research-2025-06-26":[0.00001,0.00004,null,0.0000025],"o3-mini":[0.0000011,0.0000044,null,5.5e-7],"o3-mini-2025-01-31":[0.0000011,0.0000044,null,5.5e-7],"o3-pro":[0.00002,0.00008,null,null],"o3-pro-2025-06-10":[0.00002,0.00008,null,null],"o4-mini":[0.0000011,0.0000044,null,2.75e-7],"o4-mini-2025-04-16":[0.0000011,0.0000044,null,2.75e-7],"o4-mini-deep-research":[0.000002,0.000008,null,5e-7],"o4-mini-deep-research-2025-06-26":[0.000002,0.000008,null,5e-7],"omni-moderation-2024-09-26":[0,0,null,null],"omni-moderation-latest":[0,0,null,null],"openai.gpt-oss-120b-1:0":[1.5e-7,6e-7,null,null],"openai.gpt-oss-20b-1:0":[7e-8,3e-7,null,null],"openai.gpt-oss-safeguard-120b":[1.5e-7,6e-7,null,null],"openai.gpt-oss-safeguard-20b":[7e-8,2e-7,null,null],"qwen.qwen3-coder-480b-a35b-v1:0":[2.2e-7,0.0000018,null,null],"qwen.qwen3-235b-a22b-2507-v1:0":[2.2e-7,8.8e-7,null,null],"qwen.qwen3-coder-30b-a3b-v1:0":[1.5e-7,6e-7,null,null],"qwen.qwen3-32b-v1:0":[1.5e-7,6e-7,null,null],"qwen.qwen3-next-80b-a3b":[1.5e-7,0.0000012,null,null],"qwen.qwen3-vl-235b-a22b":[5.3e-7,0.00000266,null,null],"qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"rerank-english-v2.0":[0,0,null,null],"rerank-english-v3.0":[0,0,null,null],"rerank-multilingual-v2.0":[0,0,null,null],"rerank-multilingual-v3.0":[0,0,null,null],"rerank-v3.5":[0,0,null,null],"text-embedding-004":[1e-7,0,null,null],"text-embedding-005":[1e-7,0,null,null],"text-embedding-3-large":[1.3e-7,0,null,null],"text-embedding-3-small":[2e-8,0,null,null],"text-embedding-ada-002":[1e-7,0,null,null],"text-embedding-ada-002-v2":[1e-7,0,null,null],"text-embedding-large-exp-03-07":[1e-7,0,null,null],"text-embedding-preview-0409":[6.25e-9,0,null,null],"text-moderation-007":[0,0,null,null],"text-moderation-latest":[0,0,null,null],"text-moderation-stable":[0,0,null,null],"text-multilingual-embedding-002":[1e-7,0,null,null],"text-unicorn":[0.00001,0.000028,null,null],"text-unicorn@001":[0.00001,0.000028,null,null],"together-ai-21.1b-41b":[8e-7,8e-7,null,null],"together-ai-4.1b-8b":[2e-7,2e-7,null,null],"together-ai-41.1b-80b":[9e-7,9e-7,null,null],"together-ai-8.1b-21b":[3e-7,3e-7,null,null],"together-ai-81.1b-110b":[0.0000018,0.0000018,null,null],"together-ai-embedding-151m-to-350m":[1.6e-8,0,null,null],"together-ai-embedding-up-to-150m":[8e-9,0,null,null],"together-ai-up-to-4b":[1e-7,1e-7,null,null],"us.amazon.nova-lite-v1:0":[6e-8,2.4e-7,null,null],"us.amazon.nova-micro-v1:0":[3.5e-8,1.4e-7,null,null],"us.amazon.nova-premier-v1:0":[0.0000025,0.0000125,null,null],"us.amazon.nova-pro-v1:0":[8e-7,0.0000032,null,null],"us.anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"us.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"us.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"us.anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"au.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"us.anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-opus-4-5-20251101-v1:0":[0.0000055,0.0000275,0.000006875,5.5e-7],"global.anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"eu.anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.deepseek.r1-v1:0":[0.00000135,0.0000054,null,null],"us.deepseek.v3.2":[6.2e-7,0.00000185,null,null],"eu.deepseek.v3.2":[7.4e-7,0.00000222,null,null],"us.meta.llama3-1-405b-instruct-v1:0":[0.00000532,0.000016,null,null],"us.meta.llama3-1-70b-instruct-v1:0":[9.9e-7,9.9e-7,null,null],"us.meta.llama3-1-8b-instruct-v1:0":[2.2e-7,2.2e-7,null,null],"us.meta.llama3-2-11b-instruct-v1:0":[3.5e-7,3.5e-7,null,null],"us.meta.llama3-2-1b-instruct-v1:0":[1e-7,1e-7,null,null],"us.meta.llama3-2-3b-instruct-v1:0":[1.5e-7,1.5e-7,null,null],"us.meta.llama3-2-90b-instruct-v1:0":[0.000002,0.000002,null,null],"us.meta.llama3-3-70b-instruct-v1:0":[7.2e-7,7.2e-7,null,null],"us.meta.llama4-maverick-17b-instruct-v1:0":[2.4e-7,9.7e-7,null,null],"us.meta.llama4-scout-17b-instruct-v1:0":[1.7e-7,6.6e-7,null,null],"us.mistral.pixtral-large-2502-v1:0":[0.000002,0.000006,null,null],"zai.glm-4.7":[6e-7,0.0000022,null,null],"zai.glm-4.7-flash":[7e-8,4e-7,null,null],"zai.glm-5":[0.000001,0.0000032,null,null],"gpt-4o-mini-tts-2025-03-20":[0.0000025,0.00001,null,null],"gpt-4o-mini-tts-2025-12-15":[0.0000025,0.00001,null,null],"gpt-4o-mini-transcribe-2025-03-20":[0.00000125,0.000005,null,null],"gpt-4o-mini-transcribe-2025-12-15":[0.00000125,0.000005,null,null],"gpt-5-search-api":[0.00000125,0.00001,null,1.25e-7],"gpt-5-search-api-2025-10-14":[0.00000125,0.00001,null,1.25e-7],"gpt-realtime-mini-2025-10-06":[6e-7,0.0000024,null,6e-8],"gpt-realtime-mini-2025-12-15":[6e-7,0.0000024,null,6e-8],"gemini-2.0-flash-exp-image-generation":[0,0,null,null],"gemini-2.5-flash-native-audio-latest":[3e-7,0.0000025,null,null],"gemini-2.5-flash-native-audio-preview-09-2025":[3e-7,0.0000025,null,null],"gemini-2.5-flash-native-audio-preview-12-2025":[3e-7,0.0000025,null,null],"gemini-3.1-flash-live-preview":[7.5e-7,0.0000045,null,null],"gemini-2.5-flash-preview-tts":[3e-7,0.0000025,null,null],"gemini-flash-latest":[3e-7,0.0000025,null,3e-8],"gemini-flash-lite-latest":[1e-7,4e-7,null,1e-8],"gemini-pro-latest":[0.00000125,0.00001,null,1.25e-7],"gemini-exp-1206":[3e-7,0.0000025,null,3e-8],"anyscale/HuggingFaceH4/zephyr-7b-beta":[1.5e-7,1.5e-7,null,null],"HuggingFaceH4/zephyr-7b-beta":[1.5e-7,1.5e-7,null,null],"anyscale/codellama/CodeLlama-34b-Instruct-hf":[0.000001,0.000001,null,null],"codellama/CodeLlama-34b-Instruct-hf":[0.000001,0.000001,null,null],"anyscale/codellama/CodeLlama-70b-Instruct-hf":[0.000001,0.000001,null,null],"codellama/CodeLlama-70b-Instruct-hf":[0.000001,0.000001,null,null],"anyscale/google/gemma-7b-it":[1.5e-7,1.5e-7,null,null],"google/gemma-7b-it":[1.5e-7,1.5e-7,null,null],"anyscale/meta-llama/Llama-2-13b-chat-hf":[2.5e-7,2.5e-7,null,null],"meta-llama/Llama-2-13b-chat-hf":[2.5e-7,2.5e-7,null,null],"anyscale/meta-llama/Llama-2-70b-chat-hf":[0.000001,0.000001,null,null],"meta-llama/Llama-2-70b-chat-hf":[0.000001,0.000001,null,null],"anyscale/meta-llama/Llama-2-7b-chat-hf":[1.5e-7,1.5e-7,null,null],"meta-llama/Llama-2-7b-chat-hf":[1.5e-7,1.5e-7,null,null],"anyscale/meta-llama/Meta-Llama-3-70B-Instruct":[0.000001,0.000001,null,null],"meta-llama/Meta-Llama-3-70B-Instruct":[0.000001,0.000001,null,null],"anyscale/meta-llama/Meta-Llama-3-8B-Instruct":[1.5e-7,1.5e-7,null,null],"meta-llama/Meta-Llama-3-8B-Instruct":[1.5e-7,1.5e-7,null,null],"anyscale/mistralai/Mistral-7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"mistralai/Mistral-7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"anyscale/mistralai/Mixtral-8x22B-Instruct-v0.1":[9e-7,9e-7,null,null],"mistralai/Mixtral-8x22B-Instruct-v0.1":[9e-7,9e-7,null,null],"anyscale/mistralai/Mixtral-8x7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"mistralai/Mixtral-8x7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"azure/ada":[1e-7,0,null,null],"ada":[1e-7,0,null,null],"azure/codex-mini":[0.0000015,0.000006,null,3.75e-7],"codex-mini":[0.0000015,0.000006,null,3.75e-7],"azure/command-r-plus":[0.000003,0.000015,null,null],"azure_ai/claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"azure_ai/claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"azure_ai/claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"azure_ai/claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"azure/computer-use-preview":[0.000003,0.000012,null,null],"azure_ai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"gpt-oss-120b":[1.5e-7,6e-7,null,null],"azure_ai/model_router":[1.4e-7,0,null,null],"model_router":[1.4e-7,0,null,null],"azure/eu/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"eu/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"azure/eu/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"eu/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"azure/eu/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"eu/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"azure/eu/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"eu/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"azure/eu/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"eu/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"azure/eu/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"eu/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"azure/eu/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"eu/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"azure/eu/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"eu/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"azure/eu/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"eu/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"azure/eu/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"eu/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"azure/eu/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"eu/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"azure/eu/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"eu/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"azure/eu/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"eu/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"azure/eu/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"eu/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"azure/global-standard/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"global-standard/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/global-standard/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"global-standard/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"azure/global-standard/gpt-4o-mini":[1.5e-7,6e-7,null,null],"global-standard/gpt-4o-mini":[1.5e-7,6e-7,null,null],"azure/global/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"global/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/global/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"global/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"azure/global/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"global/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-3.5-turbo":[5e-7,0.0000015,null,null],"azure/gpt-3.5-turbo-0125":[5e-7,0.0000015,null,null],"azure/gpt-3.5-turbo-instruct-0914":[0.0000015,0.000002,null,null],"azure/gpt-35-turbo":[5e-7,0.0000015,null,null],"gpt-35-turbo":[5e-7,0.0000015,null,null],"azure/gpt-35-turbo-0125":[5e-7,0.0000015,null,null],"gpt-35-turbo-0125":[5e-7,0.0000015,null,null],"azure/gpt-35-turbo-1106":[0.000001,0.000002,null,null],"gpt-35-turbo-1106":[0.000001,0.000002,null,null],"azure/gpt-35-turbo-16k":[0.000003,0.000004,null,null],"gpt-35-turbo-16k":[0.000003,0.000004,null,null],"azure/gpt-35-turbo-16k-0613":[0.000003,0.000004,null,null],"gpt-35-turbo-16k-0613":[0.000003,0.000004,null,null],"azure/gpt-35-turbo-instruct":[0.0000015,0.000002,null,null],"gpt-35-turbo-instruct":[0.0000015,0.000002,null,null],"azure/gpt-35-turbo-instruct-0914":[0.0000015,0.000002,null,null],"gpt-35-turbo-instruct-0914":[0.0000015,0.000002,null,null],"azure/gpt-4":[0.00003,0.00006,null,null],"azure/gpt-4-0125-preview":[0.00001,0.00003,null,null],"azure/gpt-4-0613":[0.00003,0.00006,null,null],"azure/gpt-4-1106-preview":[0.00001,0.00003,null,null],"azure/gpt-4-32k":[0.00006,0.00012,null,null],"gpt-4-32k":[0.00006,0.00012,null,null],"azure/gpt-4-32k-0613":[0.00006,0.00012,null,null],"gpt-4-32k-0613":[0.00006,0.00012,null,null],"azure/gpt-4-turbo":[0.00001,0.00003,null,null],"azure/gpt-4-turbo-2024-04-09":[0.00001,0.00003,null,null],"azure/gpt-4-turbo-vision-preview":[0.00001,0.00003,null,null],"gpt-4-turbo-vision-preview":[0.00001,0.00003,null,null],"azure/gpt-4.1":[0.000002,0.000008,null,5e-7],"azure/gpt-4.1-2025-04-14":[0.000002,0.000008,null,5e-7],"azure/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"azure/gpt-4.1-mini-2025-04-14":[4e-7,0.0000016,null,1e-7],"azure/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"azure/gpt-4.1-nano-2025-04-14":[1e-7,4e-7,null,2.5e-8],"azure/gpt-4.5-preview":[0.000075,0.00015,null,0.0000375],"gpt-4.5-preview":[0.000075,0.00015,null,0.0000375],"azure/gpt-4o":[0.0000025,0.00001,null,0.00000125],"azure/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"azure/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/gpt-4o-2024-11-20":[0.00000275,0.000011,null,0.00000125],"azure/gpt-audio-2025-08-28":[0.0000025,0.00001,null,null],"azure/gpt-audio-1.5-2026-02-23":[0.0000025,0.00001,null,null],"gpt-audio-1.5-2026-02-23":[0.0000025,0.00001,null,null],"azure/gpt-audio-mini-2025-10-06":[6e-7,0.0000024,null,null],"azure/gpt-4o-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"azure/gpt-4o-mini":[1.65e-7,6.6e-7,null,7.5e-8],"azure/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,7.5e-8],"azure/gpt-4o-mini-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"azure/gpt-4o-mini-realtime-preview-2024-12-17":[6e-7,0.0000024,null,3e-7],"azure/gpt-realtime-2025-08-28":[0.000004,0.000016,null,0.000004],"azure/gpt-realtime-1.5-2026-02-23":[0.000004,0.000016,null,0.000004],"gpt-realtime-1.5-2026-02-23":[0.000004,0.000016,null,0.000004],"azure/gpt-realtime-mini-2025-10-06":[6e-7,0.0000024,null,6e-8],"azure/gpt-4o-mini-transcribe":[0.00000125,0.000005,null,null],"azure/gpt-4o-mini-tts":[0.0000025,0.00001,null,null],"azure/gpt-4o-realtime-preview-2024-10-01":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2024-10-01":[0.000005,0.00002,null,0.0000025],"azure/gpt-4o-realtime-preview-2024-12-17":[0.000005,0.00002,null,0.0000025],"azure/gpt-4o-transcribe":[0.0000025,0.00001,null,null],"azure/gpt-4o-transcribe-diarize":[0.0000025,0.00001,null,null],"azure/gpt-5.1-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-chat-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-mini-2025-11-13":[2.5e-7,0.000002,null,2.5e-8],"gpt-5.1-codex-mini-2025-11-13":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-2025-08-07":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-chat-latest":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5-mini-2025-08-07":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5-nano":[5e-8,4e-7,null,5e-9],"azure/gpt-5-nano-2025-08-07":[5e-8,4e-7,null,5e-9],"azure/gpt-5-pro":[0.000015,0.00012,null,null],"azure/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-chat-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.3-chat":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-chat":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.3-codex":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-pro":[0.000021,0.000168,null,null],"azure/gpt-5.2-pro-2025-12-11":[0.000021,0.000168,null,null],"azure/gpt-5.4":[0.0000025,0.000015,null,2.5e-7],"azure/gpt-5.4-2026-03-05":[0.0000025,0.000015,null,2.5e-7],"azure/gpt-5.4-pro":[0.00003,0.00018,null,0.000003],"azure/gpt-5.4-pro-2026-03-05":[0.00003,0.00018,null,0.000003],"azure/gpt-5.5":[0.000005,0.00003,null,5e-7],"azure/gpt-5.5-2026-04-23":[0.000005,0.00003,null,5e-7],"azure/gpt-5.5-pro":[0.00006,0.00036,null,0.000006],"azure/gpt-5.5-pro-2026-04-23":[0.00006,0.00036,null,0.000006],"azure/gpt-5.4-mini":[7.5e-7,0.0000045,null,7.5e-8],"azure/gpt-5.4-mini-2026-03-17":[7.5e-7,0.0000045,null,7.5e-8],"azure/gpt-5.4-nano":[2e-7,0.00000125,null,2e-8],"azure/gpt-5.4-nano-2026-03-17":[2e-7,0.00000125,null,2e-8],"azure/mistral-large-2402":[0.000008,0.000024,null,null],"mistral-large-2402":[0.000008,0.000024,null,null],"azure/mistral-large-latest":[0.000008,0.000024,null,null],"mistral-large-latest":[0.000008,0.000024,null,null],"azure/o1":[0.000015,0.00006,null,0.0000075],"azure/o1-2024-12-17":[0.000015,0.00006,null,0.0000075],"azure/o1-mini":[0.00000121,0.00000484,null,6.05e-7],"o1-mini":[0.00000121,0.00000484,null,6.05e-7],"azure/o1-mini-2024-09-12":[0.0000011,0.0000044,null,5.5e-7],"o1-mini-2024-09-12":[0.0000011,0.0000044,null,5.5e-7],"azure/o1-preview":[0.000015,0.00006,null,0.0000075],"o1-preview":[0.000015,0.00006,null,0.0000075],"azure/o1-preview-2024-09-12":[0.000015,0.00006,null,0.0000075],"o1-preview-2024-09-12":[0.000015,0.00006,null,0.0000075],"azure/o3":[0.000002,0.000008,null,5e-7],"azure/o3-2025-04-16":[0.000002,0.000008,null,5e-7],"azure/o3-deep-research":[0.00001,0.00004,null,0.0000025],"azure/o3-mini":[0.0000011,0.0000044,null,5.5e-7],"azure/o3-mini-2025-01-31":[0.0000011,0.0000044,null,5.5e-7],"azure/o3-pro":[0.00002,0.00008,null,null],"azure/o3-pro-2025-06-10":[0.00002,0.00008,null,null],"azure/o4-mini":[0.0000011,0.0000044,null,2.75e-7],"azure/o4-mini-2025-04-16":[0.0000011,0.0000044,null,2.75e-7],"azure/text-embedding-3-large":[1.3e-7,0,null,null],"azure/text-embedding-3-small":[2e-8,0,null,null],"azure/text-embedding-ada-002":[1e-7,0,null,null],"azure/us/gpt-4.1-2025-04-14":[0.0000022,0.0000088,null,5.5e-7],"us/gpt-4.1-2025-04-14":[0.0000022,0.0000088,null,5.5e-7],"azure/us/gpt-4.1-mini-2025-04-14":[4.4e-7,0.00000176,null,1.1e-7],"us/gpt-4.1-mini-2025-04-14":[4.4e-7,0.00000176,null,1.1e-7],"azure/us/gpt-4.1-nano-2025-04-14":[1.1e-7,4.4e-7,null,2.5e-8],"us/gpt-4.1-nano-2025-04-14":[1.1e-7,4.4e-7,null,2.5e-8],"azure/us/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"us/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"azure/us/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"us/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"azure/us/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"us/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"azure/us/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"us/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"azure/us/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"us/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"azure/us/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"us/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"azure/us/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"us/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"azure/us/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"us/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"azure/us/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"us/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"azure/us/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"us/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"azure/us/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"us/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"azure/us/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"us/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"azure/us/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"us/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"azure/us/o3-2025-04-16":[0.0000022,0.0000088,null,5.5e-7],"us/o3-2025-04-16":[0.0000022,0.0000088,null,5.5e-7],"azure/us/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"us/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"azure/us/o4-mini-2025-04-16":[0.00000121,0.00000484,null,3.1e-7],"us/o4-mini-2025-04-16":[0.00000121,0.00000484,null,3.1e-7],"azure_ai/Cohere-embed-v3-english":[1e-7,0,null,null],"Cohere-embed-v3-english":[1e-7,0,null,null],"azure_ai/Cohere-embed-v3-multilingual":[1e-7,0,null,null],"Cohere-embed-v3-multilingual":[1e-7,0,null,null],"azure_ai/Llama-3.2-11B-Vision-Instruct":[3.7e-7,3.7e-7,null,null],"Llama-3.2-11B-Vision-Instruct":[3.7e-7,3.7e-7,null,null],"azure_ai/Llama-3.2-90B-Vision-Instruct":[0.00000204,0.00000204,null,null],"Llama-3.2-90B-Vision-Instruct":[0.00000204,0.00000204,null,null],"azure_ai/Llama-3.3-70B-Instruct":[7.1e-7,7.1e-7,null,null],"Llama-3.3-70B-Instruct":[7.1e-7,7.1e-7,null,null],"azure_ai/Llama-4-Maverick-17B-128E-Instruct-FP8":[0.00000141,3.5e-7,null,null],"Llama-4-Maverick-17B-128E-Instruct-FP8":[0.00000141,3.5e-7,null,null],"azure_ai/Llama-4-Scout-17B-16E-Instruct":[2e-7,7.8e-7,null,null],"Llama-4-Scout-17B-16E-Instruct":[2e-7,7.8e-7,null,null],"azure_ai/Meta-Llama-3-70B-Instruct":[0.0000011,3.7e-7,null,null],"Meta-Llama-3-70B-Instruct":[0.0000011,3.7e-7,null,null],"azure_ai/Meta-Llama-3.1-405B-Instruct":[0.00000533,0.000016,null,null],"Meta-Llama-3.1-405B-Instruct":[0.00000533,0.000016,null,null],"azure_ai/Meta-Llama-3.1-70B-Instruct":[0.00000268,0.00000354,null,null],"Meta-Llama-3.1-70B-Instruct":[0.00000268,0.00000354,null,null],"azure_ai/Meta-Llama-3.1-8B-Instruct":[3e-7,6.1e-7,null,null],"Meta-Llama-3.1-8B-Instruct":[3e-7,6.1e-7,null,null],"azure_ai/Phi-3-medium-128k-instruct":[1.7e-7,6.8e-7,null,null],"Phi-3-medium-128k-instruct":[1.7e-7,6.8e-7,null,null],"azure_ai/Phi-3-medium-4k-instruct":[1.7e-7,6.8e-7,null,null],"Phi-3-medium-4k-instruct":[1.7e-7,6.8e-7,null,null],"azure_ai/Phi-3-mini-128k-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3-mini-128k-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3-mini-4k-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3-mini-4k-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3-small-128k-instruct":[1.5e-7,6e-7,null,null],"Phi-3-small-128k-instruct":[1.5e-7,6e-7,null,null],"azure_ai/Phi-3-small-8k-instruct":[1.5e-7,6e-7,null,null],"Phi-3-small-8k-instruct":[1.5e-7,6e-7,null,null],"azure_ai/Phi-3.5-MoE-instruct":[1.6e-7,6.4e-7,null,null],"Phi-3.5-MoE-instruct":[1.6e-7,6.4e-7,null,null],"azure_ai/Phi-3.5-mini-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3.5-mini-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3.5-vision-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3.5-vision-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-4":[1.25e-7,5e-7,null,null],"Phi-4":[1.25e-7,5e-7,null,null],"azure_ai/Phi-4-mini-instruct":[7.5e-8,3e-7,null,null],"Phi-4-mini-instruct":[7.5e-8,3e-7,null,null],"azure_ai/Phi-4-multimodal-instruct":[8e-8,3.2e-7,null,null],"Phi-4-multimodal-instruct":[8e-8,3.2e-7,null,null],"azure_ai/Phi-4-mini-reasoning":[8e-8,3.2e-7,null,null],"Phi-4-mini-reasoning":[8e-8,3.2e-7,null,null],"azure_ai/Phi-4-reasoning":[1.25e-7,5e-7,null,null],"Phi-4-reasoning":[1.25e-7,5e-7,null,null],"azure_ai/MAI-DS-R1":[0.00000135,0.0000054,null,null],"MAI-DS-R1":[0.00000135,0.0000054,null,null],"azure_ai/cohere-rerank-v3-english":[0,0,null,null],"cohere-rerank-v3-english":[0,0,null,null],"azure_ai/cohere-rerank-v3-multilingual":[0,0,null,null],"cohere-rerank-v3-multilingual":[0,0,null,null],"azure_ai/cohere-rerank-v3.5":[0,0,null,null],"cohere-rerank-v3.5":[0,0,null,null],"azure_ai/cohere-rerank-v4.0-pro":[0,0,null,null],"cohere-rerank-v4.0-pro":[0,0,null,null],"azure_ai/cohere-rerank-v4.0-fast":[0,0,null,null],"cohere-rerank-v4.0-fast":[0,0,null,null],"azure_ai/deepseek-v3.2":[5.8e-7,0.00000168,null,null],"deepseek-v3.2":[5.8e-7,0.00000168,null,null],"azure_ai/deepseek-v3.2-speciale":[5.8e-7,0.00000168,null,null],"deepseek-v3.2-speciale":[5.8e-7,0.00000168,null,null],"azure_ai/deepseek-r1":[0.00000135,0.0000054,null,null],"deepseek-r1":[0.00000135,0.0000054,null,null],"azure_ai/deepseek-v3":[0.00000114,0.00000456,null,null],"deepseek-v3":[0.00000114,0.00000456,null,null],"azure_ai/deepseek-v3-0324":[0.00000114,0.00000456,null,null],"deepseek-v3-0324":[0.00000114,0.00000456,null,null],"azure_ai/embed-v-4-0":[1.2e-7,0,null,null],"embed-v-4-0":[1.2e-7,0,null,null],"azure_ai/global/grok-3":[0.000003,0.000015,null,null],"global/grok-3":[0.000003,0.000015,null,null],"azure_ai/global/grok-3-mini":[2.5e-7,0.00000127,null,null],"global/grok-3-mini":[2.5e-7,0.00000127,null,null],"azure_ai/grok-3":[0.000003,0.000015,null,null],"grok-3":[0.000003,0.000015,null,null],"azure_ai/grok-3-mini":[2.5e-7,0.00000127,null,null],"grok-3-mini":[2.5e-7,0.00000127,null,null],"azure_ai/grok-4":[0.000003,0.000015,null,null],"grok-4":[0.000003,0.000015,null,null],"azure_ai/grok-4-fast-non-reasoning":[2e-7,5e-7,null,null],"grok-4-fast-non-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-fast-reasoning":[2e-7,5e-7,null,null],"grok-4-fast-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,null],"grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-1-fast-reasoning":[2e-7,5e-7,null,null],"grok-4-1-fast-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-code-fast-1":[2e-7,0.0000015,null,null],"grok-code-fast-1":[2e-7,0.0000015,null,null],"azure_ai/jais-30b-chat":[0.0032,0.00971,null,null],"jais-30b-chat":[0.0032,0.00971,null,null],"azure_ai/jamba-instruct":[5e-7,7e-7,null,null],"jamba-instruct":[5e-7,7e-7,null,null],"azure_ai/kimi-k2.5":[6e-7,0.000003,null,null],"kimi-k2.5":[6e-7,0.000003,null,null],"azure_ai/ministral-3b":[4e-8,4e-8,null,null],"ministral-3b":[4e-8,4e-8,null,null],"azure_ai/mistral-large":[0.000004,0.000012,null,null],"mistral-large":[0.000004,0.000012,null,null],"azure_ai/mistral-large-2407":[0.000002,0.000006,null,null],"mistral-large-2407":[0.000002,0.000006,null,null],"azure_ai/mistral-large-latest":[0.000002,0.000006,null,null],"azure_ai/mistral-large-3":[5e-7,0.0000015,null,null],"mistral-large-3":[5e-7,0.0000015,null,null],"azure_ai/mistral-medium-2505":[4e-7,0.000002,null,null],"mistral-medium-2505":[4e-7,0.000002,null,null],"azure_ai/mistral-nemo":[1.5e-7,1.5e-7,null,null],"mistral-nemo":[1.5e-7,1.5e-7,null,null],"azure_ai/mistral-small":[0.000001,0.000003,null,null],"mistral-small":[0.000001,0.000003,null,null],"azure_ai/mistral-small-2503":[1e-7,3e-7,null,null],"mistral-small-2503":[1e-7,3e-7,null,null],"bedrock/ap-northeast-1/anthropic.claude-instant-v1":[0.00000223,0.00000755,null,null],"ap-northeast-1/anthropic.claude-instant-v1":[0.00000223,0.00000755,null,null],"bedrock/ap-northeast-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"ap-northeast-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/ap-northeast-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"ap-northeast-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/ap-northeast-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-northeast-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-northeast-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-northeast-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-northeast-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-northeast-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-northeast-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"ap-northeast-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/ap-northeast-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-northeast-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-northeast-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-northeast-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/moonshotai.kimi-k2.5":[6e-7,0.00000303,null,null],"bedrock/ap-south-1/meta.llama3-70b-instruct-v1:0":[0.00000318,0.0000042,null,null],"ap-south-1/meta.llama3-70b-instruct-v1:0":[0.00000318,0.0000042,null,null],"bedrock/ap-south-1/meta.llama3-8b-instruct-v1:0":[3.6e-7,7.2e-7,null,null],"ap-south-1/meta.llama3-8b-instruct-v1:0":[3.6e-7,7.2e-7,null,null],"bedrock/ap-south-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-south-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-south-1/moonshotai.kimi-k2-thinking":[7.1e-7,0.00000294,null,null],"ap-south-1/moonshotai.kimi-k2-thinking":[7.1e-7,0.00000294,null,null],"bedrock/ap-south-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-south-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/ap-southeast-2/minimax.minimax-m2.5":[3.09e-7,0.000001236,null,null],"ap-southeast-2/minimax.minimax-m2.5":[3.09e-7,0.000001236,null,null],"bedrock/ap-southeast-3/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-southeast-3/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-southeast-3/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-southeast-3/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-southeast-3/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-southeast-3/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-southeast-3/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-southeast-3/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-southeast-3/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-southeast-3/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/ca-central-1/meta.llama3-70b-instruct-v1:0":[0.00000305,0.00000403,null,null],"ca-central-1/meta.llama3-70b-instruct-v1:0":[0.00000305,0.00000403,null,null],"bedrock/ca-central-1/meta.llama3-8b-instruct-v1:0":[3.5e-7,6.9e-7,null,null],"ca-central-1/meta.llama3-8b-instruct-v1:0":[3.5e-7,6.9e-7,null,null],"bedrock/eu-north-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"eu-north-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/eu-north-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-north-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-north-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-north-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-north-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"eu-north-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/eu-central-1/anthropic.claude-instant-v1":[0.00000248,0.00000838,null,null],"eu-central-1/anthropic.claude-instant-v1":[0.00000248,0.00000838,null,null],"bedrock/eu-central-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"eu-central-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/eu-central-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"eu-central-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/eu-central-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-central-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-central-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-central-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-central-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-central-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/eu-west-1/meta.llama3-70b-instruct-v1:0":[0.00000286,0.00000378,null,null],"eu-west-1/meta.llama3-70b-instruct-v1:0":[0.00000286,0.00000378,null,null],"bedrock/eu-west-1/meta.llama3-8b-instruct-v1:0":[3.2e-7,6.5e-7,null,null],"eu-west-1/meta.llama3-8b-instruct-v1:0":[3.2e-7,6.5e-7,null,null],"bedrock/eu-west-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-west-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-west-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-west-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-west-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-west-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/eu-west-2/meta.llama3-70b-instruct-v1:0":[0.00000345,0.00000455,null,null],"eu-west-2/meta.llama3-70b-instruct-v1:0":[0.00000345,0.00000455,null,null],"bedrock/eu-west-2/meta.llama3-8b-instruct-v1:0":[3.9e-7,7.8e-7,null,null],"eu-west-2/meta.llama3-8b-instruct-v1:0":[3.9e-7,7.8e-7,null,null],"bedrock/eu-west-2/minimax.minimax-m2.1":[4.7e-7,0.00000186,null,null],"eu-west-2/minimax.minimax-m2.1":[4.7e-7,0.00000186,null,null],"bedrock/eu-west-2/minimax.minimax-m2.5":[4.7e-7,0.00000186,null,null],"eu-west-2/minimax.minimax-m2.5":[4.7e-7,0.00000186,null,null],"bedrock/eu-west-2/qwen.qwen3-coder-next":[7.8e-7,0.00000186,null,null],"eu-west-2/qwen.qwen3-coder-next":[7.8e-7,0.00000186,null,null],"bedrock/eu-west-3/mistral.mistral-7b-instruct-v0:2":[2e-7,2.6e-7,null,null],"eu-west-3/mistral.mistral-7b-instruct-v0:2":[2e-7,2.6e-7,null,null],"bedrock/eu-west-3/mistral.mistral-large-2402-v1:0":[0.0000104,0.0000312,null,null],"eu-west-3/mistral.mistral-large-2402-v1:0":[0.0000104,0.0000312,null,null],"bedrock/eu-west-3/mistral.mixtral-8x7b-instruct-v0:1":[5.9e-7,9.1e-7,null,null],"eu-west-3/mistral.mixtral-8x7b-instruct-v0:1":[5.9e-7,9.1e-7,null,null],"bedrock/eu-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/invoke/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"invoke/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"bedrock/sa-east-1/meta.llama3-70b-instruct-v1:0":[0.00000445,0.00000588,null,null],"sa-east-1/meta.llama3-70b-instruct-v1:0":[0.00000445,0.00000588,null,null],"bedrock/sa-east-1/meta.llama3-8b-instruct-v1:0":[5e-7,0.00000101,null,null],"sa-east-1/meta.llama3-8b-instruct-v1:0":[5e-7,0.00000101,null,null],"bedrock/sa-east-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"sa-east-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/sa-east-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"sa-east-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/sa-east-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"sa-east-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/sa-east-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"sa-east-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/sa-east-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"sa-east-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/sa-east-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"sa-east-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/us-east-1/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"us-east-1/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"bedrock/us-east-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"us-east-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/us-east-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"us-east-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/us-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"us-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"bedrock/us-east-1/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"us-east-1/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"bedrock/us-east-1/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"us-east-1/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"bedrock/us-east-1/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"us-east-1/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"bedrock/us-east-1/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-east-1/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-east-1/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-east-1/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-east-1/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-east-1/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-east-1/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-east-1/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-east-1/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-east-1/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-east-1/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-east-1/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us-east-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-east-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-east-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-east-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-east-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-east-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-east-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-east-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-east-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-east-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-east-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-east-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us-gov-east-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"us-gov-east-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"bedrock/us-gov-east-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"us-gov-east-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"bedrock/us-gov-east-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"us-gov-east-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"bedrock/us-gov-east-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"us-gov-east-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"bedrock/us-gov-east-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"us-gov-east-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"bedrock/us-gov-east-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"us-gov-east-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"bedrock/us-gov-east-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-east-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-east-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"us-gov-east-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"bedrock/us-gov-east-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-east-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-east-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-east-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-gov-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-gov-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"us-gov-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"bedrock/us-gov-west-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"us-gov-west-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"bedrock/us-gov-west-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"us-gov-west-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"bedrock/us-gov-west-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"us-gov-west-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"bedrock/us-gov-west-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"us-gov-west-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"bedrock/us-gov-west-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"us-gov-west-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"bedrock/us-gov-west-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"us-gov-west-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"bedrock/us-gov-west-1/anthropic.claude-3-7-sonnet-20250219-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-west-1/anthropic.claude-3-7-sonnet-20250219-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-west-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-west-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-west-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"us-gov-west-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"bedrock/us-gov-west-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-west-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-west-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-west-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-gov-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-gov-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"us-gov-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"bedrock/us-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"us-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"bedrock/us-west-2/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"us-west-2/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"bedrock/us-west-2/anthropic.claude-v1":[0.000008,0.000024,null,null],"us-west-2/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/us-west-2/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"us-west-2/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/us-west-2/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"us-west-2/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"bedrock/us-west-2/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"us-west-2/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"bedrock/us-west-2/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"us-west-2/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"bedrock/us-west-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-west-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-west-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-west-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-west-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-west-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-west-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-west-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-west-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-west-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-west-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-west-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"cerebras/llama-3.3-70b":[8.5e-7,0.0000012,null,null],"llama-3.3-70b":[8.5e-7,0.0000012,null,null],"cerebras/llama3.1-70b":[6e-7,6e-7,null,null],"llama3.1-70b":[6e-7,6e-7,null,null],"cerebras/llama3.1-8b":[1e-7,1e-7,null,null],"llama3.1-8b":[1e-7,1e-7,null,null],"cerebras/gpt-oss-120b":[3.5e-7,7.5e-7,null,null],"cerebras/qwen-3-32b":[4e-7,8e-7,null,null],"qwen-3-32b":[4e-7,8e-7,null,null],"cerebras/zai-glm-4.6":[0.00000225,0.00000275,null,null],"zai-glm-4.6":[0.00000225,0.00000275,null,null],"cerebras/zai-glm-4.7":[0.00000225,0.00000275,null,null],"zai-glm-4.7":[0.00000225,0.00000275,null,null],"cloudflare/@cf/meta/llama-2-7b-chat-fp16":[0.000001923,0.000001923,null,null],"@cf/meta/llama-2-7b-chat-fp16":[0.000001923,0.000001923,null,null],"cloudflare/@cf/meta/llama-2-7b-chat-int8":[0.000001923,0.000001923,null,null],"@cf/meta/llama-2-7b-chat-int8":[0.000001923,0.000001923,null,null],"cloudflare/@cf/mistral/mistral-7b-instruct-v0.1":[0.000001923,0.000001923,null,null],"@cf/mistral/mistral-7b-instruct-v0.1":[0.000001923,0.000001923,null,null],"cloudflare/@hf/thebloke/codellama-7b-instruct-awq":[0.000001923,0.000001923,null,null],"@hf/thebloke/codellama-7b-instruct-awq":[0.000001923,0.000001923,null,null],"codestral/codestral-2405":[0,0,null,null],"codestral-2405":[0,0,null,null],"codestral/codestral-latest":[0,0,null,null],"codestral-latest":[0,0,null,null],"cohere/embed-v4.0":[1.2e-7,0,null,null],"embed-v4.0":[1.2e-7,0,null,null],"dashscope/qwen-coder":[3e-7,0.0000015,null,null],"qwen-coder":[3e-7,0.0000015,null,null],"dashscope/qwen-max":[0.0000016,0.0000064,null,null],"qwen-max":[0.0000016,0.0000064,null,null],"dashscope/qwen-plus":[4e-7,0.0000012,null,null],"qwen-plus":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-01-25":[4e-7,0.0000012,null,null],"qwen-plus-2025-01-25":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-04-28":[4e-7,0.0000012,null,null],"qwen-plus-2025-04-28":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-07-14":[4e-7,0.0000012,null,null],"qwen-plus-2025-07-14":[4e-7,0.0000012,null,null],"dashscope/qwen-turbo":[5e-8,2e-7,null,null],"qwen-turbo":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-2024-11-01":[5e-8,2e-7,null,null],"qwen-turbo-2024-11-01":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-2025-04-28":[5e-8,2e-7,null,null],"qwen-turbo-2025-04-28":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-latest":[5e-8,2e-7,null,null],"qwen-turbo-latest":[5e-8,2e-7,null,null],"dashscope/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000012,null,null],"qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000012,null,null],"dashscope/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000012,null,null],"qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000012,null,null],"dashscope/qwen3-vl-235b-a22b-instruct":[4e-7,0.0000016,null,null],"qwen3-vl-235b-a22b-instruct":[4e-7,0.0000016,null,null],"dashscope/qwen3-vl-235b-a22b-thinking":[4e-7,0.000004,null,null],"qwen3-vl-235b-a22b-thinking":[4e-7,0.000004,null,null],"dashscope/qwen3-vl-32b-instruct":[1.6e-7,6.4e-7,null,null],"qwen3-vl-32b-instruct":[1.6e-7,6.4e-7,null,null],"dashscope/qwen3-vl-32b-thinking":[1.6e-7,0.00000287,null,null],"qwen3-vl-32b-thinking":[1.6e-7,0.00000287,null,null],"dashscope/qwq-plus":[8e-7,0.0000024,null,null],"qwq-plus":[8e-7,0.0000024,null,null],"databricks/databricks-bge-large-en":[1.0003e-7,0,null,null],"databricks-bge-large-en":[1.0003e-7,0,null,null],"databricks/databricks-claude-3-7-sonnet":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-3-7-sonnet":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-haiku-4-5":[0.00000100002,0.00000500003,null,null],"databricks-claude-haiku-4-5":[0.00000100002,0.00000500003,null,null],"databricks/databricks-claude-opus-4":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks-claude-opus-4":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks/databricks-claude-opus-4-1":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks-claude-opus-4-1":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks/databricks-claude-opus-4-5":[0.00000500003,0.000025000010000000002,null,null],"databricks-claude-opus-4-5":[0.00000500003,0.000025000010000000002,null,null],"databricks/databricks-claude-sonnet-4":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-sonnet-4-1":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4-1":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-sonnet-4-5":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4-5":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-gemini-2-5-flash":[3.0001999999999996e-7,0.00000249998,null,null],"databricks-gemini-2-5-flash":[3.0001999999999996e-7,0.00000249998,null,null],"databricks/databricks-gemini-2-5-pro":[0.00000124999,0.000009999990000000002,null,null],"databricks-gemini-2-5-pro":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gemma-3-12b":[1.5000999999999998e-7,5.0001e-7,null,null],"databricks-gemma-3-12b":[1.5000999999999998e-7,5.0001e-7,null,null],"databricks/databricks-gpt-5":[0.00000124999,0.000009999990000000002,null,null],"databricks-gpt-5":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gpt-5-1":[0.00000124999,0.000009999990000000002,null,null],"databricks-gpt-5-1":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gpt-5-mini":[2.4997000000000006e-7,0.0000019999700000000004,null,null],"databricks-gpt-5-mini":[2.4997000000000006e-7,0.0000019999700000000004,null,null],"databricks/databricks-gpt-5-nano":[4.998e-8,3.9998000000000007e-7,null,null],"databricks-gpt-5-nano":[4.998e-8,3.9998000000000007e-7,null,null],"databricks/databricks-gpt-oss-120b":[1.5000999999999998e-7,5.9997e-7,null,null],"databricks-gpt-oss-120b":[1.5000999999999998e-7,5.9997e-7,null,null],"databricks/databricks-gpt-oss-20b":[7e-8,3.0001999999999996e-7,null,null],"databricks-gpt-oss-20b":[7e-8,3.0001999999999996e-7,null,null],"databricks/databricks-gte-large-en":[1.2999000000000001e-7,0,null,null],"databricks-gte-large-en":[1.2999000000000001e-7,0,null,null],"databricks/databricks-llama-2-70b-chat":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-llama-2-70b-chat":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-llama-4-maverick":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-llama-4-maverick":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-meta-llama-3-1-405b-instruct":[0.00000500003,0.000015000020000000002,null,null],"databricks-meta-llama-3-1-405b-instruct":[0.00000500003,0.000015000020000000002,null,null],"databricks/databricks-meta-llama-3-1-8b-instruct":[1.5000999999999998e-7,4.5003000000000007e-7,null,null],"databricks-meta-llama-3-1-8b-instruct":[1.5000999999999998e-7,4.5003000000000007e-7,null,null],"databricks/databricks-meta-llama-3-3-70b-instruct":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-meta-llama-3-3-70b-instruct":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-meta-llama-3-70b-instruct":[0.00000100002,0.0000029999900000000002,null,null],"databricks-meta-llama-3-70b-instruct":[0.00000100002,0.0000029999900000000002,null,null],"databricks/databricks-mixtral-8x7b-instruct":[5.0001e-7,0.00000100002,null,null],"databricks-mixtral-8x7b-instruct":[5.0001e-7,0.00000100002,null,null],"databricks/databricks-mpt-30b-instruct":[0.00000100002,0.00000100002,null,null],"databricks-mpt-30b-instruct":[0.00000100002,0.00000100002,null,null],"databricks/databricks-mpt-7b-instruct":[5.0001e-7,0,null,null],"databricks-mpt-7b-instruct":[5.0001e-7,0,null,null],"deepinfra/Gryphe/MythoMax-L2-13b":[8e-8,9e-8,null,null],"Gryphe/MythoMax-L2-13b":[8e-8,9e-8,null,null],"deepinfra/NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000001,null,null],"NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000001,null,null],"deepinfra/NousResearch/Hermes-3-Llama-3.1-70B":[3e-7,3e-7,null,null],"NousResearch/Hermes-3-Llama-3.1-70B":[3e-7,3e-7,null,null],"deepinfra/Qwen/QwQ-32B":[1.5e-7,4e-7,null,null],"Qwen/QwQ-32B":[1.5e-7,4e-7,null,null],"deepinfra/Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3.9e-7,null,null],"Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3.9e-7,null,null],"deepinfra/Qwen/Qwen2.5-7B-Instruct":[4e-8,1e-7,null,null],"Qwen/Qwen2.5-7B-Instruct":[4e-8,1e-7,null,null],"deepinfra/Qwen/Qwen2.5-VL-32B-Instruct":[2e-7,6e-7,null,null],"Qwen/Qwen2.5-VL-32B-Instruct":[2e-7,6e-7,null,null],"deepinfra/Qwen/Qwen3-14B":[6e-8,2.4e-7,null,null],"Qwen/Qwen3-14B":[6e-8,2.4e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B":[1.8e-7,5.4e-7,null,null],"Qwen/Qwen3-235B-A22B":[1.8e-7,5.4e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B-Instruct-2507":[9e-8,6e-7,null,null],"Qwen/Qwen3-235B-A22B-Instruct-2507":[9e-8,6e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B-Thinking-2507":[3e-7,0.0000029,null,null],"Qwen/Qwen3-235B-A22B-Thinking-2507":[3e-7,0.0000029,null,null],"deepinfra/Qwen/Qwen3-30B-A3B":[8e-8,2.9e-7,null,null],"Qwen/Qwen3-30B-A3B":[8e-8,2.9e-7,null,null],"deepinfra/Qwen/Qwen3-32B":[1e-7,2.8e-7,null,null],"Qwen/Qwen3-32B":[1e-7,2.8e-7,null,null],"deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct":[4e-7,0.0000016,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct":[4e-7,0.0000016,null,null],"deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo":[2.9e-7,0.0000012,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo":[2.9e-7,0.0000012,null,null],"deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct":[1.4e-7,0.0000014,null,null],"Qwen/Qwen3-Next-80B-A3B-Instruct":[1.4e-7,0.0000014,null,null],"deepinfra/Qwen/Qwen3-Next-80B-A3B-Thinking":[1.4e-7,0.0000014,null,null],"Qwen/Qwen3-Next-80B-A3B-Thinking":[1.4e-7,0.0000014,null,null],"deepinfra/Sao10K/L3-8B-Lunaris-v1-Turbo":[4e-8,5e-8,null,null],"Sao10K/L3-8B-Lunaris-v1-Turbo":[4e-8,5e-8,null,null],"deepinfra/Sao10K/L3.1-70B-Euryale-v2.2":[6.5e-7,7.5e-7,null,null],"Sao10K/L3.1-70B-Euryale-v2.2":[6.5e-7,7.5e-7,null,null],"deepinfra/Sao10K/L3.3-70B-Euryale-v2.3":[6.5e-7,7.5e-7,null,null],"Sao10K/L3.3-70B-Euryale-v2.3":[6.5e-7,7.5e-7,null,null],"deepinfra/allenai/olmOCR-7B-0725-FP8":[2.7e-7,0.0000015,null,null],"allenai/olmOCR-7B-0725-FP8":[2.7e-7,0.0000015,null,null],"deepinfra/anthropic/claude-3-7-sonnet-latest":[0.0000033,0.0000165,null,3.3e-7],"anthropic/claude-3-7-sonnet-latest":[0.0000033,0.0000165,null,3.3e-7],"deepinfra/anthropic/claude-4-opus":[0.0000165,0.0000825,null,null],"anthropic/claude-4-opus":[0.0000165,0.0000825,null,null],"deepinfra/anthropic/claude-4-sonnet":[0.0000033,0.0000165,null,null],"anthropic/claude-4-sonnet":[0.0000033,0.0000165,null,null],"deepinfra/deepseek-ai/DeepSeek-R1":[7e-7,0.0000024,null,null],"deepseek-ai/DeepSeek-R1":[7e-7,0.0000024,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-0528":[5e-7,0.00000215,null,4e-7],"deepseek-ai/DeepSeek-R1-0528":[5e-7,0.00000215,null,4e-7],"deepinfra/deepseek-ai/DeepSeek-R1-0528-Turbo":[0.000001,0.000003,null,null],"deepseek-ai/DeepSeek-R1-0528-Turbo":[0.000001,0.000003,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2e-7,6e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2e-7,6e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[2.7e-7,2.7e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[2.7e-7,2.7e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Turbo":[0.000001,0.000003,null,null],"deepseek-ai/DeepSeek-R1-Turbo":[0.000001,0.000003,null,null],"deepinfra/deepseek-ai/DeepSeek-V3":[3.8e-7,8.9e-7,null,null],"deepseek-ai/DeepSeek-V3":[3.8e-7,8.9e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-V3-0324":[2.5e-7,8.8e-7,null,null],"deepseek-ai/DeepSeek-V3-0324":[2.5e-7,8.8e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-V3.1":[2.7e-7,0.000001,null,2.16e-7],"deepseek-ai/DeepSeek-V3.1":[2.7e-7,0.000001,null,2.16e-7],"deepinfra/deepseek-ai/DeepSeek-V3.1-Terminus":[2.7e-7,0.000001,null,2.16e-7],"deepseek-ai/DeepSeek-V3.1-Terminus":[2.7e-7,0.000001,null,2.16e-7],"deepinfra/google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"deepinfra/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"deepinfra/google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"deepinfra/google/gemma-3-12b-it":[5e-8,1e-7,null,null],"google/gemma-3-12b-it":[5e-8,1e-7,null,null],"deepinfra/google/gemma-3-27b-it":[9e-8,1.6e-7,null,null],"google/gemma-3-27b-it":[9e-8,1.6e-7,null,null],"deepinfra/google/gemma-3-4b-it":[4e-8,8e-8,null,null],"google/gemma-3-4b-it":[4e-8,8e-8,null,null],"deepinfra/meta-llama/Llama-3.2-11B-Vision-Instruct":[4.9e-8,4.9e-8,null,null],"meta-llama/Llama-3.2-11B-Vision-Instruct":[4.9e-8,4.9e-8,null,null],"deepinfra/meta-llama/Llama-3.2-3B-Instruct":[2e-8,2e-8,null,null],"meta-llama/Llama-3.2-3B-Instruct":[2e-8,2e-8,null,null],"deepinfra/meta-llama/Llama-3.3-70B-Instruct":[2.3e-7,4e-7,null,null],"meta-llama/Llama-3.3-70B-Instruct":[2.3e-7,4e-7,null,null],"deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo":[1.3e-7,3.9e-7,null,null],"meta-llama/Llama-3.3-70B-Instruct-Turbo":[1.3e-7,3.9e-7,null,null],"deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[1.5e-7,6e-7,null,null],"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[1.5e-7,6e-7,null,null],"deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct":[8e-8,3e-7,null,null],"meta-llama/Llama-4-Scout-17B-16E-Instruct":[8e-8,3e-7,null,null],"deepinfra/meta-llama/Llama-Guard-3-8B":[5.5e-8,5.5e-8,null,null],"meta-llama/Llama-Guard-3-8B":[5.5e-8,5.5e-8,null,null],"deepinfra/meta-llama/Llama-Guard-4-12B":[1.8e-7,1.8e-7,null,null],"meta-llama/Llama-Guard-4-12B":[1.8e-7,1.8e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3-8B-Instruct":[3e-8,6e-8,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct":[4e-7,4e-7,null,null],"meta-llama/Meta-Llama-3.1-70B-Instruct":[4e-7,4e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[1e-7,2.8e-7,null,null],"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[1e-7,2.8e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct":[3e-8,5e-8,null,null],"meta-llama/Meta-Llama-3.1-8B-Instruct":[3e-8,5e-8,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[2e-8,3e-8,null,null],"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[2e-8,3e-8,null,null],"deepinfra/microsoft/WizardLM-2-8x22B":[4.8e-7,4.8e-7,null,null],"microsoft/WizardLM-2-8x22B":[4.8e-7,4.8e-7,null,null],"deepinfra/microsoft/phi-4":[7e-8,1.4e-7,null,null],"microsoft/phi-4":[7e-8,1.4e-7,null,null],"deepinfra/mistralai/Mistral-Nemo-Instruct-2407":[2e-8,4e-8,null,null],"mistralai/Mistral-Nemo-Instruct-2407":[2e-8,4e-8,null,null],"deepinfra/mistralai/Mistral-Small-24B-Instruct-2501":[5e-8,8e-8,null,null],"mistralai/Mistral-Small-24B-Instruct-2501":[5e-8,8e-8,null,null],"deepinfra/mistralai/Mistral-Small-3.2-24B-Instruct-2506":[7.5e-8,2e-7,null,null],"mistralai/Mistral-Small-3.2-24B-Instruct-2506":[7.5e-8,2e-7,null,null],"deepinfra/mistralai/Mixtral-8x7B-Instruct-v0.1":[4e-7,4e-7,null,null],"deepinfra/moonshotai/Kimi-K2-Instruct":[5e-7,0.000002,null,null],"moonshotai/Kimi-K2-Instruct":[5e-7,0.000002,null,null],"deepinfra/moonshotai/Kimi-K2-Instruct-0905":[5e-7,0.000002,null,4e-7],"moonshotai/Kimi-K2-Instruct-0905":[5e-7,0.000002,null,4e-7],"deepinfra/nvidia/Llama-3.1-Nemotron-70B-Instruct":[6e-7,6e-7,null,null],"nvidia/Llama-3.1-Nemotron-70B-Instruct":[6e-7,6e-7,null,null],"deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5":[1e-7,4e-7,null,null],"nvidia/Llama-3.3-Nemotron-Super-49B-v1.5":[1e-7,4e-7,null,null],"deepinfra/nvidia/NVIDIA-Nemotron-Nano-9B-v2":[4e-8,1.6e-7,null,null],"nvidia/NVIDIA-Nemotron-Nano-9B-v2":[4e-8,1.6e-7,null,null],"deepinfra/openai/gpt-oss-120b":[5e-8,4.5e-7,null,null],"openai/gpt-oss-120b":[5e-8,4.5e-7,null,null],"deepinfra/openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"deepinfra/zai-org/GLM-4.5":[4e-7,0.0000016,null,null],"zai-org/GLM-4.5":[4e-7,0.0000016,null,null],"deepseek/deepseek-chat":[2.8e-7,4.2e-7,0,2.8e-8],"deepseek/deepseek-coder":[1.4e-7,2.8e-7,null,null],"deepseek-coder":[1.4e-7,2.8e-7,null,null],"deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"deepseek/deepseek-reasoner":[2.8e-7,4.2e-7,null,2.8e-8],"deepseek/deepseek-v3":[2.7e-7,0.0000011,0,7e-8],"deepseek/deepseek-v3.2":[2.8e-7,4e-7,null,null],"fireworks_ai/WhereIsAI/UAE-Large-V1":[1.6e-8,0,null,null],"WhereIsAI/UAE-Large-V1":[1.6e-8,0,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-coder-v2-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1":[0.000003,0.000008,null,null],"accounts/fireworks/models/deepseek-r1":[0.000003,0.000008,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-0528":[0.000003,0.000008,null,null],"accounts/fireworks/models/deepseek-r1-0528":[0.000003,0.000008,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-basic":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/deepseek-r1-basic":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-v3":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3-0324":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-v3-0324":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p1":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p1":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p1-terminus":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p1-terminus":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p2":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p2":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/firefunction-v2":[9e-7,9e-7,null,null],"accounts/fireworks/models/firefunction-v2":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/glm-4p5":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5-air":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/glm-4p5-air":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p6":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/glm-4p6":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p7":[6e-7,0.0000022,null,3e-7],"accounts/fireworks/models/glm-4p7":[6e-7,0.0000022,null,3e-7],"fireworks_ai/accounts/fireworks/models/gpt-oss-120b":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/gpt-oss-120b":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-20b":[5e-8,2e-7,null,null],"accounts/fireworks/models/gpt-oss-20b":[5e-8,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-instruct":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-instruct":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-instruct-0905":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-instruct-0905":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-thinking":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-thinking":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2p5":[6e-7,0.000003,null,1e-7],"accounts/fireworks/models/kimi-k2p5":[6e-7,0.000003,null,1e-7],"fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct":[0.000003,0.000003,null,null],"accounts/fireworks/models/llama-v3p1-405b-instruct":[0.000003,0.000003,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-8b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-8b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-11b-vision-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3p2-11b-vision-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-1b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-1b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-3b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-3b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-90b-vision-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p2-90b-vision-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama4-maverick-instruct-basic":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/llama4-maverick-instruct-basic":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama4-scout-instruct-basic":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/llama4-scout-instruct-basic":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"accounts/fireworks/models/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct-hf":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b-instruct-hf":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-large":[0.000003,0.000003,null,null],"accounts/fireworks/models/yi-large":[0.000003,0.000003,null,null],"fireworks_ai/glm-4p7":[6e-7,0.0000022,null,3e-7],"glm-4p7":[6e-7,0.0000022,null,3e-7],"fireworks_ai/kimi-k2p5":[6e-7,0.000003,null,1e-7],"kimi-k2p5":[6e-7,0.000003,null,1e-7],"fireworks_ai/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"minimax-m2p1":[3e-7,0.0000012,null,3e-8],"fireworks_ai/nomic-ai/nomic-embed-text-v1":[8e-9,0,null,null],"nomic-ai/nomic-embed-text-v1":[8e-9,0,null,null],"fireworks_ai/nomic-ai/nomic-embed-text-v1.5":[8e-9,0,null,null],"nomic-ai/nomic-embed-text-v1.5":[8e-9,0,null,null],"fireworks_ai/thenlper/gte-base":[8e-9,0,null,null],"thenlper/gte-base":[8e-9,0,null,null],"fireworks_ai/thenlper/gte-large":[1.6e-8,0,null,null],"thenlper/gte-large":[1.6e-8,0,null,null],"friendliai/meta-llama-3.1-70b-instruct":[6e-7,6e-7,null,null],"meta-llama-3.1-70b-instruct":[6e-7,6e-7,null,null],"friendliai/meta-llama-3.1-8b-instruct":[1e-7,1e-7,null,null],"meta-llama-3.1-8b-instruct":[1e-7,1e-7,null,null],"gemini/gemini-live-2.5-flash-preview-native-audio-09-2025":[3e-7,0.000002,null,7.5e-8],"vertex_ai/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"vertex_ai/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"vertex_ai/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"vertex_ai/gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini/gemini-robotics-er-1.5-preview":[3e-7,0.0000025,null,0],"vertex_ai/gemini-embedding-2-preview":[1.5e-7,0,null,null],"vertex_ai/gemini-embedding-2":[2e-7,0,null,null],"gemini/gemini-embedding-001":[1.5e-7,0,null,null],"gemini/gemini-embedding-2-preview":[2e-7,0,null,null],"gemini/gemini-embedding-2":[2e-7,0,null,null],"gemini/gemini-1.5-flash":[7.5e-8,0,null,null],"gemini-1.5-flash":[7.5e-8,0,null,null],"gemini/gemini-2.0-flash":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.0-flash-001":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,1.875e-8],"gemini/gemini-2.5-flash":[3e-7,0.0000025,null,3e-8],"gemini/gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"gemini/gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"gemini/gemini-3.1-flash-image-preview":[2.5e-7,0.0000015,null,null],"gemini/deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"gemini/gemini-2.5-flash-lite":[1e-7,4e-7,null,1e-8],"gemini/gemini-2.5-flash-lite-preview-09-2025":[1e-7,4e-7,null,1e-8],"gemini/gemini-2.5-flash-preview-09-2025":[3e-7,0.0000025,null,7.5e-8],"gemini/gemini-flash-latest":[3e-7,0.0000025,null,7.5e-8],"gemini/gemini-flash-lite-latest":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.5-flash-lite-preview-06-17":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.5-flash-preview-tts":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-pro":[0.00000125,0.00001,null,1.25e-7],"gemini/gemini-2.5-computer-use-preview-10-2025":[0.00000125,0.00001,null,null],"gemini/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"gemini/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"gemini/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"gemini/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"gemini/gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini/gemini-2.5-pro-preview-tts":[0.00000125,0.00001,null,1.25e-7],"gemini/gemini-exp-1114":[0,0,null,null],"gemini-exp-1114":[0,0,null,null],"gemini/gemini-exp-1206":[0,0,null,null],"gemini/gemini-gemma-2-27b-it":[3.5e-7,0.00000105,null,null],"gemini-gemma-2-27b-it":[3.5e-7,0.00000105,null,null],"gemini/gemini-gemma-2-9b-it":[3.5e-7,0.00000105,null,null],"gemini-gemma-2-9b-it":[3.5e-7,0.00000105,null,null],"gemini/gemma-3-27b-it":[0,0,null,null],"gemma-3-27b-it":[0,0,null,null],"gemini/learnlm-1.5-pro-experimental":[0,0,null,null],"learnlm-1.5-pro-experimental":[0,0,null,null],"gemini/lyria-3-clip-preview":[0,0,null,null],"lyria-3-clip-preview":[0,0,null,null],"gemini/lyria-3-pro-preview":[0,0,null,null],"lyria-3-pro-preview":[0,0,null,null],"gigachat/GigaChat-2-Lite":[0,0,null,null],"GigaChat-2-Lite":[0,0,null,null],"gigachat/GigaChat-2-Max":[0,0,null,null],"GigaChat-2-Max":[0,0,null,null],"gigachat/GigaChat-2-Pro":[0,0,null,null],"GigaChat-2-Pro":[0,0,null,null],"gigachat/Embeddings":[0,0,null,null],"Embeddings":[0,0,null,null],"gigachat/Embeddings-2":[0,0,null,null],"Embeddings-2":[0,0,null,null],"gigachat/EmbeddingsGigaR":[0,0,null,null],"EmbeddingsGigaR":[0,0,null,null],"gmi/anthropic/claude-opus-4.5":[0.000005,0.000025,null,null],"anthropic/claude-opus-4.5":[0.000005,0.000025,null,null],"gmi/anthropic/claude-sonnet-4.5":[0.000003,0.000015,null,null],"anthropic/claude-sonnet-4.5":[0.000003,0.000015,null,null],"gmi/anthropic/claude-sonnet-4":[0.000003,0.000015,null,null],"anthropic/claude-sonnet-4":[0.000003,0.000015,null,null],"gmi/anthropic/claude-opus-4":[0.000015,0.000075,null,null],"anthropic/claude-opus-4":[0.000015,0.000075,null,null],"gmi/openai/gpt-5.2":[0.00000175,0.000014,null,null],"openai/gpt-5.2":[0.00000175,0.000014,null,null],"gmi/openai/gpt-5.1":[0.00000125,0.00001,null,null],"openai/gpt-5.1":[0.00000125,0.00001,null,null],"gmi/openai/gpt-5":[0.00000125,0.00001,null,null],"openai/gpt-5":[0.00000125,0.00001,null,null],"gmi/openai/gpt-4o":[0.0000025,0.00001,null,null],"openai/gpt-4o":[0.0000025,0.00001,null,null],"gmi/openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"gmi/deepseek-ai/DeepSeek-V3.2":[2.8e-7,4e-7,null,null],"deepseek-ai/DeepSeek-V3.2":[2.8e-7,4e-7,null,null],"gmi/deepseek-ai/DeepSeek-V3-0324":[2.8e-7,8.8e-7,null,null],"gmi/google/gemini-3-pro-preview":[0.000002,0.000012,null,null],"google/gemini-3-pro-preview":[0.000002,0.000012,null,null],"gmi/google/gemini-3-flash-preview":[5e-7,0.000003,null,null],"google/gemini-3-flash-preview":[5e-7,0.000003,null,null],"gmi/moonshotai/Kimi-K2-Thinking":[8e-7,0.0000012,null,null],"moonshotai/Kimi-K2-Thinking":[8e-7,0.0000012,null,null],"gmi/MiniMaxAI/MiniMax-M2.1":[3e-7,0.0000012,null,null],"MiniMaxAI/MiniMax-M2.1":[3e-7,0.0000012,null,null],"baseten/MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"baseten/nvidia/Nemotron-120B-A12B":[3e-7,7.5e-7,null,null],"nvidia/Nemotron-120B-A12B":[3e-7,7.5e-7,null,null],"baseten/zai-org/GLM-5":[9.5e-7,0.00000315,null,null],"zai-org/GLM-5":[9.5e-7,0.00000315,null,null],"baseten/zai-org/GLM-4.7":[6e-7,0.0000022,null,null],"zai-org/GLM-4.7":[6e-7,0.0000022,null,null],"baseten/zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"baseten/moonshotai/Kimi-K2.5":[6e-7,0.000003,null,null],"moonshotai/Kimi-K2.5":[6e-7,0.000003,null,null],"baseten/moonshotai/Kimi-K2-Thinking":[6e-7,0.0000025,null,null],"baseten/moonshotai/Kimi-K2-Instruct-0905":[6e-7,0.0000025,null,null],"baseten/openai/gpt-oss-120b":[1e-7,5e-7,null,null],"baseten/deepseek-ai/DeepSeek-V3.1":[5e-7,0.0000015,null,null],"baseten/deepseek-ai/DeepSeek-V3-0324":[7.7e-7,7.7e-7,null,null],"gmi/Qwen/Qwen3-VL-235B-A22B-Instruct-FP8":[3e-7,0.0000014,null,null],"Qwen/Qwen3-VL-235B-A22B-Instruct-FP8":[3e-7,0.0000014,null,null],"gmi/zai-org/GLM-4.7-FP8":[4e-7,0.000002,null,null],"zai-org/GLM-4.7-FP8":[4e-7,0.000002,null,null],"gradient_ai/anthropic-claude-3-opus":[0.000015,0.000075,null,null],"anthropic-claude-3-opus":[0.000015,0.000075,null,null],"gradient_ai/anthropic-claude-3.5-haiku":[8e-7,0.000004,null,null],"anthropic-claude-3.5-haiku":[8e-7,0.000004,null,null],"gradient_ai/anthropic-claude-3.5-sonnet":[0.000003,0.000015,null,null],"anthropic-claude-3.5-sonnet":[0.000003,0.000015,null,null],"gradient_ai/anthropic-claude-3.7-sonnet":[0.000003,0.000015,null,null],"anthropic-claude-3.7-sonnet":[0.000003,0.000015,null,null],"gradient_ai/deepseek-r1-distill-llama-70b":[9.9e-7,9.9e-7,null,null],"deepseek-r1-distill-llama-70b":[9.9e-7,9.9e-7,null,null],"gradient_ai/llama3-8b-instruct":[2e-7,2e-7,null,null],"llama3-8b-instruct":[2e-7,2e-7,null,null],"gradient_ai/llama3.3-70b-instruct":[6.5e-7,6.5e-7,null,null],"llama3.3-70b-instruct":[6.5e-7,6.5e-7,null,null],"gradient_ai/mistral-nemo-instruct-2407":[3e-7,3e-7,null,null],"mistral-nemo-instruct-2407":[3e-7,3e-7,null,null],"gradient_ai/openai-o3":[0.000002,0.000008,null,null],"openai-o3":[0.000002,0.000008,null,null],"gradient_ai/openai-o3-mini":[0.0000011,0.0000044,null,null],"openai-o3-mini":[0.0000011,0.0000044,null,null],"lemonade/Qwen3-Coder-30B-A3B-Instruct-GGUF":[0,0,null,null],"Qwen3-Coder-30B-A3B-Instruct-GGUF":[0,0,null,null],"lemonade/gpt-oss-20b-mxfp4-GGUF":[0,0,null,null],"gpt-oss-20b-mxfp4-GGUF":[0,0,null,null],"lemonade/gpt-oss-120b-mxfp-GGUF":[0,0,null,null],"gpt-oss-120b-mxfp-GGUF":[0,0,null,null],"lemonade/Gemma-3-4b-it-GGUF":[0,0,null,null],"Gemma-3-4b-it-GGUF":[0,0,null,null],"lemonade/Qwen3-4B-Instruct-2507-GGUF":[0,0,null,null],"Qwen3-4B-Instruct-2507-GGUF":[0,0,null,null],"amazon-nova/nova-micro-v1":[3.5e-8,1.4e-7,null,null],"nova-micro-v1":[3.5e-8,1.4e-7,null,null],"amazon-nova/nova-lite-v1":[6e-8,2.4e-7,null,null],"nova-lite-v1":[6e-8,2.4e-7,null,null],"amazon-nova/nova-premier-v1":[0.0000025,0.0000125,null,null],"nova-premier-v1":[0.0000025,0.0000125,null,null],"amazon-nova/nova-pro-v1":[8e-7,0.0000032,null,null],"nova-pro-v1":[8e-7,0.0000032,null,null],"groq/llama-3.1-8b-instant":[5e-8,8e-8,null,null],"llama-3.1-8b-instant":[5e-8,8e-8,null,null],"groq/llama-3.3-70b-versatile":[5.9e-7,7.9e-7,null,null],"llama-3.3-70b-versatile":[5.9e-7,7.9e-7,null,null],"groq/gemma-7b-it":[5e-8,8e-8,null,null],"gemma-7b-it":[5e-8,8e-8,null,null],"groq/meta-llama/llama-guard-4-12b":[2e-7,2e-7,null,null],"meta-llama/llama-guard-4-12b":[2e-7,2e-7,null,null],"groq/meta-llama/llama-4-maverick-17b-128e-instruct":[2e-7,6e-7,null,null],"meta-llama/llama-4-maverick-17b-128e-instruct":[2e-7,6e-7,null,null],"groq/meta-llama/llama-4-scout-17b-16e-instruct":[1.1e-7,3.4e-7,null,null],"meta-llama/llama-4-scout-17b-16e-instruct":[1.1e-7,3.4e-7,null,null],"groq/moonshotai/kimi-k2-instruct-0905":[0.000001,0.000003,null,5e-7],"moonshotai/kimi-k2-instruct-0905":[0.000001,0.000003,null,5e-7],"groq/openai/gpt-oss-120b":[1.5e-7,6e-7,null,7.5e-8],"groq/openai/gpt-oss-20b":[7.5e-8,3e-7,null,3.75e-8],"groq/openai/gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,3.7e-8],"openai/gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,3.7e-8],"groq/qwen/qwen3-32b":[2.9e-7,5.9e-7,null,null],"qwen/qwen3-32b":[2.9e-7,5.9e-7,null,null],"hyperbolic/NousResearch/Hermes-3-Llama-3.1-70B":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/QwQ-32B":[2e-7,2e-7,null,null],"hyperbolic/Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/Qwen2.5-Coder-32B-Instruct":[1.2e-7,3e-7,null,null],"Qwen/Qwen2.5-Coder-32B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/Qwen3-235B-A22B":[0.000002,0.000002,null,null],"hyperbolic/deepseek-ai/DeepSeek-R1":[4e-7,4e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-R1-0528":[2.5e-7,2.5e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-V3":[2e-7,2e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-V3-0324":[4e-7,4e-7,null,null],"hyperbolic/meta-llama/Llama-3.2-3B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Llama-3.3-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-405B-Instruct":[1.2e-7,3e-7,null,null],"meta-llama/Meta-Llama-3.1-405B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-8B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/moonshotai/Kimi-K2-Instruct":[0.000002,0.000002,null,null],"lambda_ai/deepseek-llama3.3-70b":[2e-7,6e-7,null,null],"deepseek-llama3.3-70b":[2e-7,6e-7,null,null],"lambda_ai/deepseek-r1-0528":[2e-7,6e-7,null,null],"deepseek-r1-0528":[2e-7,6e-7,null,null],"lambda_ai/deepseek-r1-671b":[8e-7,8e-7,null,null],"deepseek-r1-671b":[8e-7,8e-7,null,null],"lambda_ai/deepseek-v3-0324":[2e-7,6e-7,null,null],"lambda_ai/hermes3-405b":[8e-7,8e-7,null,null],"hermes3-405b":[8e-7,8e-7,null,null],"lambda_ai/hermes3-70b":[1.2e-7,3e-7,null,null],"hermes3-70b":[1.2e-7,3e-7,null,null],"lambda_ai/hermes3-8b":[2.5e-8,4e-8,null,null],"hermes3-8b":[2.5e-8,4e-8,null,null],"lambda_ai/lfm-40b":[1e-7,2e-7,null,null],"lfm-40b":[1e-7,2e-7,null,null],"lambda_ai/lfm-7b":[2.5e-8,4e-8,null,null],"lfm-7b":[2.5e-8,4e-8,null,null],"lambda_ai/llama-4-maverick-17b-128e-instruct-fp8":[5e-8,1e-7,null,null],"llama-4-maverick-17b-128e-instruct-fp8":[5e-8,1e-7,null,null],"lambda_ai/llama-4-scout-17b-16e-instruct":[5e-8,1e-7,null,null],"llama-4-scout-17b-16e-instruct":[5e-8,1e-7,null,null],"lambda_ai/llama3.1-405b-instruct-fp8":[8e-7,8e-7,null,null],"llama3.1-405b-instruct-fp8":[8e-7,8e-7,null,null],"lambda_ai/llama3.1-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.1-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/llama3.1-8b-instruct":[2.5e-8,4e-8,null,null],"llama3.1-8b-instruct":[2.5e-8,4e-8,null,null],"lambda_ai/llama3.1-nemotron-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.1-nemotron-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/llama3.2-11b-vision-instruct":[1.5e-8,2.5e-8,null,null],"llama3.2-11b-vision-instruct":[1.5e-8,2.5e-8,null,null],"lambda_ai/llama3.2-3b-instruct":[1.5e-8,2.5e-8,null,null],"llama3.2-3b-instruct":[1.5e-8,2.5e-8,null,null],"lambda_ai/llama3.3-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.3-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/qwen25-coder-32b-instruct":[5e-8,1e-7,null,null],"qwen25-coder-32b-instruct":[5e-8,1e-7,null,null],"lambda_ai/qwen3-32b-fp8":[5e-8,1e-7,null,null],"qwen3-32b-fp8":[5e-8,1e-7,null,null],"minimax/MiniMax-M2.1":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2.1":[3e-7,0.0000012,3.75e-7,3e-8],"minimax/MiniMax-M2.1-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"MiniMax-M2.1-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"minimax/MiniMax-M2.5":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2.5":[3e-7,0.0000012,3.75e-7,3e-8],"minimax/MiniMax-M2.5-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"MiniMax-M2.5-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"minimax/MiniMax-M2":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2":[3e-7,0.0000012,3.75e-7,3e-8],"mistral/codestral-2405":[0.000001,0.000003,null,null],"mistral/codestral-2508":[3e-7,9e-7,null,null],"codestral-2508":[3e-7,9e-7,null,null],"mistral/codestral-latest":[0.000001,0.000003,null,null],"mistral/codestral-mamba-latest":[2.5e-7,2.5e-7,null,null],"codestral-mamba-latest":[2.5e-7,2.5e-7,null,null],"mistral/devstral-medium-2507":[4e-7,0.000002,null,null],"devstral-medium-2507":[4e-7,0.000002,null,null],"mistral/devstral-small-2505":[1e-7,3e-7,null,null],"devstral-small-2505":[1e-7,3e-7,null,null],"mistral/devstral-small-2507":[1e-7,3e-7,null,null],"devstral-small-2507":[1e-7,3e-7,null,null],"mistral/devstral-small-latest":[1e-7,3e-7,null,null],"devstral-small-latest":[1e-7,3e-7,null,null],"mistral/labs-devstral-small-2512":[1e-7,3e-7,null,null],"labs-devstral-small-2512":[1e-7,3e-7,null,null],"mistral/devstral-latest":[4e-7,0.000002,null,null],"devstral-latest":[4e-7,0.000002,null,null],"mistral/devstral-medium-latest":[4e-7,0.000002,null,null],"devstral-medium-latest":[4e-7,0.000002,null,null],"mistral/devstral-2512":[4e-7,0.000002,null,null],"devstral-2512":[4e-7,0.000002,null,null],"mistral/magistral-medium-2506":[0.000002,0.000005,null,null],"magistral-medium-2506":[0.000002,0.000005,null,null],"mistral/magistral-medium-2509":[0.000002,0.000005,null,null],"magistral-medium-2509":[0.000002,0.000005,null,null],"mistral/magistral-medium-1-2-2509":[0.000002,0.000005,null,null],"magistral-medium-1-2-2509":[0.000002,0.000005,null,null],"mistral/magistral-medium-latest":[0.000002,0.000005,null,null],"magistral-medium-latest":[0.000002,0.000005,null,null],"mistral/magistral-small-2506":[5e-7,0.0000015,null,null],"magistral-small-2506":[5e-7,0.0000015,null,null],"mistral/magistral-small-latest":[5e-7,0.0000015,null,null],"magistral-small-latest":[5e-7,0.0000015,null,null],"mistral/magistral-small-1-2-2509":[5e-7,0.0000015,null,null],"magistral-small-1-2-2509":[5e-7,0.0000015,null,null],"mistral/mistral-large-2402":[0.000004,0.000012,null,null],"mistral/mistral-large-2407":[0.000003,0.000009,null,null],"mistral/mistral-large-2411":[0.000002,0.000006,null,null],"mistral-large-2411":[0.000002,0.000006,null,null],"mistral/mistral-large-latest":[5e-7,0.0000015,null,null],"mistral/mistral-large-3":[5e-7,0.0000015,null,null],"mistral/mistral-large-2512":[5e-7,0.0000015,null,null],"mistral-large-2512":[5e-7,0.0000015,null,null],"mistral/mistral-medium":[0.0000027,0.0000081,null,null],"mistral-medium":[0.0000027,0.0000081,null,null],"mistral/mistral-medium-2312":[0.0000027,0.0000081,null,null],"mistral-medium-2312":[0.0000027,0.0000081,null,null],"mistral/mistral-medium-2505":[4e-7,0.000002,null,null],"mistral/mistral-medium-latest":[4e-7,0.000002,null,null],"mistral-medium-latest":[4e-7,0.000002,null,null],"mistral/mistral-medium-3-1-2508":[4e-7,0.000002,null,null],"mistral-medium-3-1-2508":[4e-7,0.000002,null,null],"mistral/mistral-small":[1e-7,3e-7,null,null],"mistral/mistral-small-latest":[6e-8,1.8e-7,null,null],"mistral-small-latest":[6e-8,1.8e-7,null,null],"mistral/mistral-small-3-2-2506":[6e-8,1.8e-7,null,null],"mistral-small-3-2-2506":[6e-8,1.8e-7,null,null],"mistral/ministral-3-3b-2512":[1e-7,1e-7,null,null],"ministral-3-3b-2512":[1e-7,1e-7,null,null],"mistral/ministral-3-8b-2512":[1.5e-7,1.5e-7,null,null],"ministral-3-8b-2512":[1.5e-7,1.5e-7,null,null],"mistral/ministral-3-14b-2512":[2e-7,2e-7,null,null],"ministral-3-14b-2512":[2e-7,2e-7,null,null],"mistral/mistral-tiny":[2.5e-7,2.5e-7,null,null],"mistral-tiny":[2.5e-7,2.5e-7,null,null],"mistral/open-codestral-mamba":[2.5e-7,2.5e-7,null,null],"open-codestral-mamba":[2.5e-7,2.5e-7,null,null],"mistral/open-mistral-7b":[2.5e-7,2.5e-7,null,null],"open-mistral-7b":[2.5e-7,2.5e-7,null,null],"mistral/open-mistral-nemo":[3e-7,3e-7,null,null],"open-mistral-nemo":[3e-7,3e-7,null,null],"mistral/open-mistral-nemo-2407":[3e-7,3e-7,null,null],"open-mistral-nemo-2407":[3e-7,3e-7,null,null],"mistral/open-mixtral-8x22b":[0.000002,0.000006,null,null],"open-mixtral-8x22b":[0.000002,0.000006,null,null],"mistral/open-mixtral-8x7b":[7e-7,7e-7,null,null],"open-mixtral-8x7b":[7e-7,7e-7,null,null],"mistral/pixtral-12b-2409":[1.5e-7,1.5e-7,null,null],"pixtral-12b-2409":[1.5e-7,1.5e-7,null,null],"mistral/pixtral-large-2411":[0.000002,0.000006,null,null],"pixtral-large-2411":[0.000002,0.000006,null,null],"mistral/pixtral-large-latest":[0.000002,0.000006,null,null],"pixtral-large-latest":[0.000002,0.000006,null,null],"moonshot/kimi-k2-0711-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-0711-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-0905-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-0905-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-turbo-preview":[0.00000115,0.000008,null,1.5e-7],"kimi-k2-turbo-preview":[0.00000115,0.000008,null,1.5e-7],"moonshot/kimi-k2.5":[6e-7,0.000003,null,1e-7],"moonshot/kimi-k2.6":[9.5e-7,0.000004,null,1.6e-7],"kimi-k2.6":[9.5e-7,0.000004,null,1.6e-7],"moonshot/kimi-latest":[0.000002,0.000005,null,1.5e-7],"kimi-latest":[0.000002,0.000005,null,1.5e-7],"moonshot/kimi-latest-128k":[0.000002,0.000005,null,1.5e-7],"kimi-latest-128k":[0.000002,0.000005,null,1.5e-7],"moonshot/kimi-latest-32k":[0.000001,0.000003,null,1.5e-7],"kimi-latest-32k":[0.000001,0.000003,null,1.5e-7],"moonshot/kimi-latest-8k":[2e-7,0.000002,null,1.5e-7],"kimi-latest-8k":[2e-7,0.000002,null,1.5e-7],"moonshot/kimi-thinking-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-thinking-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-thinking":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-thinking":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-thinking-turbo":[0.00000115,0.000008,null,1.5e-7],"kimi-k2-thinking-turbo":[0.00000115,0.000008,null,1.5e-7],"moonshot/moonshot-v1-128k":[0.000002,0.000005,null,null],"moonshot-v1-128k":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-128k-0430":[0.000002,0.000005,null,null],"moonshot-v1-128k-0430":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-128k-vision-preview":[0.000002,0.000005,null,null],"moonshot-v1-128k-vision-preview":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-32k":[0.000001,0.000003,null,null],"moonshot-v1-32k":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-32k-0430":[0.000001,0.000003,null,null],"moonshot-v1-32k-0430":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-32k-vision-preview":[0.000001,0.000003,null,null],"moonshot-v1-32k-vision-preview":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-8k":[2e-7,0.000002,null,null],"moonshot-v1-8k":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-8k-0430":[2e-7,0.000002,null,null],"moonshot-v1-8k-0430":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-8k-vision-preview":[2e-7,0.000002,null,null],"moonshot-v1-8k-vision-preview":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-auto":[0.000002,0.000005,null,null],"moonshot-v1-auto":[0.000002,0.000005,null,null],"morph/morph-v3-fast":[8e-7,0.0000012,null,null],"morph-v3-fast":[8e-7,0.0000012,null,null],"morph/morph-v3-large":[9e-7,0.0000019,null,null],"morph-v3-large":[9e-7,0.0000019,null,null],"nscale/Qwen/QwQ-32B":[1.8e-7,2e-7,null,null],"nscale/Qwen/Qwen2.5-Coder-32B-Instruct":[6e-8,2e-7,null,null],"nscale/Qwen/Qwen2.5-Coder-3B-Instruct":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-3B-Instruct":[1e-8,3e-8,null,null],"nscale/Qwen/Qwen2.5-Coder-7B-Instruct":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-7B-Instruct":[1e-8,3e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[3.75e-7,3.75e-7,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-8B":[2.5e-8,2.5e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Llama-8B":[2.5e-8,2.5e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B":[9e-8,9e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B":[9e-8,9e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":[7e-8,7e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":[7e-8,7e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[1.5e-7,1.5e-7,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B":[2e-7,2e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B":[2e-7,2e-7,null,null],"nscale/meta-llama/Llama-3.1-8B-Instruct":[3e-8,3e-8,null,null],"meta-llama/Llama-3.1-8B-Instruct":[3e-8,3e-8,null,null],"nscale/meta-llama/Llama-3.3-70B-Instruct":[2e-7,2e-7,null,null],"nscale/meta-llama/Llama-4-Scout-17B-16E-Instruct":[9e-8,2.9e-7,null,null],"nscale/mistralai/mixtral-8x22b-instruct-v0.1":[6e-7,6e-7,null,null],"mistralai/mixtral-8x22b-instruct-v0.1":[6e-7,6e-7,null,null],"nebius/deepseek-ai/DeepSeek-R1":[8e-7,0.0000024,null,null],"nebius/deepseek-ai/DeepSeek-R1-0528":[8e-7,0.0000024,null,null],"nebius/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2.5e-7,7.5e-7,null,null],"nebius/deepseek-ai/DeepSeek-V3":[5e-7,0.0000015,null,null],"nebius/deepseek-ai/DeepSeek-V3-0324":[5e-7,0.0000015,null,null],"nebius/google/gemma-3-27b-it":[6e-8,2e-7,null,null],"nebius/meta-llama/Llama-3.3-70B-Instruct":[1.3e-7,4e-7,null,null],"nebius/meta-llama/Llama-Guard-3-8B":[2e-8,6e-8,null,null],"nebius/meta-llama/Meta-Llama-3.1-8B-Instruct":[2e-8,6e-8,null,null],"nebius/meta-llama/Meta-Llama-3.1-70B-Instruct":[1.3e-7,4e-7,null,null],"nebius/meta-llama/Meta-Llama-3.1-405B-Instruct":[0.000001,0.000003,null,null],"nebius/mistralai/Mistral-Nemo-Instruct-2407":[4e-8,1.2e-7,null,null],"nebius/NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000003,null,null],"nebius/nvidia/Llama-3.1-Nemotron-Ultra-253B-v1":[6e-7,0.0000018,null,null],"nvidia/Llama-3.1-Nemotron-Ultra-253B-v1":[6e-7,0.0000018,null,null],"nebius/nvidia/Llama-3.3-Nemotron-Super-49B-v1":[1e-7,4e-7,null,null],"nvidia/Llama-3.3-Nemotron-Super-49B-v1":[1e-7,4e-7,null,null],"nebius/Qwen/Qwen3-235B-A22B":[2e-7,6e-7,null,null],"nebius/Qwen/Qwen3-32B":[1e-7,3e-7,null,null],"nebius/Qwen/Qwen3-30B-A3B":[1e-7,3e-7,null,null],"nebius/Qwen/Qwen3-14B":[8e-8,2.4e-7,null,null],"nebius/Qwen/Qwen3-4B":[8e-8,2.4e-7,null,null],"Qwen/Qwen3-4B":[8e-8,2.4e-7,null,null],"nebius/Qwen/QwQ-32B":[1.5e-7,4.5e-7,null,null],"nebius/Qwen/Qwen2.5-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2.5-32B-Instruct":[6e-8,2e-7,null,null],"Qwen/Qwen2.5-32B-Instruct":[6e-8,2e-7,null,null],"nebius/Qwen/Qwen2.5-Coder-7B":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-7B":[1e-8,3e-8,null,null],"nebius/Qwen/Qwen2.5-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"Qwen/Qwen2.5-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"Qwen/Qwen2-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2-VL-7B-Instruct":[2e-8,6e-8,null,null],"Qwen/Qwen2-VL-7B-Instruct":[2e-8,6e-8,null,null],"nebius/BAAI/bge-en-icl":[1e-8,0,null,null],"BAAI/bge-en-icl":[1e-8,0,null,null],"nebius/BAAI/bge-multilingual-gemma2":[1e-8,0,null,null],"BAAI/bge-multilingual-gemma2":[1e-8,0,null,null],"nebius/intfloat/e5-mistral-7b-instruct":[1e-8,0,null,null],"intfloat/e5-mistral-7b-instruct":[1e-8,0,null,null],"oci/meta.llama-3.1-405b-instruct":[0.00001068,0.00001068,null,null],"meta.llama-3.1-405b-instruct":[0.00001068,0.00001068,null,null],"oci/meta.llama-3.2-90b-vision-instruct":[0.000002,0.000002,null,null],"meta.llama-3.2-90b-vision-instruct":[0.000002,0.000002,null,null],"oci/meta.llama-3.3-70b-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-3.3-70b-instruct":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-4-maverick-17b-128e-instruct-fp8":[7.2e-7,7.2e-7,null,null],"meta.llama-4-maverick-17b-128e-instruct-fp8":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-4-scout-17b-16e-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-4-scout-17b-16e-instruct":[7.2e-7,7.2e-7,null,null],"oci/xai.grok-3":[0.000003,0.000015,null,null],"xai.grok-3":[0.000003,0.000015,null,null],"oci/xai.grok-3-fast":[0.000005,0.000025,null,null],"xai.grok-3-fast":[0.000005,0.000025,null,null],"oci/xai.grok-3-mini":[3e-7,5e-7,null,null],"xai.grok-3-mini":[3e-7,5e-7,null,null],"oci/xai.grok-3-mini-fast":[6e-7,0.000004,null,null],"xai.grok-3-mini-fast":[6e-7,0.000004,null,null],"oci/xai.grok-4":[0.000003,0.000015,null,null],"xai.grok-4":[0.000003,0.000015,null,null],"oci/cohere.command-latest":[0.00000156,0.00000156,null,null],"cohere.command-latest":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-03-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-03-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-plus-latest":[0.00000156,0.00000156,null,null],"cohere.command-plus-latest":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-reasoning-08-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-reasoning-08-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-vision-07-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-vision-07-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-translate-08-2025":[9e-8,9e-8,null,null],"cohere.command-a-translate-08-2025":[9e-8,9e-8,null,null],"oci/cohere.command-r-08-2024":[1.5e-7,1.5e-7,null,null],"cohere.command-r-08-2024":[1.5e-7,1.5e-7,null,null],"oci/cohere.command-r-plus-08-2024":[0.00000156,0.00000156,null,null],"cohere.command-r-plus-08-2024":[0.00000156,0.00000156,null,null],"oci/meta.llama-3.2-11b-vision-instruct":[0.000002,0.000002,null,null],"meta.llama-3.2-11b-vision-instruct":[0.000002,0.000002,null,null],"oci/meta.llama-3.1-70b-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-3.1-70b-instruct":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-3.3-70b-instruct-fp8-dynamic":[7.2e-7,7.2e-7,null,null],"meta.llama-3.3-70b-instruct-fp8-dynamic":[7.2e-7,7.2e-7,null,null],"oci/xai.grok-4-fast":[0.000005,0.000025,null,null],"xai.grok-4-fast":[0.000005,0.000025,null,null],"oci/xai.grok-4.1-fast":[0.000005,0.000025,null,null],"xai.grok-4.1-fast":[0.000005,0.000025,null,null],"oci/xai.grok-4.20":[0.000003,0.000015,null,null],"xai.grok-4.20":[0.000003,0.000015,null,null],"oci/xai.grok-4.20-multi-agent":[0.000003,0.000015,null,null],"xai.grok-4.20-multi-agent":[0.000003,0.000015,null,null],"oci/xai.grok-code-fast-1":[0.000005,0.000025,null,null],"xai.grok-code-fast-1":[0.000005,0.000025,null,null],"oci/google.gemini-2.5-pro":[0.00000125,0.00001,null,null],"google.gemini-2.5-pro":[0.00000125,0.00001,null,null],"oci/google.gemini-2.5-flash":[1.5e-7,6e-7,null,null],"google.gemini-2.5-flash":[1.5e-7,6e-7,null,null],"oci/google.gemini-2.5-flash-lite":[7.5e-8,3e-7,null,null],"google.gemini-2.5-flash-lite":[7.5e-8,3e-7,null,null],"oci/cohere.embed-english-v3.0":[1e-7,0,null,null],"cohere.embed-english-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-light-v3.0":[1e-7,0,null,null],"cohere.embed-english-light-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-light-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-light-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-image-v3.0":[1e-7,0,null,null],"cohere.embed-english-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-light-image-v3.0":[1e-7,0,null,null],"cohere.embed-english-light-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-light-image-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-light-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-v4.0":[1.2e-7,0,null,null],"cohere.embed-v4.0":[1.2e-7,0,null,null],"ollama/codegeex4":[0,0,null,null],"codegeex4":[0,0,null,null],"ollama/codegemma":[0,0,null,null],"codegemma":[0,0,null,null],"ollama/codellama":[0,0,null,null],"codellama":[0,0,null,null],"ollama/deepseek-coder-v2-base":[0,0,null,null],"deepseek-coder-v2-base":[0,0,null,null],"ollama/deepseek-coder-v2-instruct":[0,0,null,null],"deepseek-coder-v2-instruct":[0,0,null,null],"ollama/deepseek-coder-v2-lite-base":[0,0,null,null],"deepseek-coder-v2-lite-base":[0,0,null,null],"ollama/deepseek-coder-v2-lite-instruct":[0,0,null,null],"deepseek-coder-v2-lite-instruct":[0,0,null,null],"ollama/deepseek-v3.1:671b-cloud":[0,0,null,null],"deepseek-v3.1:671b-cloud":[0,0,null,null],"ollama/gpt-oss:120b-cloud":[0,0,null,null],"gpt-oss:120b-cloud":[0,0,null,null],"ollama/gpt-oss:20b-cloud":[0,0,null,null],"gpt-oss:20b-cloud":[0,0,null,null],"ollama/internlm2_5-20b-chat":[0,0,null,null],"internlm2_5-20b-chat":[0,0,null,null],"ollama/llama2":[0,0,null,null],"llama2":[0,0,null,null],"ollama/llama2-uncensored":[0,0,null,null],"llama2-uncensored":[0,0,null,null],"ollama/llama2:13b":[0,0,null,null],"llama2:13b":[0,0,null,null],"ollama/llama2:70b":[0,0,null,null],"llama2:70b":[0,0,null,null],"ollama/llama2:7b":[0,0,null,null],"llama2:7b":[0,0,null,null],"ollama/llama3":[0,0,null,null],"llama3":[0,0,null,null],"ollama/llama3.1":[0,0,null,null],"llama3.1":[0,0,null,null],"ollama/llama3:70b":[0,0,null,null],"llama3:70b":[0,0,null,null],"ollama/llama3:8b":[0,0,null,null],"llama3:8b":[0,0,null,null],"ollama/mistral":[0,0,null,null],"mistral":[0,0,null,null],"ollama/mistral-7B-Instruct-v0.1":[0,0,null,null],"mistral-7B-Instruct-v0.1":[0,0,null,null],"ollama/mistral-7B-Instruct-v0.2":[0,0,null,null],"mistral-7B-Instruct-v0.2":[0,0,null,null],"ollama/mistral-large-instruct-2407":[0,0,null,null],"mistral-large-instruct-2407":[0,0,null,null],"ollama/mixtral-8x22B-Instruct-v0.1":[0,0,null,null],"mixtral-8x22B-Instruct-v0.1":[0,0,null,null],"ollama/mixtral-8x7B-Instruct-v0.1":[0,0,null,null],"mixtral-8x7B-Instruct-v0.1":[0,0,null,null],"ollama/orca-mini":[0,0,null,null],"orca-mini":[0,0,null,null],"ollama/qwen3-coder:480b-cloud":[0,0,null,null],"qwen3-coder:480b-cloud":[0,0,null,null],"ollama/vicuna":[0,0,null,null],"vicuna":[0,0,null,null],"openrouter/anthropic/claude-3-haiku":[2.5e-7,0.00000125,null,null],"anthropic/claude-3-haiku":[2.5e-7,0.00000125,null,null],"openrouter/anthropic/claude-3.5-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-3.5-sonnet":[0.000003,0.000015,null,null],"openrouter/anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"openrouter/anthropic/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"openrouter/anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"openrouter/anthropic/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-sonnet-4.6":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-sonnet-4.6":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-opus-4.5":[0.000005,0.000025,0.00000625,5e-7],"openrouter/anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"openrouter/anthropic/claude-sonnet-4.5":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"openrouter/anthropic/claude-opus-4.7":[0.000005,0.000025,0.00000625,5e-7],"anthropic/claude-opus-4.7":[0.000005,0.000025,0.00000625,5e-7],"openrouter/bytedance/ui-tars-1.5-7b":[1e-7,2e-7,null,null],"bytedance/ui-tars-1.5-7b":[1e-7,2e-7,null,null],"openrouter/deepseek/deepseek-chat":[1.4e-7,2.8e-7,null,null],"openrouter/deepseek/deepseek-chat-v3-0324":[1.4e-7,2.8e-7,null,null],"deepseek/deepseek-chat-v3-0324":[1.4e-7,2.8e-7,null,null],"openrouter/deepseek/deepseek-chat-v3.1":[2e-7,8e-7,null,null],"deepseek/deepseek-chat-v3.1":[2e-7,8e-7,null,null],"openrouter/deepseek/deepseek-v3.2":[2.8e-7,4e-7,null,null],"openrouter/deepseek/deepseek-v3.2-exp":[2e-7,4e-7,null,null],"deepseek/deepseek-v3.2-exp":[2e-7,4e-7,null,null],"openrouter/deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"openrouter/deepseek/deepseek-r1-0528":[5e-7,0.00000215,null,null],"deepseek/deepseek-r1-0528":[5e-7,0.00000215,null,null],"openrouter/google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"openrouter/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"openrouter/google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"openrouter/google/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"openrouter/google/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"openrouter/google/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"google/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"openrouter/google/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"google/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"openrouter/gryphe/mythomax-l2-13b":[0.000001875,0.000001875,null,null],"gryphe/mythomax-l2-13b":[0.000001875,0.000001875,null,null],"openrouter/mancer/weaver":[0.000005625,0.000005625,null,null],"mancer/weaver":[0.000005625,0.000005625,null,null],"openrouter/meta-llama/llama-3-70b-instruct":[5.9e-7,7.9e-7,null,null],"meta-llama/llama-3-70b-instruct":[5.9e-7,7.9e-7,null,null],"openrouter/minimax/minimax-m2":[2.55e-7,0.00000102,null,null],"minimax/minimax-m2":[2.55e-7,0.00000102,null,null],"openrouter/mistralai/devstral-2512":[1.5e-7,6e-7,null,null],"mistralai/devstral-2512":[1.5e-7,6e-7,null,null],"openrouter/mistralai/ministral-3b-2512":[1e-7,1e-7,null,null],"mistralai/ministral-3b-2512":[1e-7,1e-7,null,null],"openrouter/mistralai/ministral-8b-2512":[1.5e-7,1.5e-7,null,null],"mistralai/ministral-8b-2512":[1.5e-7,1.5e-7,null,null],"openrouter/mistralai/ministral-14b-2512":[2e-7,2e-7,null,null],"mistralai/ministral-14b-2512":[2e-7,2e-7,null,null],"openrouter/mistralai/mistral-large-2512":[5e-7,0.0000015,null,null],"mistralai/mistral-large-2512":[5e-7,0.0000015,null,null],"openrouter/mistralai/mistral-7b-instruct":[1.3e-7,1.3e-7,null,null],"mistralai/mistral-7b-instruct":[1.3e-7,1.3e-7,null,null],"openrouter/mistralai/mistral-large":[0.000008,0.000024,null,null],"mistralai/mistral-large":[0.000008,0.000024,null,null],"openrouter/mistralai/mistral-small-3.1-24b-instruct":[1e-7,3e-7,null,null],"mistralai/mistral-small-3.1-24b-instruct":[1e-7,3e-7,null,null],"openrouter/mistralai/mistral-small-3.2-24b-instruct":[1e-7,3e-7,null,null],"mistralai/mistral-small-3.2-24b-instruct":[1e-7,3e-7,null,null],"openrouter/mistralai/mixtral-8x22b-instruct":[6.5e-7,6.5e-7,null,null],"mistralai/mixtral-8x22b-instruct":[6.5e-7,6.5e-7,null,null],"openrouter/moonshotai/kimi-k2.5":[6e-7,0.000003,null,1e-7],"moonshotai/kimi-k2.5":[6e-7,0.000003,null,1e-7],"openrouter/openai/gpt-3.5-turbo":[0.0000015,0.000002,null,null],"openai/gpt-3.5-turbo":[0.0000015,0.000002,null,null],"openrouter/openai/gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"openai/gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"openrouter/openai/gpt-4":[0.00003,0.00006,null,null],"openai/gpt-4":[0.00003,0.00006,null,null],"openrouter/openai/gpt-4.1":[0.000002,0.000008,null,5e-7],"openai/gpt-4.1":[0.000002,0.000008,null,5e-7],"openrouter/openai/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"openai/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"openrouter/openai/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"openai/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"openrouter/openai/gpt-4o":[0.0000025,0.00001,null,null],"openrouter/openai/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"openai/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"openrouter/openai/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"openai/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"openai/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"openrouter/openai/gpt-5-nano":[5e-8,4e-7,null,5e-9],"openai/gpt-5-nano":[5e-8,4e-7,null,5e-9],"openrouter/openai/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"openai/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5.2-pro":[0.000021,0.000168,null,null],"openai/gpt-5.2-pro":[0.000021,0.000168,null,null],"openrouter/openai/gpt-oss-120b":[1.8e-7,8e-7,null,null],"openrouter/openai/gpt-oss-20b":[2e-8,1e-7,null,null],"openrouter/openai/o1":[0.000015,0.00006,null,0.0000075],"openai/o1":[0.000015,0.00006,null,0.0000075],"openrouter/openai/o3-mini":[0.0000011,0.0000044,null,null],"openai/o3-mini":[0.0000011,0.0000044,null,null],"openrouter/openai/o3-mini-high":[0.0000011,0.0000044,null,null],"openai/o3-mini-high":[0.0000011,0.0000044,null,null],"openrouter/qwen/qwen-2.5-coder-32b-instruct":[1.8e-7,1.8e-7,null,null],"qwen/qwen-2.5-coder-32b-instruct":[1.8e-7,1.8e-7,null,null],"openrouter/qwen/qwen-vl-plus":[2.1e-7,6.3e-7,null,null],"qwen/qwen-vl-plus":[2.1e-7,6.3e-7,null,null],"openrouter/qwen/qwen3-coder":[2.2e-7,9.5e-7,null,null],"qwen/qwen3-coder":[2.2e-7,9.5e-7,null,null],"openrouter/qwen/qwen3-coder-plus":[0.000001,0.000005,null,null],"qwen/qwen3-coder-plus":[0.000001,0.000005,null,null],"openrouter/qwen/qwen3-235b-a22b-2507":[7.1e-8,1e-7,null,null],"qwen/qwen3-235b-a22b-2507":[7.1e-8,1e-7,null,null],"openrouter/qwen/qwen3-235b-a22b-thinking-2507":[1.1e-7,6e-7,null,null],"qwen/qwen3-235b-a22b-thinking-2507":[1.1e-7,6e-7,null,null],"openrouter/qwen/qwen3.5-35b-a3b":[2.5e-7,0.000002,null,null],"qwen/qwen3.5-35b-a3b":[2.5e-7,0.000002,null,null],"openrouter/qwen/qwen3.5-27b":[3e-7,0.0000024,null,null],"qwen/qwen3.5-27b":[3e-7,0.0000024,null,null],"openrouter/qwen/qwen3.5-122b-a10b":[4e-7,0.000002,null,null],"qwen/qwen3.5-122b-a10b":[4e-7,0.000002,null,null],"openrouter/qwen/qwen3.5-flash-02-23":[1e-7,4e-7,null,null],"qwen/qwen3.5-flash-02-23":[1e-7,4e-7,null,null],"openrouter/qwen/qwen3.5-plus-02-15":[4e-7,0.0000024,null,null],"qwen/qwen3.5-plus-02-15":[4e-7,0.0000024,null,null],"openrouter/qwen/qwen3.5-397b-a17b":[6e-7,0.0000036,null,null],"qwen/qwen3.5-397b-a17b":[6e-7,0.0000036,null,null],"openrouter/switchpoint/router":[8.5e-7,0.0000034,null,null],"switchpoint/router":[8.5e-7,0.0000034,null,null],"openrouter/undi95/remm-slerp-l2-13b":[0.000001875,0.000001875,null,null],"undi95/remm-slerp-l2-13b":[0.000001875,0.000001875,null,null],"openrouter/x-ai/grok-4":[0.000003,0.000015,null,null],"x-ai/grok-4":[0.000003,0.000015,null,null],"openrouter/z-ai/glm-4.6":[4e-7,0.00000175,null,null],"z-ai/glm-4.6":[4e-7,0.00000175,null,null],"openrouter/z-ai/glm-4.6:exacto":[4.5e-7,0.0000019,null,null],"z-ai/glm-4.6:exacto":[4.5e-7,0.0000019,null,null],"openrouter/xiaomi/mimo-v2-flash":[9e-8,2.9e-7,0,0],"xiaomi/mimo-v2-flash":[9e-8,2.9e-7,0,0],"openrouter/z-ai/glm-4.7":[4e-7,0.0000015,0,0],"z-ai/glm-4.7":[4e-7,0.0000015,0,0],"openrouter/z-ai/glm-4.7-flash":[7e-8,4e-7,0,0],"z-ai/glm-4.7-flash":[7e-8,4e-7,0,0],"openrouter/z-ai/glm-5":[8e-7,0.00000256,null,null],"z-ai/glm-5":[8e-7,0.00000256,null,null],"openrouter/minimax/minimax-m2.1":[2.7e-7,0.0000012,0,0],"minimax/minimax-m2.1":[2.7e-7,0.0000012,0,0],"openrouter/minimax/minimax-m2.5":[3e-7,0.0000011,null,1.5e-7],"minimax/minimax-m2.5":[3e-7,0.0000011,null,1.5e-7],"openrouter/openrouter/auto":[0,0,null,null],"openrouter/auto":[0,0,null,null],"openrouter/openrouter/free":[0,0,null,null],"openrouter/free":[0,0,null,null],"openrouter/openrouter/bodybuilder":[0,0,null,null],"openrouter/bodybuilder":[0,0,null,null],"ovhcloud/DeepSeek-R1-Distill-Llama-70B":[6.7e-7,6.7e-7,null,null],"DeepSeek-R1-Distill-Llama-70B":[6.7e-7,6.7e-7,null,null],"ovhcloud/Llama-3.1-8B-Instruct":[1e-7,1e-7,null,null],"Llama-3.1-8B-Instruct":[1e-7,1e-7,null,null],"ovhcloud/Meta-Llama-3_1-70B-Instruct":[6.7e-7,6.7e-7,null,null],"Meta-Llama-3_1-70B-Instruct":[6.7e-7,6.7e-7,null,null],"ovhcloud/Meta-Llama-3_3-70B-Instruct":[6.7e-7,6.7e-7,null,null],"Meta-Llama-3_3-70B-Instruct":[6.7e-7,6.7e-7,null,null],"ovhcloud/Mistral-7B-Instruct-v0.3":[1e-7,1e-7,null,null],"Mistral-7B-Instruct-v0.3":[1e-7,1e-7,null,null],"ovhcloud/Mistral-Nemo-Instruct-2407":[1.3e-7,1.3e-7,null,null],"Mistral-Nemo-Instruct-2407":[1.3e-7,1.3e-7,null,null],"ovhcloud/Mistral-Small-3.2-24B-Instruct-2506":[9e-8,2.8e-7,null,null],"Mistral-Small-3.2-24B-Instruct-2506":[9e-8,2.8e-7,null,null],"ovhcloud/Mixtral-8x7B-Instruct-v0.1":[6.3e-7,6.3e-7,null,null],"Mixtral-8x7B-Instruct-v0.1":[6.3e-7,6.3e-7,null,null],"ovhcloud/Qwen2.5-Coder-32B-Instruct":[8.7e-7,8.7e-7,null,null],"Qwen2.5-Coder-32B-Instruct":[8.7e-7,8.7e-7,null,null],"ovhcloud/Qwen2.5-VL-72B-Instruct":[9.1e-7,9.1e-7,null,null],"Qwen2.5-VL-72B-Instruct":[9.1e-7,9.1e-7,null,null],"ovhcloud/Qwen3-32B":[8e-8,2.3e-7,null,null],"Qwen3-32B":[8e-8,2.3e-7,null,null],"ovhcloud/gpt-oss-120b":[8e-8,4e-7,null,null],"ovhcloud/gpt-oss-20b":[4e-8,1.5e-7,null,null],"gpt-oss-20b":[4e-8,1.5e-7,null,null],"ovhcloud/llava-v1.6-mistral-7b-hf":[2.9e-7,2.9e-7,null,null],"llava-v1.6-mistral-7b-hf":[2.9e-7,2.9e-7,null,null],"ovhcloud/mamba-codestral-7B-v0.1":[1.9e-7,1.9e-7,null,null],"mamba-codestral-7B-v0.1":[1.9e-7,1.9e-7,null,null],"palm/chat-bison":[1.25e-7,1.25e-7,null,null],"chat-bison":[1.25e-7,1.25e-7,null,null],"palm/chat-bison-001":[1.25e-7,1.25e-7,null,null],"chat-bison-001":[1.25e-7,1.25e-7,null,null],"palm/text-bison":[1.25e-7,1.25e-7,null,null],"text-bison":[1.25e-7,1.25e-7,null,null],"palm/text-bison-001":[1.25e-7,1.25e-7,null,null],"text-bison-001":[1.25e-7,1.25e-7,null,null],"palm/text-bison-safety-off":[1.25e-7,1.25e-7,null,null],"text-bison-safety-off":[1.25e-7,1.25e-7,null,null],"palm/text-bison-safety-recitation-off":[1.25e-7,1.25e-7,null,null],"text-bison-safety-recitation-off":[1.25e-7,1.25e-7,null,null],"perplexity/codellama-34b-instruct":[3.5e-7,0.0000014,null,null],"codellama-34b-instruct":[3.5e-7,0.0000014,null,null],"perplexity/codellama-70b-instruct":[7e-7,0.0000028,null,null],"codellama-70b-instruct":[7e-7,0.0000028,null,null],"perplexity/llama-2-70b-chat":[7e-7,0.0000028,null,null],"llama-2-70b-chat":[7e-7,0.0000028,null,null],"perplexity/llama-3.1-70b-instruct":[0.000001,0.000001,null,null],"llama-3.1-70b-instruct":[0.000001,0.000001,null,null],"perplexity/llama-3.1-8b-instruct":[2e-7,2e-7,null,null],"llama-3.1-8b-instruct":[2e-7,2e-7,null,null],"perplexity/mistral-7b-instruct":[7e-8,2.8e-7,null,null],"mistral-7b-instruct":[7e-8,2.8e-7,null,null],"perplexity/mixtral-8x7b-instruct":[7e-8,2.8e-7,null,null],"mixtral-8x7b-instruct":[7e-8,2.8e-7,null,null],"perplexity/pplx-70b-chat":[7e-7,0.0000028,null,null],"pplx-70b-chat":[7e-7,0.0000028,null,null],"perplexity/pplx-70b-online":[0,0.0000028,null,null],"pplx-70b-online":[0,0.0000028,null,null],"perplexity/pplx-7b-chat":[7e-8,2.8e-7,null,null],"pplx-7b-chat":[7e-8,2.8e-7,null,null],"perplexity/pplx-7b-online":[0,2.8e-7,null,null],"pplx-7b-online":[0,2.8e-7,null,null],"perplexity/sonar":[0.000001,0.000001,null,null],"sonar":[0.000001,0.000001,null,null],"perplexity/sonar-deep-research":[0.000002,0.000008,null,null],"sonar-deep-research":[0.000002,0.000008,null,null],"perplexity/sonar-medium-chat":[6e-7,0.0000018,null,null],"sonar-medium-chat":[6e-7,0.0000018,null,null],"perplexity/sonar-medium-online":[0,0.0000018,null,null],"sonar-medium-online":[0,0.0000018,null,null],"perplexity/sonar-pro":[0.000003,0.000015,null,null],"sonar-pro":[0.000003,0.000015,null,null],"perplexity/sonar-reasoning":[0.000001,0.000005,null,null],"sonar-reasoning":[0.000001,0.000005,null,null],"perplexity/sonar-reasoning-pro":[0.000002,0.000008,null,null],"sonar-reasoning-pro":[0.000002,0.000008,null,null],"perplexity/sonar-small-chat":[7e-8,2.8e-7,null,null],"sonar-small-chat":[7e-8,2.8e-7,null,null],"perplexity/sonar-small-online":[0,2.8e-7,null,null],"sonar-small-online":[0,2.8e-7,null,null],"publicai/swiss-ai/apertus-8b-instruct":[0,0,null,null],"swiss-ai/apertus-8b-instruct":[0,0,null,null],"publicai/swiss-ai/apertus-70b-instruct":[0,0,null,null],"swiss-ai/apertus-70b-instruct":[0,0,null,null],"publicai/aisingapore/Gemma-SEA-LION-v4-27B-IT":[0,0,null,null],"aisingapore/Gemma-SEA-LION-v4-27B-IT":[0,0,null,null],"publicai/BSC-LT/salamandra-7b-instruct-tools-16k":[0,0,null,null],"BSC-LT/salamandra-7b-instruct-tools-16k":[0,0,null,null],"publicai/BSC-LT/ALIA-40b-instruct_Q8_0":[0,0,null,null],"BSC-LT/ALIA-40b-instruct_Q8_0":[0,0,null,null],"publicai/allenai/Olmo-3-7B-Instruct":[0,0,null,null],"allenai/Olmo-3-7B-Instruct":[0,0,null,null],"perplexity/pplx-embed-v1-0.6b":[4e-9,0,null,null],"pplx-embed-v1-0.6b":[4e-9,0,null,null],"perplexity/pplx-embed-v1-4b":[3e-8,0,null,null],"pplx-embed-v1-4b":[3e-8,0,null,null],"publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT":[0,0,null,null],"aisingapore/Qwen-SEA-LION-v4-32B-IT":[0,0,null,null],"publicai/allenai/Olmo-3-7B-Think":[0,0,null,null],"allenai/Olmo-3-7B-Think":[0,0,null,null],"publicai/allenai/Olmo-3-32B-Think":[0,0,null,null],"allenai/Olmo-3-32B-Think":[0,0,null,null],"replicate/meta/llama-2-13b":[1e-7,5e-7,null,null],"meta/llama-2-13b":[1e-7,5e-7,null,null],"replicate/meta/llama-2-13b-chat":[1e-7,5e-7,null,null],"meta/llama-2-13b-chat":[1e-7,5e-7,null,null],"replicate/meta/llama-2-70b":[6.5e-7,0.00000275,null,null],"meta/llama-2-70b":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-2-70b-chat":[6.5e-7,0.00000275,null,null],"meta/llama-2-70b-chat":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-2-7b":[5e-8,2.5e-7,null,null],"meta/llama-2-7b":[5e-8,2.5e-7,null,null],"replicate/meta/llama-2-7b-chat":[5e-8,2.5e-7,null,null],"meta/llama-2-7b-chat":[5e-8,2.5e-7,null,null],"replicate/meta/llama-3-70b":[6.5e-7,0.00000275,null,null],"meta/llama-3-70b":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-3-70b-instruct":[6.5e-7,0.00000275,null,null],"meta/llama-3-70b-instruct":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-3-8b":[5e-8,2.5e-7,null,null],"meta/llama-3-8b":[5e-8,2.5e-7,null,null],"replicate/meta/llama-3-8b-instruct":[5e-8,2.5e-7,null,null],"meta/llama-3-8b-instruct":[5e-8,2.5e-7,null,null],"replicate/mistralai/mistral-7b-instruct-v0.2":[5e-8,2.5e-7,null,null],"mistralai/mistral-7b-instruct-v0.2":[5e-8,2.5e-7,null,null],"replicate/mistralai/mistral-7b-v0.1":[5e-8,2.5e-7,null,null],"mistralai/mistral-7b-v0.1":[5e-8,2.5e-7,null,null],"replicate/mistralai/mixtral-8x7b-instruct-v0.1":[3e-7,0.000001,null,null],"mistralai/mixtral-8x7b-instruct-v0.1":[3e-7,0.000001,null,null],"replicate/openai/gpt-5":[0.00000125,0.00001,null,null],"replicateopenai/gpt-oss-20b":[9e-8,3.6e-7,null,null],"replicate/anthropic/claude-4.5-haiku":[0.000001,0.000005,null,null],"anthropic/claude-4.5-haiku":[0.000001,0.000005,null,null],"replicate/ibm-granite/granite-3.3-8b-instruct":[3e-8,2.5e-7,null,null],"ibm-granite/granite-3.3-8b-instruct":[3e-8,2.5e-7,null,null],"replicate/openai/gpt-4o":[0.0000025,0.00001,null,null],"replicate/openai/o4-mini":[0.000001,0.000004,null,null],"openai/o4-mini":[0.000001,0.000004,null,null],"replicate/openai/o1-mini":[0.0000011,0.0000044,null,null],"openai/o1-mini":[0.0000011,0.0000044,null,null],"replicate/openai/o1":[0.000015,0.00006,null,null],"replicate/openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"replicate/qwen/qwen3-235b-a22b-instruct-2507":[2.64e-7,0.00000106,null,null],"qwen/qwen3-235b-a22b-instruct-2507":[2.64e-7,0.00000106,null,null],"replicate/anthropic/claude-4-sonnet":[0.000003,0.000015,null,null],"replicate/deepseek-ai/deepseek-v3":[0.00000145,0.00000145,null,null],"deepseek-ai/deepseek-v3":[0.00000145,0.00000145,null,null],"replicate/anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"replicate/anthropic/claude-3.5-haiku":[0.000001,0.000005,null,null],"anthropic/claude-3.5-haiku":[0.000001,0.000005,null,null],"replicate/anthropic/claude-3.5-sonnet":[0.00000375,0.00001875,null,null],"replicate/google/gemini-3-pro":[0.000002,0.000012,null,null],"google/gemini-3-pro":[0.000002,0.000012,null,null],"replicate/anthropic/claude-4.5-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-4.5-sonnet":[0.000003,0.000015,null,null],"replicate/openai/gpt-4.1":[0.000002,0.000008,null,null],"replicate/openai/gpt-4.1-nano":[1e-7,4e-7,null,null],"replicate/openai/gpt-4.1-mini":[4e-7,0.0000016,null,null],"replicate/openai/gpt-5-nano":[5e-8,4e-7,null,null],"replicate/openai/gpt-5-mini":[2.5e-7,0.000002,null,null],"replicate/google/gemini-2.5-flash":[0.0000025,0.0000025,null,null],"replicate/openai/gpt-oss-120b":[1.8e-7,7.2e-7,null,null],"replicate/deepseek-ai/deepseek-v3.1":[6.72e-7,0.000002016,null,null],"deepseek-ai/deepseek-v3.1":[6.72e-7,0.000002016,null,null],"replicate/xai/grok-4":[0.0000072,0.000036,null,null],"xai/grok-4":[0.0000072,0.000036,null,null],"replicate/deepseek-ai/deepseek-r1":[0.00000375,0.00001,null,null],"deepseek-ai/deepseek-r1":[0.00000375,0.00001,null,null],"nvidia_nim/nvidia/nv-rerankqa-mistral-4b-v3":[0,0,null,null],"nvidia/nv-rerankqa-mistral-4b-v3":[0,0,null,null],"nvidia_nim/nvidia/llama-3_2-nv-rerankqa-1b-v2":[0,0,null,null],"nvidia/llama-3_2-nv-rerankqa-1b-v2":[0,0,null,null],"nvidia_nim/ranking/nvidia/llama-3.2-nv-rerankqa-1b-v2":[0,0,null,null],"ranking/nvidia/llama-3.2-nv-rerankqa-1b-v2":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-13b":[0,0,null,null],"meta-textgeneration-llama-2-13b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-13b-f":[0,0,null,null],"meta-textgeneration-llama-2-13b-f":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-70b":[0,0,null,null],"meta-textgeneration-llama-2-70b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-70b-b-f":[0,0,null,null],"meta-textgeneration-llama-2-70b-b-f":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-7b":[0,0,null,null],"meta-textgeneration-llama-2-7b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-7b-f":[0,0,null,null],"meta-textgeneration-llama-2-7b-f":[0,0,null,null],"sambanova/DeepSeek-R1":[0.000005,0.000007,null,null],"DeepSeek-R1":[0.000005,0.000007,null,null],"sambanova/DeepSeek-R1-Distill-Llama-70B":[7e-7,0.0000014,null,null],"sambanova/DeepSeek-V3-0324":[0.000003,0.0000045,null,null],"DeepSeek-V3-0324":[0.000003,0.0000045,null,null],"sambanova/Llama-4-Maverick-17B-128E-Instruct":[6.3e-7,0.0000018,null,null],"Llama-4-Maverick-17B-128E-Instruct":[6.3e-7,0.0000018,null,null],"sambanova/Llama-4-Scout-17B-16E-Instruct":[4e-7,7e-7,null,null],"sambanova/Meta-Llama-3.1-405B-Instruct":[0.000005,0.00001,null,null],"sambanova/Meta-Llama-3.1-8B-Instruct":[1e-7,2e-7,null,null],"sambanova/Meta-Llama-3.2-1B-Instruct":[4e-8,8e-8,null,null],"Meta-Llama-3.2-1B-Instruct":[4e-8,8e-8,null,null],"sambanova/Meta-Llama-3.2-3B-Instruct":[8e-8,1.6e-7,null,null],"Meta-Llama-3.2-3B-Instruct":[8e-8,1.6e-7,null,null],"sambanova/Meta-Llama-3.3-70B-Instruct":[6e-7,0.0000012,null,null],"Meta-Llama-3.3-70B-Instruct":[6e-7,0.0000012,null,null],"sambanova/Meta-Llama-Guard-3-8B":[3e-7,3e-7,null,null],"Meta-Llama-Guard-3-8B":[3e-7,3e-7,null,null],"sambanova/QwQ-32B":[5e-7,0.000001,null,null],"QwQ-32B":[5e-7,0.000001,null,null],"sambanova/Qwen2-Audio-7B-Instruct":[5e-7,0.0001,null,null],"Qwen2-Audio-7B-Instruct":[5e-7,0.0001,null,null],"sambanova/Qwen3-32B":[4e-7,8e-7,null,null],"sambanova/DeepSeek-V3.1":[0.000003,0.0000045,null,null],"DeepSeek-V3.1":[0.000003,0.0000045,null,null],"sambanova/gpt-oss-120b":[0.000003,0.0000045,null,null],"text-completion-codestral/codestral-2405":[0,0,null,null],"text-completion-codestral/codestral-latest":[0,0,null,null],"together_ai/baai/bge-base-en-v1.5":[8e-9,0,null,null],"baai/bge-base-en-v1.5":[8e-9,0,null,null],"together_ai/BAAI/bge-base-en-v1.5":[8e-9,0,null,null],"BAAI/bge-base-en-v1.5":[8e-9,0,null,null],"together_ai/Qwen/Qwen3-235B-A22B-Instruct-2507-tput":[2e-7,0.000006,null,null],"Qwen/Qwen3-235B-A22B-Instruct-2507-tput":[2e-7,0.000006,null,null],"together_ai/Qwen/Qwen3-235B-A22B-Thinking-2507":[6.5e-7,0.000003,null,null],"together_ai/Qwen/Qwen3-235B-A22B-fp8-tput":[2e-7,6e-7,null,null],"Qwen/Qwen3-235B-A22B-fp8-tput":[2e-7,6e-7,null,null],"together_ai/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":[0.000002,0.000002,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":[0.000002,0.000002,null,null],"together_ai/deepseek-ai/DeepSeek-R1":[0.000003,0.000007,null,null],"together_ai/deepseek-ai/DeepSeek-R1-0528-tput":[5.5e-7,0.00000219,null,null],"deepseek-ai/DeepSeek-R1-0528-tput":[5.5e-7,0.00000219,null,null],"together_ai/deepseek-ai/DeepSeek-V3":[0.00000125,0.00000125,null,null],"together_ai/deepseek-ai/DeepSeek-V3.1":[6e-7,0.0000017,null,null],"together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo":[8.8e-7,8.8e-7,null,null],"together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo-Free":[0,0,null,null],"meta-llama/Llama-3.3-70B-Instruct-Turbo-Free":[0,0,null,null],"together_ai/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[2.7e-7,8.5e-7,null,null],"together_ai/meta-llama/Llama-4-Scout-17B-16E-Instruct":[1.8e-7,5.9e-7,null,null],"together_ai/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo":[0.0000035,0.0000035,null,null],"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo":[0.0000035,0.0000035,null,null],"together_ai/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[8.8e-7,8.8e-7,null,null],"together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[1.8e-7,1.8e-7,null,null],"together_ai/mistralai/Mixtral-8x7B-Instruct-v0.1":[6e-7,6e-7,null,null],"together_ai/moonshotai/Kimi-K2-Instruct":[0.000001,0.000003,null,null],"together_ai/openai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"together_ai/openai/gpt-oss-20b":[5e-8,2e-7,null,null],"together_ai/zai-org/GLM-4.5-Air-FP8":[2e-7,0.0000011,null,null],"zai-org/GLM-4.5-Air-FP8":[2e-7,0.0000011,null,null],"together_ai/zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"together_ai/zai-org/GLM-4.7":[4.5e-7,0.000002,null,null],"together_ai/moonshotai/Kimi-K2.5":[5e-7,0.0000028,null,null],"together_ai/moonshotai/Kimi-K2-Instruct-0905":[0.000001,0.000003,null,null],"together_ai/Qwen/Qwen3-Next-80B-A3B-Instruct":[1.5e-7,0.0000015,null,null],"together_ai/Qwen/Qwen3-Next-80B-A3B-Thinking":[1.5e-7,0.0000015,null,null],"together_ai/Qwen/Qwen3.5-397B-A17B":[6e-7,0.0000036,null,null],"Qwen/Qwen3.5-397B-A17B":[6e-7,0.0000036,null,null],"v0/v0-1.0-md":[0.000003,0.000015,null,null],"v0-1.0-md":[0.000003,0.000015,null,null],"v0/v0-1.5-lg":[0.000015,0.000075,null,null],"v0-1.5-lg":[0.000015,0.000075,null,null],"v0/v0-1.5-md":[0.000003,0.000015,null,null],"v0-1.5-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/alibaba/qwen-3-14b":[8e-8,2.4e-7,null,null],"alibaba/qwen-3-14b":[8e-8,2.4e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-235b":[2e-7,6e-7,null,null],"alibaba/qwen-3-235b":[2e-7,6e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-30b":[1e-7,3e-7,null,null],"alibaba/qwen-3-30b":[1e-7,3e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-32b":[1e-7,3e-7,null,null],"alibaba/qwen-3-32b":[1e-7,3e-7,null,null],"vercel_ai_gateway/alibaba/qwen3-coder":[4e-7,0.0000016,null,null],"alibaba/qwen3-coder":[4e-7,0.0000016,null,null],"vercel_ai_gateway/amazon/nova-lite":[6e-8,2.4e-7,null,null],"amazon/nova-lite":[6e-8,2.4e-7,null,null],"vercel_ai_gateway/amazon/nova-micro":[3.5e-8,1.4e-7,null,null],"amazon/nova-micro":[3.5e-8,1.4e-7,null,null],"vercel_ai_gateway/amazon/nova-pro":[8e-7,0.0000032,null,null],"amazon/nova-pro":[8e-7,0.0000032,null,null],"vercel_ai_gateway/amazon/titan-embed-text-v2":[2e-8,0,null,null],"amazon/titan-embed-text-v2":[2e-8,0,null,null],"vercel_ai_gateway/anthropic/claude-3-haiku":[2.5e-7,0.00000125,3e-7,3e-8],"vercel_ai_gateway/anthropic/claude-3-opus":[0.000015,0.000075,0.00001875,0.0000015],"anthropic/claude-3-opus":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-3.5-haiku":[8e-7,0.000004,0.000001,8e-8],"vercel_ai_gateway/anthropic/claude-3.5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3.7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-4-opus":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-4-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-5-sonnet-20241022":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-5-sonnet-20241022":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"vercel_ai_gateway/anthropic/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-opus-4.5":[0.000005,0.000025,0.00000625,5e-7],"vercel_ai_gateway/anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"vercel_ai_gateway/anthropic/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-sonnet-4.5":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/cohere/command-a":[0.0000025,0.00001,null,null],"cohere/command-a":[0.0000025,0.00001,null,null],"vercel_ai_gateway/cohere/command-r":[1.5e-7,6e-7,null,null],"cohere/command-r":[1.5e-7,6e-7,null,null],"vercel_ai_gateway/cohere/command-r-plus":[0.0000025,0.00001,null,null],"cohere/command-r-plus":[0.0000025,0.00001,null,null],"vercel_ai_gateway/cohere/embed-v4.0":[1.2e-7,0,null,null],"vercel_ai_gateway/deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"vercel_ai_gateway/deepseek/deepseek-r1-distill-llama-70b":[7.5e-7,9.9e-7,null,null],"deepseek/deepseek-r1-distill-llama-70b":[7.5e-7,9.9e-7,null,null],"vercel_ai_gateway/deepseek/deepseek-v3":[9e-7,9e-7,null,null],"vercel_ai_gateway/google/gemini-2.0-flash":[1.5e-7,6e-7,null,null],"google/gemini-2.0-flash":[1.5e-7,6e-7,null,null],"vercel_ai_gateway/google/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,null],"google/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,null],"vercel_ai_gateway/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"vercel_ai_gateway/google/gemini-2.5-pro":[0.0000025,0.00001,null,null],"vercel_ai_gateway/google/gemini-embedding-001":[1.5e-7,0,null,null],"google/gemini-embedding-001":[1.5e-7,0,null,null],"vercel_ai_gateway/google/gemma-2-9b":[2e-7,2e-7,null,null],"google/gemma-2-9b":[2e-7,2e-7,null,null],"vercel_ai_gateway/google/text-embedding-005":[2.5e-8,0,null,null],"google/text-embedding-005":[2.5e-8,0,null,null],"vercel_ai_gateway/google/text-multilingual-embedding-002":[2.5e-8,0,null,null],"google/text-multilingual-embedding-002":[2.5e-8,0,null,null],"vercel_ai_gateway/inception/mercury-coder-small":[2.5e-7,0.000001,null,null],"inception/mercury-coder-small":[2.5e-7,0.000001,null,null],"vercel_ai_gateway/meta/llama-3-70b":[5.9e-7,7.9e-7,null,null],"vercel_ai_gateway/meta/llama-3-8b":[5e-8,8e-8,null,null],"vercel_ai_gateway/meta/llama-3.1-70b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.1-70b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-3.1-8b":[5e-8,8e-8,null,null],"meta/llama-3.1-8b":[5e-8,8e-8,null,null],"vercel_ai_gateway/meta/llama-3.2-11b":[1.6e-7,1.6e-7,null,null],"meta/llama-3.2-11b":[1.6e-7,1.6e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-1b":[1e-7,1e-7,null,null],"meta/llama-3.2-1b":[1e-7,1e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-3b":[1.5e-7,1.5e-7,null,null],"meta/llama-3.2-3b":[1.5e-7,1.5e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-90b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.2-90b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-3.3-70b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.3-70b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-4-maverick":[2e-7,6e-7,null,null],"meta/llama-4-maverick":[2e-7,6e-7,null,null],"vercel_ai_gateway/meta/llama-4-scout":[1e-7,3e-7,null,null],"meta/llama-4-scout":[1e-7,3e-7,null,null],"vercel_ai_gateway/mistral/codestral":[3e-7,9e-7,null,null],"mistral/codestral":[3e-7,9e-7,null,null],"vercel_ai_gateway/mistral/codestral-embed":[1.5e-7,0,null,null],"mistral/codestral-embed":[1.5e-7,0,null,null],"vercel_ai_gateway/mistral/devstral-small":[7e-8,2.8e-7,null,null],"mistral/devstral-small":[7e-8,2.8e-7,null,null],"vercel_ai_gateway/mistral/magistral-medium":[0.000002,0.000005,null,null],"mistral/magistral-medium":[0.000002,0.000005,null,null],"vercel_ai_gateway/mistral/magistral-small":[5e-7,0.0000015,null,null],"mistral/magistral-small":[5e-7,0.0000015,null,null],"vercel_ai_gateway/mistral/ministral-3b":[4e-8,4e-8,null,null],"mistral/ministral-3b":[4e-8,4e-8,null,null],"vercel_ai_gateway/mistral/ministral-8b":[1e-7,1e-7,null,null],"mistral/ministral-8b":[1e-7,1e-7,null,null],"vercel_ai_gateway/mistral/mistral-embed":[1e-7,0,null,null],"mistral/mistral-embed":[1e-7,0,null,null],"vercel_ai_gateway/mistral/mistral-large":[0.000002,0.000006,null,null],"mistral/mistral-large":[0.000002,0.000006,null,null],"vercel_ai_gateway/mistral/mistral-saba-24b":[7.9e-7,7.9e-7,null,null],"mistral/mistral-saba-24b":[7.9e-7,7.9e-7,null,null],"vercel_ai_gateway/mistral/mistral-small":[1e-7,3e-7,null,null],"vercel_ai_gateway/mistral/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"mistral/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"vercel_ai_gateway/mistral/pixtral-12b":[1.5e-7,1.5e-7,null,null],"mistral/pixtral-12b":[1.5e-7,1.5e-7,null,null],"vercel_ai_gateway/mistral/pixtral-large":[0.000002,0.000006,null,null],"mistral/pixtral-large":[0.000002,0.000006,null,null],"vercel_ai_gateway/moonshotai/kimi-k2":[5.5e-7,0.0000022,null,null],"moonshotai/kimi-k2":[5.5e-7,0.0000022,null,null],"vercel_ai_gateway/morph/morph-v3-fast":[8e-7,0.0000012,null,null],"vercel_ai_gateway/morph/morph-v3-large":[9e-7,0.0000019,null,null],"vercel_ai_gateway/openai/gpt-3.5-turbo":[5e-7,0.0000015,null,null],"vercel_ai_gateway/openai/gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"openai/gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"vercel_ai_gateway/openai/gpt-4-turbo":[0.00001,0.00003,null,null],"openai/gpt-4-turbo":[0.00001,0.00003,null,null],"vercel_ai_gateway/openai/gpt-4.1":[0.000002,0.000008,0,5e-7],"vercel_ai_gateway/openai/gpt-4.1-mini":[4e-7,0.0000016,0,1e-7],"vercel_ai_gateway/openai/gpt-4.1-nano":[1e-7,4e-7,0,2.5e-8],"vercel_ai_gateway/openai/gpt-4o":[0.0000025,0.00001,0,0.00000125],"vercel_ai_gateway/openai/gpt-4o-mini":[1.5e-7,6e-7,0,7.5e-8],"vercel_ai_gateway/openai/o1":[0.000015,0.00006,0,0.0000075],"vercel_ai_gateway/openai/o3":[0.000002,0.000008,0,5e-7],"openai/o3":[0.000002,0.000008,0,5e-7],"vercel_ai_gateway/openai/o3-mini":[0.0000011,0.0000044,0,5.5e-7],"vercel_ai_gateway/openai/o4-mini":[0.0000011,0.0000044,0,2.75e-7],"vercel_ai_gateway/openai/text-embedding-3-large":[1.3e-7,0,null,null],"openai/text-embedding-3-large":[1.3e-7,0,null,null],"vercel_ai_gateway/openai/text-embedding-3-small":[2e-8,0,null,null],"openai/text-embedding-3-small":[2e-8,0,null,null],"vercel_ai_gateway/openai/text-embedding-ada-002":[1e-7,0,null,null],"openai/text-embedding-ada-002":[1e-7,0,null,null],"vercel_ai_gateway/perplexity/sonar":[0.000001,0.000001,null,null],"vercel_ai_gateway/perplexity/sonar-pro":[0.000003,0.000015,null,null],"vercel_ai_gateway/perplexity/sonar-reasoning":[0.000001,0.000005,null,null],"vercel_ai_gateway/perplexity/sonar-reasoning-pro":[0.000002,0.000008,null,null],"vercel_ai_gateway/vercel/v0-1.0-md":[0.000003,0.000015,null,null],"vercel/v0-1.0-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/vercel/v0-1.5-md":[0.000003,0.000015,null,null],"vercel/v0-1.5-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/xai/grok-2":[0.000002,0.00001,null,null],"xai/grok-2":[0.000002,0.00001,null,null],"vercel_ai_gateway/xai/grok-2-vision":[0.000002,0.00001,null,null],"xai/grok-2-vision":[0.000002,0.00001,null,null],"vercel_ai_gateway/xai/grok-3":[0.000003,0.000015,null,null],"xai/grok-3":[0.000003,0.000015,null,null],"vercel_ai_gateway/xai/grok-3-fast":[0.000005,0.000025,null,null],"xai/grok-3-fast":[0.000005,0.000025,null,null],"vercel_ai_gateway/xai/grok-3-mini":[3e-7,5e-7,null,null],"xai/grok-3-mini":[3e-7,5e-7,null,null],"vercel_ai_gateway/xai/grok-3-mini-fast":[6e-7,0.000004,null,null],"xai/grok-3-mini-fast":[6e-7,0.000004,null,null],"vercel_ai_gateway/xai/grok-4":[0.000003,0.000015,null,null],"vercel_ai_gateway/zai/glm-4.5":[6e-7,0.0000022,null,null],"zai/glm-4.5":[6e-7,0.0000022,null,null],"vercel_ai_gateway/zai/glm-4.5-air":[2e-7,0.0000011,null,null],"zai/glm-4.5-air":[2e-7,0.0000011,null,null],"vercel_ai_gateway/zai/glm-4.6":[4.5e-7,0.0000018,null,1.1e-7],"zai/glm-4.6":[4.5e-7,0.0000018,null,1.1e-7],"vertex_ai/claude-3-5-haiku":[0.000001,0.000005,null,null],"claude-3-5-haiku":[0.000001,0.000005,null,null],"vertex_ai/claude-3-5-haiku@20241022":[0.000001,0.000005,null,null],"claude-3-5-haiku@20241022":[0.000001,0.000005,null,null],"vertex_ai/claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"vertex_ai/claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"vertex_ai/claude-3-5-sonnet":[0.000003,0.000015,null,null],"claude-3-5-sonnet":[0.000003,0.000015,null,null],"vertex_ai/claude-3-5-sonnet@20240620":[0.000003,0.000015,null,null],"claude-3-5-sonnet@20240620":[0.000003,0.000015,null,null],"vertex_ai/claude-3-7-sonnet@20250219":[0.000003,0.000015,0.00000375,3e-7],"claude-3-7-sonnet@20250219":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-3-haiku":[2.5e-7,0.00000125,null,null],"claude-3-haiku":[2.5e-7,0.00000125,null,null],"vertex_ai/claude-3-haiku@20240307":[2.5e-7,0.00000125,null,null],"claude-3-haiku@20240307":[2.5e-7,0.00000125,null,null],"vertex_ai/claude-3-opus":[0.000015,0.000075,null,null],"claude-3-opus":[0.000015,0.000075,null,null],"vertex_ai/claude-3-opus@20240229":[0.000015,0.000075,null,null],"claude-3-opus@20240229":[0.000015,0.000075,null,null],"vertex_ai/claude-3-sonnet":[0.000003,0.000015,null,null],"claude-3-sonnet":[0.000003,0.000015,null,null],"vertex_ai/claude-3-sonnet@20240229":[0.000003,0.000015,null,null],"claude-3-sonnet@20240229":[0.000003,0.000015,null,null],"vertex_ai/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-1@20250805":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-1@20250805":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-5@20251101":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-5@20251101":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-6@default":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6@default":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-7@default":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7@default":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4-5@20250929":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5@20250929":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-opus-4@20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4@20250514":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4@20250514":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4@20250514":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/mistralai/codestral-2@001":[3e-7,9e-7,null,null],"mistralai/codestral-2@001":[3e-7,9e-7,null,null],"vertex_ai/codestral-2":[3e-7,9e-7,null,null],"codestral-2":[3e-7,9e-7,null,null],"vertex_ai/codestral-2@001":[3e-7,9e-7,null,null],"codestral-2@001":[3e-7,9e-7,null,null],"vertex_ai/mistralai/codestral-2":[3e-7,9e-7,null,null],"mistralai/codestral-2":[3e-7,9e-7,null,null],"vertex_ai/codestral-2501":[2e-7,6e-7,null,null],"codestral-2501":[2e-7,6e-7,null,null],"vertex_ai/codestral@2405":[2e-7,6e-7,null,null],"codestral@2405":[2e-7,6e-7,null,null],"vertex_ai/codestral@latest":[2e-7,6e-7,null,null],"codestral@latest":[2e-7,6e-7,null,null],"vertex_ai/deepseek-ai/deepseek-v3.1-maas":[0.00000135,0.0000054,null,null],"deepseek-ai/deepseek-v3.1-maas":[0.00000135,0.0000054,null,null],"vertex_ai/deepseek-ai/deepseek-v3.2-maas":[5.6e-7,0.00000168,null,null],"deepseek-ai/deepseek-v3.2-maas":[5.6e-7,0.00000168,null,null],"vertex_ai/deepseek-ai/deepseek-r1-0528-maas":[0.00000135,0.0000054,null,null],"deepseek-ai/deepseek-r1-0528-maas":[0.00000135,0.0000054,null,null],"vertex_ai/gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"vertex_ai/gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"vertex_ai/gemini-3.1-flash-image-preview":[5e-7,0.000003,null,null],"vertex_ai/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"vertex_ai/deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"vertex_ai/jamba-1.5":[2e-7,4e-7,null,null],"vertex_ai/jamba-1.5-large":[0.000002,0.000008,null,null],"vertex_ai/jamba-1.5-large@001":[0.000002,0.000008,null,null],"vertex_ai/jamba-1.5-mini":[2e-7,4e-7,null,null],"vertex_ai/jamba-1.5-mini@001":[2e-7,4e-7,null,null],"vertex_ai/meta/llama-3.1-405b-instruct-maas":[0.000005,0.000016,null,null],"meta/llama-3.1-405b-instruct-maas":[0.000005,0.000016,null,null],"vertex_ai/meta/llama-3.1-70b-instruct-maas":[0,0,null,null],"meta/llama-3.1-70b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-3.1-8b-instruct-maas":[0,0,null,null],"meta/llama-3.1-8b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-3.2-90b-vision-instruct-maas":[0,0,null,null],"meta/llama-3.2-90b-vision-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas":[3.5e-7,0.00000115,null,null],"meta/llama-4-maverick-17b-128e-instruct-maas":[3.5e-7,0.00000115,null,null],"vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas":[3.5e-7,0.00000115,null,null],"meta/llama-4-maverick-17b-16e-instruct-maas":[3.5e-7,0.00000115,null,null],"vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas":[2.5e-7,7e-7,null,null],"meta/llama-4-scout-17b-128e-instruct-maas":[2.5e-7,7e-7,null,null],"vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas":[2.5e-7,7e-7,null,null],"meta/llama-4-scout-17b-16e-instruct-maas":[2.5e-7,7e-7,null,null],"vertex_ai/meta/llama3-405b-instruct-maas":[0,0,null,null],"meta/llama3-405b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama3-70b-instruct-maas":[0,0,null,null],"meta/llama3-70b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama3-8b-instruct-maas":[0,0,null,null],"meta/llama3-8b-instruct-maas":[0,0,null,null],"vertex_ai/minimaxai/minimax-m2-maas":[3e-7,0.0000012,null,null],"minimaxai/minimax-m2-maas":[3e-7,0.0000012,null,null],"vertex_ai/moonshotai/kimi-k2-thinking-maas":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-thinking-maas":[6e-7,0.0000025,null,null],"vertex_ai/zai-org/glm-4.7-maas":[6e-7,0.0000022,null,null],"zai-org/glm-4.7-maas":[6e-7,0.0000022,null,null],"vertex_ai/zai-org/glm-5-maas":[0.000001,0.0000032,null,1e-7],"zai-org/glm-5-maas":[0.000001,0.0000032,null,1e-7],"vertex_ai/mistral-medium-3":[4e-7,0.000002,null,null],"mistral-medium-3":[4e-7,0.000002,null,null],"vertex_ai/mistral-medium-3@001":[4e-7,0.000002,null,null],"mistral-medium-3@001":[4e-7,0.000002,null,null],"vertex_ai/mistralai/mistral-medium-3":[4e-7,0.000002,null,null],"mistralai/mistral-medium-3":[4e-7,0.000002,null,null],"vertex_ai/mistralai/mistral-medium-3@001":[4e-7,0.000002,null,null],"mistralai/mistral-medium-3@001":[4e-7,0.000002,null,null],"vertex_ai/mistral-large-2411":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@2407":[0.000002,0.000006,null,null],"mistral-large@2407":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@2411-001":[0.000002,0.000006,null,null],"mistral-large@2411-001":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@latest":[0.000002,0.000006,null,null],"mistral-large@latest":[0.000002,0.000006,null,null],"vertex_ai/mistral-nemo@2407":[0.000003,0.000003,null,null],"mistral-nemo@2407":[0.000003,0.000003,null,null],"vertex_ai/mistral-nemo@latest":[1.5e-7,1.5e-7,null,null],"mistral-nemo@latest":[1.5e-7,1.5e-7,null,null],"vertex_ai/mistral-small-2503":[0.000001,0.000003,null,null],"vertex_ai/mistral-small-2503@001":[0.000001,0.000003,null,null],"mistral-small-2503@001":[0.000001,0.000003,null,null],"vertex_ai/deepseek-ai/deepseek-ocr-maas":[3e-7,0.0000012,null,null],"deepseek-ai/deepseek-ocr-maas":[3e-7,0.0000012,null,null],"vertex_ai/openai/gpt-oss-120b-maas":[1.5e-7,6e-7,null,null],"openai/gpt-oss-120b-maas":[1.5e-7,6e-7,null,null],"vertex_ai/openai/gpt-oss-20b-maas":[7.5e-8,3e-7,null,null],"openai/gpt-oss-20b-maas":[7.5e-8,3e-7,null,null],"vertex_ai/qwen/qwen3-235b-a22b-instruct-2507-maas":[2.5e-7,0.000001,null,null],"qwen/qwen3-235b-a22b-instruct-2507-maas":[2.5e-7,0.000001,null,null],"vertex_ai/qwen/qwen3-coder-480b-a35b-instruct-maas":[0.000001,0.000004,null,null],"qwen/qwen3-coder-480b-a35b-instruct-maas":[0.000001,0.000004,null,null],"vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas":[1.5e-7,0.0000012,null,null],"qwen/qwen3-next-80b-a3b-instruct-maas":[1.5e-7,0.0000012,null,null],"vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas":[1.5e-7,0.0000012,null,null],"qwen/qwen3-next-80b-a3b-thinking-maas":[1.5e-7,0.0000012,null,null],"voyage/rerank-2":[5e-8,0,null,null],"rerank-2":[5e-8,0,null,null],"voyage/rerank-2-lite":[2e-8,0,null,null],"rerank-2-lite":[2e-8,0,null,null],"voyage/rerank-2.5":[5e-8,0,null,null],"rerank-2.5":[5e-8,0,null,null],"voyage/rerank-2.5-lite":[2e-8,0,null,null],"rerank-2.5-lite":[2e-8,0,null,null],"voyage/voyage-2":[1e-7,0,null,null],"voyage-2":[1e-7,0,null,null],"voyage/voyage-3":[6e-8,0,null,null],"voyage-3":[6e-8,0,null,null],"voyage/voyage-3-large":[1.8e-7,0,null,null],"voyage-3-large":[1.8e-7,0,null,null],"voyage/voyage-3-lite":[2e-8,0,null,null],"voyage-3-lite":[2e-8,0,null,null],"voyage/voyage-3.5":[6e-8,0,null,null],"voyage-3.5":[6e-8,0,null,null],"voyage/voyage-3.5-lite":[2e-8,0,null,null],"voyage-3.5-lite":[2e-8,0,null,null],"voyage/voyage-code-2":[1.2e-7,0,null,null],"voyage-code-2":[1.2e-7,0,null,null],"voyage/voyage-code-3":[1.8e-7,0,null,null],"voyage-code-3":[1.8e-7,0,null,null],"voyage/voyage-context-3":[1.8e-7,0,null,null],"voyage-context-3":[1.8e-7,0,null,null],"voyage/voyage-finance-2":[1.2e-7,0,null,null],"voyage-finance-2":[1.2e-7,0,null,null],"voyage/voyage-large-2":[1.2e-7,0,null,null],"voyage-large-2":[1.2e-7,0,null,null],"voyage/voyage-law-2":[1.2e-7,0,null,null],"voyage-law-2":[1.2e-7,0,null,null],"voyage/voyage-lite-01":[1e-7,0,null,null],"voyage-lite-01":[1e-7,0,null,null],"voyage/voyage-lite-02-instruct":[1e-7,0,null,null],"voyage-lite-02-instruct":[1e-7,0,null,null],"voyage/voyage-multimodal-3":[1.2e-7,0,null,null],"voyage-multimodal-3":[1.2e-7,0,null,null],"wandb/openai/gpt-oss-120b":[0.015,0.06,null,null],"wandb/openai/gpt-oss-20b":[0.005,0.02,null,null],"wandb/zai-org/GLM-4.5":[0.055,0.2,null,null],"wandb/Qwen/Qwen3-235B-A22B-Instruct-2507":[0.01,0.01,null,null],"wandb/Qwen/Qwen3-Coder-480B-A35B-Instruct":[0.1,0.15,null,null],"wandb/Qwen/Qwen3-235B-A22B-Thinking-2507":[0.01,0.01,null,null],"wandb/moonshotai/Kimi-K2-Instruct":[6e-7,0.0000025,null,null],"wandb/moonshotai/Kimi-K2.5":[6e-7,0.000003,null,1e-7],"wandb/MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"wandb/meta-llama/Llama-3.1-8B-Instruct":[0.022,0.022,null,null],"wandb/deepseek-ai/DeepSeek-V3.1":[0.055,0.165,null,null],"wandb/deepseek-ai/DeepSeek-R1-0528":[0.135,0.54,null,null],"wandb/deepseek-ai/DeepSeek-V3-0324":[0.114,0.275,null,null],"wandb/meta-llama/Llama-3.3-70B-Instruct":[0.071,0.071,null,null],"wandb/meta-llama/Llama-4-Scout-17B-16E-Instruct":[0.017,0.066,null,null],"wandb/microsoft/Phi-4-mini-instruct":[0.008,0.035,null,null],"microsoft/Phi-4-mini-instruct":[0.008,0.035,null,null],"watsonx/ibm/granite-3-8b-instruct":[2e-7,2e-7,null,null],"ibm/granite-3-8b-instruct":[2e-7,2e-7,null,null],"watsonx/mistralai/mistral-large":[0.000003,0.00001,null,null],"watsonx/bigscience/mt0-xxl-13b":[0.0005,0.002,null,null],"bigscience/mt0-xxl-13b":[0.0005,0.002,null,null],"watsonx/core42/jais-13b-chat":[0.0005,0.002,null,null],"core42/jais-13b-chat":[0.0005,0.002,null,null],"watsonx/google/flan-t5-xl-3b":[6e-7,6e-7,null,null],"google/flan-t5-xl-3b":[6e-7,6e-7,null,null],"watsonx/ibm/granite-13b-chat-v2":[6e-7,6e-7,null,null],"ibm/granite-13b-chat-v2":[6e-7,6e-7,null,null],"watsonx/ibm/granite-13b-instruct-v2":[6e-7,6e-7,null,null],"ibm/granite-13b-instruct-v2":[6e-7,6e-7,null,null],"watsonx/ibm/granite-3-3-8b-instruct":[2e-7,2e-7,null,null],"ibm/granite-3-3-8b-instruct":[2e-7,2e-7,null,null],"watsonx/ibm/granite-4-h-small":[6e-8,2.5e-7,null,null],"ibm/granite-4-h-small":[6e-8,2.5e-7,null,null],"watsonx/ibm/granite-guardian-3-2-2b":[1e-7,1e-7,null,null],"ibm/granite-guardian-3-2-2b":[1e-7,1e-7,null,null],"watsonx/ibm/granite-guardian-3-3-8b":[2e-7,2e-7,null,null],"ibm/granite-guardian-3-3-8b":[2e-7,2e-7,null,null],"watsonx/ibm/granite-ttm-1024-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-1024-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-ttm-1536-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-1536-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-ttm-512-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-512-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-vision-3-2-2b":[1e-7,1e-7,null,null],"ibm/granite-vision-3-2-2b":[1e-7,1e-7,null,null],"watsonx/meta-llama/llama-3-2-11b-vision-instruct":[3.5e-7,3.5e-7,null,null],"meta-llama/llama-3-2-11b-vision-instruct":[3.5e-7,3.5e-7,null,null],"watsonx/meta-llama/llama-3-2-1b-instruct":[1e-7,1e-7,null,null],"meta-llama/llama-3-2-1b-instruct":[1e-7,1e-7,null,null],"watsonx/meta-llama/llama-3-2-3b-instruct":[1.5e-7,1.5e-7,null,null],"meta-llama/llama-3-2-3b-instruct":[1.5e-7,1.5e-7,null,null],"watsonx/meta-llama/llama-3-2-90b-vision-instruct":[0.000002,0.000002,null,null],"meta-llama/llama-3-2-90b-vision-instruct":[0.000002,0.000002,null,null],"watsonx/meta-llama/llama-3-3-70b-instruct":[7.1e-7,7.1e-7,null,null],"meta-llama/llama-3-3-70b-instruct":[7.1e-7,7.1e-7,null,null],"watsonx/meta-llama/llama-4-maverick-17b":[3.5e-7,0.0000014,null,null],"meta-llama/llama-4-maverick-17b":[3.5e-7,0.0000014,null,null],"watsonx/meta-llama/llama-guard-3-11b-vision":[3.5e-7,3.5e-7,null,null],"meta-llama/llama-guard-3-11b-vision":[3.5e-7,3.5e-7,null,null],"watsonx/mistralai/mistral-medium-2505":[0.000003,0.00001,null,null],"mistralai/mistral-medium-2505":[0.000003,0.00001,null,null],"watsonx/mistralai/mistral-small-2503":[1e-7,3e-7,null,null],"mistralai/mistral-small-2503":[1e-7,3e-7,null,null],"watsonx/mistralai/mistral-small-3-1-24b-instruct-2503":[1e-7,3e-7,null,null],"mistralai/mistral-small-3-1-24b-instruct-2503":[1e-7,3e-7,null,null],"watsonx/mistralai/pixtral-12b-2409":[3.5e-7,3.5e-7,null,null],"mistralai/pixtral-12b-2409":[3.5e-7,3.5e-7,null,null],"watsonx/openai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"watsonx/sdaia/allam-1-13b-instruct":[0.0000018,0.0000018,null,null],"sdaia/allam-1-13b-instruct":[0.0000018,0.0000018,null,null],"grok-2":[0.000002,0.00001,null,null],"xai/grok-2-1212":[0.000002,0.00001,null,null],"grok-2-1212":[0.000002,0.00001,null,null],"xai/grok-2-latest":[0.000002,0.00001,null,null],"grok-2-latest":[0.000002,0.00001,null,null],"grok-2-vision":[0.000002,0.00001,null,null],"xai/grok-2-vision-1212":[0.000002,0.00001,null,null],"grok-2-vision-1212":[0.000002,0.00001,null,null],"xai/grok-2-vision-latest":[0.000002,0.00001,null,null],"grok-2-vision-latest":[0.000002,0.00001,null,null],"xai/grok-3-beta":[0.000003,0.000015,null,7.5e-7],"grok-3-beta":[0.000003,0.000015,null,7.5e-7],"xai/grok-3-fast-beta":[0.000005,0.000025,null,0.00000125],"grok-3-fast-beta":[0.000005,0.000025,null,0.00000125],"xai/grok-3-fast-latest":[0.000005,0.000025,null,0.00000125],"grok-3-fast-latest":[0.000005,0.000025,null,0.00000125],"xai/grok-3-latest":[0.000003,0.000015,null,7.5e-7],"grok-3-latest":[0.000003,0.000015,null,7.5e-7],"xai/grok-3-mini-beta":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-beta":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-fast":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-fast-beta":[6e-7,0.000004,null,1.5e-7],"grok-3-mini-fast-beta":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-fast-latest":[6e-7,0.000004,null,1.5e-7],"grok-3-mini-fast-latest":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-latest":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-latest":[3e-7,5e-7,null,7.5e-8],"xai/grok-4-fast-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-0709":[0.000003,0.000015,null,null],"grok-4-0709":[0.000003,0.000015,null,null],"xai/grok-4-latest":[0.000003,0.000015,null,null],"grok-4-latest":[0.000003,0.000015,null,null],"xai/grok-4-1-fast":[2e-7,5e-7,null,5e-8],"grok-4-1-fast":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-reasoning-latest":[2e-7,5e-7,null,5e-8],"grok-4-1-fast-reasoning-latest":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-non-reasoning-latest":[2e-7,5e-7,null,5e-8],"grok-4-1-fast-non-reasoning-latest":[2e-7,5e-7,null,5e-8],"xai/grok-4.20-multi-agent-beta-0309":[0.000002,0.000006,null,2e-7],"grok-4.20-multi-agent-beta-0309":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-beta-0309-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-beta-0309-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-0309-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-0309-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-beta-0309-non-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-beta-0309-non-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-beta":[0.000005,0.000015,null,null],"grok-beta":[0.000005,0.000015,null,null],"xai/grok-code-fast":[2e-7,0.0000015,null,2e-8],"grok-code-fast":[2e-7,0.0000015,null,2e-8],"xai/grok-code-fast-1":[2e-7,0.0000015,null,2e-8],"xai/grok-code-fast-1-0825":[2e-7,0.0000015,null,2e-8],"grok-code-fast-1-0825":[2e-7,0.0000015,null,2e-8],"xai/grok-vision-beta":[0.000005,0.000015,null,null],"grok-vision-beta":[0.000005,0.000015,null,null],"zai/glm-5":[0.000001,0.0000032,0,2e-7],"glm-5":[0.000001,0.0000032,0,2e-7],"zai/glm-5-code":[0.0000012,0.000005,0,3e-7],"glm-5-code":[0.0000012,0.000005,0,3e-7],"zai/glm-4.7":[6e-7,0.0000022,0,1.1e-7],"glm-4.7":[6e-7,0.0000022,0,1.1e-7],"glm-4.6":[6e-7,0.0000022,0,1.1e-7],"glm-4.5":[6e-7,0.0000022,null,null],"zai/glm-4.5v":[6e-7,0.0000018,null,null],"glm-4.5v":[6e-7,0.0000018,null,null],"zai/glm-4.5-x":[0.0000022,0.0000089,null,null],"glm-4.5-x":[0.0000022,0.0000089,null,null],"glm-4.5-air":[2e-7,0.0000011,null,null],"zai/glm-4.5-airx":[0.0000011,0.0000045,null,null],"glm-4.5-airx":[0.0000011,0.0000045,null,null],"zai/glm-4-32b-0414-128k":[1e-7,1e-7,null,null],"glm-4-32b-0414-128k":[1e-7,1e-7,null,null],"zai/glm-4.5-flash":[0,0,null,null],"glm-4.5-flash":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":[4.5e-7,0.0000018,null,null],"accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":[4.5e-7,0.0000018,null,null],"fireworks_ai/accounts/fireworks/models/flux-kontext-pro":[4e-8,4e-8,null,null],"accounts/fireworks/models/flux-kontext-pro":[4e-8,4e-8,null,null],"fireworks_ai/accounts/fireworks/models/SSD-1B":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/SSD-1B":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/chronos-hermes-13b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/chronos-hermes-13b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b-python":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b-python":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b-python":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b-python":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b-python":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b-python":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b-python":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b-python":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-qwen-1p5-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-qwen-1p5-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/codegemma-2b":[1e-7,1e-7,null,null],"accounts/fireworks/models/codegemma-2b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/codegemma-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/codegemma-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-671b-v2-p1":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/cogito-671b-v2-p1":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-qwen-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-qwen-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-kontext-max":[8e-8,8e-8,null,null],"accounts/fireworks/models/flux-kontext-max":[8e-8,8e-8,null,null],"fireworks_ai/accounts/fireworks/models/dbrx-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/dbrx-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-1b-base":[1e-7,1e-7,null,null],"accounts/fireworks/models/deepseek-coder-1b-base":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-33b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-coder-33b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-base":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base-v1p5":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-base-v1p5":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-base":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-coder-v2-lite-base":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-instruct":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-coder-v2-lite-instruct":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-prover-v2":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-prover-v2":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-llama-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v2-lite-chat":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-v2-lite-chat":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v2p5":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-v2p5":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/devstral-small-2505":[9e-7,9e-7,null,null],"accounts/fireworks/models/devstral-small-2505":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dobby-mini-unhinged-plus-llama-3-1-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/dobby-mini-unhinged-plus-llama-3-1-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/dobby-unhinged-llama-3-3-70b-new":[9e-7,9e-7,null,null],"accounts/fireworks/models/dobby-unhinged-llama-3-3-70b-new":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dolphin-2-9-2-qwen2-72b":[9e-7,9e-7,null,null],"accounts/fireworks/models/dolphin-2-9-2-qwen2-72b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dolphin-2p6-mixtral-8x7b":[5e-7,5e-7,null,null],"accounts/fireworks/models/dolphin-2p6-mixtral-8x7b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/ernie-4p5-21b-a3b-pt":[1e-7,1e-7,null,null],"accounts/fireworks/models/ernie-4p5-21b-a3b-pt":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/ernie-4p5-300b-a47b-pt":[1e-7,1e-7,null,null],"accounts/fireworks/models/ernie-4p5-300b-a47b-pt":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/fare-20b":[9e-7,9e-7,null,null],"accounts/fireworks/models/fare-20b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/firefunction-v1":[5e-7,5e-7,null,null],"accounts/fireworks/models/firefunction-v1":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/firellava-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/firellava-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/firesearch-ocr-v6":[2e-7,2e-7,null,null],"accounts/fireworks/models/firesearch-ocr-v6":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/fireworks-asr-large":[0,0,null,null],"accounts/fireworks/models/fireworks-asr-large":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/fireworks-asr-v2":[0,0,null,null],"accounts/fireworks/models/fireworks-asr-v2":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev":[1e-7,1e-7,null,null],"accounts/fireworks/models/flux-1-dev":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev-controlnet-union":[1e-9,1e-9,null,null],"accounts/fireworks/models/flux-1-dev-controlnet-union":[1e-9,1e-9,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev-fp8":[5e-10,5e-10,null,null],"accounts/fireworks/models/flux-1-dev-fp8":[5e-10,5e-10,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-schnell":[1e-7,1e-7,null,null],"accounts/fireworks/models/flux-1-schnell":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-schnell-fp8":[3.5e-10,3.5e-10,null,null],"accounts/fireworks/models/flux-1-schnell-fp8":[3.5e-10,3.5e-10,null,null],"fireworks_ai/accounts/fireworks/models/gemma-2b-it":[1e-7,1e-7,null,null],"accounts/fireworks/models/gemma-2b-it":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-3-27b-it":[9e-7,9e-7,null,null],"accounts/fireworks/models/gemma-3-27b-it":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-7b-it":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma-7b-it":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma2-9b-it":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma2-9b-it":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5v":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/glm-4p5v":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-120b":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/gpt-oss-safeguard-120b":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-20b":[5e-7,5e-7,null,null],"accounts/fireworks/models/gpt-oss-safeguard-20b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/hermes-2-pro-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/hermes-2-pro-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-38b":[9e-7,9e-7,null,null],"accounts/fireworks/models/internvl3-38b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-78b":[9e-7,9e-7,null,null],"accounts/fireworks/models/internvl3-78b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/internvl3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/japanese-stable-diffusion-xl":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/japanese-stable-diffusion-xl":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/kat-coder":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-coder":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/kat-dev-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-dev-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/kat-dev-72b-exp":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-dev-72b-exp":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-2-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-guard-2-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-3-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-guard-3-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-guard-3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-13b-chat":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-13b-chat":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-70b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v2-70b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-70b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v2-70b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-7b-chat":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-7b-chat":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct-hf":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3-70b-instruct-hf":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-8b-instruct-hf":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3-8b-instruct-hf":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct-long":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-405b-instruct-long":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p1-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-70b-instruct-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p3-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p3-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llamaguard-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llamaguard-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llava-yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/llava-yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m1-80k":[1e-7,1e-7,null,null],"accounts/fireworks/models/minimax-m1-80k":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m2":[3e-7,0.0000012,null,null],"accounts/fireworks/models/minimax-m2":[3e-7,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-14b-instruct-2512":[2e-7,2e-7,null,null],"accounts/fireworks/models/ministral-3-14b-instruct-2512":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-3b-instruct-2512":[1e-7,1e-7,null,null],"accounts/fireworks/models/ministral-3-3b-instruct-2512":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-8b-instruct-2512":[2e-7,2e-7,null,null],"accounts/fireworks/models/ministral-3-8b-instruct-2512":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-4k":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-4k":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v0p2":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-v0p2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v3":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-v3":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-v0p2":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-v0p2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-large-3-fp8":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mistral-large-3-fp8":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mistral-nemo-base-2407":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-nemo-base-2407":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-nemo-instruct-2407":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-nemo-instruct-2407":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-small-24b-instruct-2501":[9e-7,9e-7,null,null],"accounts/fireworks/models/mistral-small-24b-instruct-2501":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b-instruct":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct-hf":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b-instruct-hf":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mythomax-l2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/mythomax-l2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nemotron-nano-v2-12b-vl":[1e-7,1e-7,null,null],"accounts/fireworks/models/nemotron-nano-v2-12b-vl":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-capybara-7b-v1p9":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-capybara-7b-v1p9":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo":[5e-7,5e-7,null,null],"accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-2-yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/nous-hermes-2-yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-12b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/nvidia-nemotron-nano-12b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-9b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/nvidia-nemotron-nano-9b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openchat-3p5-0106-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openchat-3p5-0106-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openhermes-2-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openhermes-2-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openhermes-2p5-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openhermes-2p5-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openorca-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openorca-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/phi-2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-3-mini-128k-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/phi-3-mini-128k-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-3-vision-128k-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/phi-3-vision-128k-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-python-v1":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-python-v1":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v1":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-v1":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v2":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-v2":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/playground-v2-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/playground-v2-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/playground-v2-5-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/playground-v2-5-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/pythia-12b":[2e-7,2e-7,null,null],"accounts/fireworks/models/pythia-12b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-qwq-32b-preview":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen-qwq-32b-preview":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-v2p5-14b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen-v2p5-14b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-v2p5-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen-v2p5-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen1p5-72b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen1p5-72b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-2b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2-vl-2b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2-vl-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2-vl-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-0p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-0p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-1p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-1p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-72b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-72b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-0p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-0p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-14b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-1p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-1p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-3b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-math-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-math-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-3b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-3b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-0p6b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-0p6b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-instruct-2507":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b-instruct-2507":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-thinking-2507":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b-thinking-2507":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-instruct-2507":[5e-7,5e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b-instruct-2507":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-thinking-2507":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b-thinking-2507":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-4b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-4b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-4b-instruct-2507":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-4b-instruct-2507":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-coder-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-instruct-bf16":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-coder-480b-instruct-bf16":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-embedding-0p6b":[0,0,null,null],"accounts/fireworks/models/qwen3-embedding-0p6b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-embedding-4b":[0,0,null,null],"accounts/fireworks/models/qwen3-embedding-4b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/":[1e-7,0,null,null],"accounts/fireworks/models/":[1e-7,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-next-80b-a3b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-thinking":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-next-80b-a3b-thinking":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-0p6b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-0p6b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-4b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-4b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-8b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-8b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-instruct":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-vl-235b-a22b-instruct":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-thinking":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-vl-235b-a22b-thinking":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-vl-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-thinking":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-vl-30b-a3b-thinking":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-vl-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-8b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-vl-8b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwq-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwq-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/rolm-ocr":[2e-7,2e-7,null,null],"accounts/fireworks/models/rolm-ocr":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo":[2e-7,2e-7,null,null],"accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/stable-diffusion-xl-1024-v1-0":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/stable-diffusion-xl-1024-v1-0":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/stablecode-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/stablecode-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder-16b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder-16b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-15b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder2-15b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/starcoder2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/toppy-m-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/toppy-m-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/whisper-v3":[0,0,null,null],"accounts/fireworks/models/whisper-v3":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/whisper-v3-turbo":[0,0,null,null],"accounts/fireworks/models/whisper-v3-turbo":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b-200k-capybara":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b-200k-capybara":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-6b":[2e-7,2e-7,null,null],"accounts/fireworks/models/yi-6b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/zephyr-7b-beta":[2e-7,2e-7,null,null],"accounts/fireworks/models/zephyr-7b-beta":[2e-7,2e-7,null,null],"novita/deepseek/deepseek-v3.2":[2.69e-7,4e-7,null,1.345e-7],"novita/minimax/minimax-m2.1":[3e-7,0.0000012,null,3e-8],"novita/zai-org/glm-4.7":[6e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.7":[6e-7,0.0000022,null,1.1e-7],"novita/xiaomimimo/mimo-v2-flash":[1e-7,3e-7,null,2e-8],"xiaomimimo/mimo-v2-flash":[1e-7,3e-7,null,2e-8],"novita/zai-org/autoglm-phone-9b-multilingual":[3.5e-8,1.38e-7,null,null],"zai-org/autoglm-phone-9b-multilingual":[3.5e-8,1.38e-7,null,null],"novita/moonshotai/kimi-k2-thinking":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-thinking":[6e-7,0.0000025,null,null],"novita/minimax/minimax-m2":[3e-7,0.0000012,null,3e-8],"novita/paddlepaddle/paddleocr-vl":[2e-8,2e-8,null,null],"paddlepaddle/paddleocr-vl":[2e-8,2e-8,null,null],"novita/deepseek/deepseek-v3.2-exp":[2.7e-7,4.1e-7,null,null],"novita/qwen/qwen3-vl-235b-a22b-thinking":[9.8e-7,0.00000395,null,null],"qwen/qwen3-vl-235b-a22b-thinking":[9.8e-7,0.00000395,null,null],"novita/zai-org/glm-4.6v":[3e-7,9e-7,null,5.5e-8],"zai-org/glm-4.6v":[3e-7,9e-7,null,5.5e-8],"novita/zai-org/glm-4.6":[5.5e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.6":[5.5e-7,0.0000022,null,1.1e-7],"novita/kwaipilot/kat-coder-pro":[3e-7,0.0000012,null,6e-8],"kwaipilot/kat-coder-pro":[3e-7,0.0000012,null,6e-8],"novita/qwen/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000015,null,null],"qwen/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000015,null,null],"novita/qwen/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000015,null,null],"qwen/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000015,null,null],"novita/deepseek/deepseek-ocr":[3e-8,3e-8,null,null],"deepseek/deepseek-ocr":[3e-8,3e-8,null,null],"novita/deepseek/deepseek-v3.1-terminus":[2.7e-7,0.000001,null,1.35e-7],"deepseek/deepseek-v3.1-terminus":[2.7e-7,0.000001,null,1.35e-7],"novita/qwen/qwen3-vl-235b-a22b-instruct":[3e-7,0.0000015,null,null],"qwen/qwen3-vl-235b-a22b-instruct":[3e-7,0.0000015,null,null],"novita/qwen/qwen3-max":[0.00000211,0.00000845,null,null],"qwen/qwen3-max":[0.00000211,0.00000845,null,null],"novita/skywork/r1v4-lite":[2e-7,6e-7,null,null],"skywork/r1v4-lite":[2e-7,6e-7,null,null],"novita/deepseek/deepseek-v3.1":[2.7e-7,0.000001,null,1.35e-7],"deepseek/deepseek-v3.1":[2.7e-7,0.000001,null,1.35e-7],"novita/moonshotai/kimi-k2-0905":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-0905":[6e-7,0.0000025,null,null],"novita/qwen/qwen3-coder-480b-a35b-instruct":[3e-7,0.0000013,null,null],"qwen/qwen3-coder-480b-a35b-instruct":[3e-7,0.0000013,null,null],"novita/qwen/qwen3-coder-30b-a3b-instruct":[7e-8,2.7e-7,null,null],"qwen/qwen3-coder-30b-a3b-instruct":[7e-8,2.7e-7,null,null],"novita/openai/gpt-oss-120b":[5e-8,2.5e-7,null,null],"novita/moonshotai/kimi-k2-instruct":[5.7e-7,0.0000023,null,null],"moonshotai/kimi-k2-instruct":[5.7e-7,0.0000023,null,null],"novita/deepseek/deepseek-v3-0324":[2.7e-7,0.00000112,null,1.35e-7],"deepseek/deepseek-v3-0324":[2.7e-7,0.00000112,null,1.35e-7],"novita/zai-org/glm-4.5":[6e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.5":[6e-7,0.0000022,null,1.1e-7],"novita/qwen/qwen3-235b-a22b-thinking-2507":[3e-7,0.000003,null,null],"novita/meta-llama/llama-3.1-8b-instruct":[2e-8,5e-8,null,null],"meta-llama/llama-3.1-8b-instruct":[2e-8,5e-8,null,null],"novita/google/gemma-3-12b-it":[5e-8,1e-7,null,null],"novita/zai-org/glm-4.5v":[6e-7,0.0000018,null,1.1e-7],"zai-org/glm-4.5v":[6e-7,0.0000018,null,1.1e-7],"novita/openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"novita/qwen/qwen3-235b-a22b-instruct-2507":[9e-8,5.8e-7,null,null],"novita/deepseek/deepseek-r1-distill-qwen-14b":[1.5e-7,1.5e-7,null,null],"deepseek/deepseek-r1-distill-qwen-14b":[1.5e-7,1.5e-7,null,null],"novita/meta-llama/llama-3.3-70b-instruct":[1.35e-7,4e-7,null,null],"meta-llama/llama-3.3-70b-instruct":[1.35e-7,4e-7,null,null],"novita/qwen/qwen-2.5-72b-instruct":[3.8e-7,4e-7,null,null],"qwen/qwen-2.5-72b-instruct":[3.8e-7,4e-7,null,null],"novita/mistralai/mistral-nemo":[4e-8,1.7e-7,null,null],"mistralai/mistral-nemo":[4e-8,1.7e-7,null,null],"novita/minimaxai/minimax-m1-80k":[5.5e-7,0.0000022,null,null],"minimaxai/minimax-m1-80k":[5.5e-7,0.0000022,null,null],"novita/deepseek/deepseek-r1-0528":[7e-7,0.0000025,null,3.5e-7],"novita/deepseek/deepseek-r1-distill-qwen-32b":[3e-7,3e-7,null,null],"deepseek/deepseek-r1-distill-qwen-32b":[3e-7,3e-7,null,null],"novita/meta-llama/llama-3-8b-instruct":[4e-8,4e-8,null,null],"meta-llama/llama-3-8b-instruct":[4e-8,4e-8,null,null],"novita/microsoft/wizardlm-2-8x22b":[6.2e-7,6.2e-7,null,null],"microsoft/wizardlm-2-8x22b":[6.2e-7,6.2e-7,null,null],"novita/deepseek/deepseek-r1-0528-qwen3-8b":[6e-8,9e-8,null,null],"deepseek/deepseek-r1-0528-qwen3-8b":[6e-8,9e-8,null,null],"novita/deepseek/deepseek-r1-distill-llama-70b":[8e-7,8e-7,null,null],"novita/meta-llama/llama-3-70b-instruct":[5.1e-7,7.4e-7,null,null],"novita/qwen/qwen3-235b-a22b-fp8":[2e-7,8e-7,null,null],"qwen/qwen3-235b-a22b-fp8":[2e-7,8e-7,null,null],"novita/meta-llama/llama-4-maverick-17b-128e-instruct-fp8":[2.7e-7,8.5e-7,null,null],"meta-llama/llama-4-maverick-17b-128e-instruct-fp8":[2.7e-7,8.5e-7,null,null],"novita/meta-llama/llama-4-scout-17b-16e-instruct":[1.8e-7,5.9e-7,null,null],"novita/nousresearch/hermes-2-pro-llama-3-8b":[1.4e-7,1.4e-7,null,null],"nousresearch/hermes-2-pro-llama-3-8b":[1.4e-7,1.4e-7,null,null],"novita/qwen/qwen2.5-vl-72b-instruct":[8e-7,8e-7,null,null],"qwen/qwen2.5-vl-72b-instruct":[8e-7,8e-7,null,null],"novita/sao10k/l3-70b-euryale-v2.1":[0.00000148,0.00000148,null,null],"sao10k/l3-70b-euryale-v2.1":[0.00000148,0.00000148,null,null],"novita/baidu/ernie-4.5-21B-a3b-thinking":[7e-8,2.8e-7,null,null],"baidu/ernie-4.5-21B-a3b-thinking":[7e-8,2.8e-7,null,null],"novita/sao10k/l3-8b-lunaris":[5e-8,5e-8,null,null],"sao10k/l3-8b-lunaris":[5e-8,5e-8,null,null],"novita/baichuan/baichuan-m2-32b":[7e-8,7e-8,null,null],"baichuan/baichuan-m2-32b":[7e-8,7e-8,null,null],"novita/baidu/ernie-4.5-vl-424b-a47b":[4.2e-7,0.00000125,null,null],"baidu/ernie-4.5-vl-424b-a47b":[4.2e-7,0.00000125,null,null],"novita/baidu/ernie-4.5-300b-a47b-paddle":[2.8e-7,0.0000011,null,null],"baidu/ernie-4.5-300b-a47b-paddle":[2.8e-7,0.0000011,null,null],"novita/deepseek/deepseek-prover-v2-671b":[7e-7,0.0000025,null,null],"deepseek/deepseek-prover-v2-671b":[7e-7,0.0000025,null,null],"novita/qwen/qwen3-32b-fp8":[1e-7,4.5e-7,null,null],"qwen/qwen3-32b-fp8":[1e-7,4.5e-7,null,null],"novita/qwen/qwen3-30b-a3b-fp8":[9e-8,4.5e-7,null,null],"qwen/qwen3-30b-a3b-fp8":[9e-8,4.5e-7,null,null],"novita/google/gemma-3-27b-it":[1.19e-7,2e-7,null,null],"novita/deepseek/deepseek-v3-turbo":[4e-7,0.0000013,null,null],"deepseek/deepseek-v3-turbo":[4e-7,0.0000013,null,null],"novita/deepseek/deepseek-r1-turbo":[7e-7,0.0000025,null,null],"deepseek/deepseek-r1-turbo":[7e-7,0.0000025,null,null],"novita/Sao10K/L3-8B-Stheno-v3.2":[5e-8,5e-8,null,null],"Sao10K/L3-8B-Stheno-v3.2":[5e-8,5e-8,null,null],"novita/gryphe/mythomax-l2-13b":[9e-8,9e-8,null,null],"novita/baidu/ernie-4.5-vl-28b-a3b-thinking":[3.9e-7,3.9e-7,null,null],"baidu/ernie-4.5-vl-28b-a3b-thinking":[3.9e-7,3.9e-7,null,null],"novita/qwen/qwen3-vl-8b-instruct":[8e-8,5e-7,null,null],"qwen/qwen3-vl-8b-instruct":[8e-8,5e-7,null,null],"novita/zai-org/glm-4.5-air":[1.3e-7,8.5e-7,null,null],"zai-org/glm-4.5-air":[1.3e-7,8.5e-7,null,null],"novita/qwen/qwen3-vl-30b-a3b-instruct":[2e-7,7e-7,null,null],"qwen/qwen3-vl-30b-a3b-instruct":[2e-7,7e-7,null,null],"novita/qwen/qwen3-vl-30b-a3b-thinking":[2e-7,0.000001,null,null],"qwen/qwen3-vl-30b-a3b-thinking":[2e-7,0.000001,null,null],"novita/qwen/qwen3-omni-30b-a3b-thinking":[2.5e-7,9.7e-7,null,null],"qwen/qwen3-omni-30b-a3b-thinking":[2.5e-7,9.7e-7,null,null],"novita/qwen/qwen3-omni-30b-a3b-instruct":[2.5e-7,9.7e-7,null,null],"qwen/qwen3-omni-30b-a3b-instruct":[2.5e-7,9.7e-7,null,null],"novita/qwen/qwen-mt-plus":[2.5e-7,7.5e-7,null,null],"qwen/qwen-mt-plus":[2.5e-7,7.5e-7,null,null],"novita/baidu/ernie-4.5-vl-28b-a3b":[1.4e-7,5.6e-7,null,null],"baidu/ernie-4.5-vl-28b-a3b":[1.4e-7,5.6e-7,null,null],"novita/baidu/ernie-4.5-21B-a3b":[7e-8,2.8e-7,null,null],"baidu/ernie-4.5-21B-a3b":[7e-8,2.8e-7,null,null],"novita/qwen/qwen3-8b-fp8":[3.5e-8,1.38e-7,null,null],"qwen/qwen3-8b-fp8":[3.5e-8,1.38e-7,null,null],"novita/qwen/qwen3-4b-fp8":[3e-8,3e-8,null,null],"qwen/qwen3-4b-fp8":[3e-8,3e-8,null,null],"novita/qwen/qwen2.5-7b-instruct":[7e-8,7e-8,null,null],"qwen/qwen2.5-7b-instruct":[7e-8,7e-8,null,null],"novita/meta-llama/llama-3.2-3b-instruct":[3e-8,5e-8,null,null],"meta-llama/llama-3.2-3b-instruct":[3e-8,5e-8,null,null],"novita/sao10k/l31-70b-euryale-v2.2":[0.00000148,0.00000148,null,null],"sao10k/l31-70b-euryale-v2.2":[0.00000148,0.00000148,null,null],"novita/qwen/qwen3-embedding-0.6b":[7e-8,0,null,null],"qwen/qwen3-embedding-0.6b":[7e-8,0,null,null],"novita/qwen/qwen3-embedding-8b":[7e-8,0,null,null],"qwen/qwen3-embedding-8b":[7e-8,0,null,null],"novita/baai/bge-m3":[1e-8,1e-8,null,null],"baai/bge-m3":[1e-8,1e-8,null,null],"novita/qwen/qwen3-reranker-8b":[5e-8,5e-8,null,null],"qwen/qwen3-reranker-8b":[5e-8,5e-8,null,null],"novita/baai/bge-reranker-v2-m3":[1e-8,1e-8,null,null],"baai/bge-reranker-v2-m3":[1e-8,1e-8,null,null],"llamagate/llama-3.1-8b":[3e-8,5e-8,null,null],"llama-3.1-8b":[3e-8,5e-8,null,null],"llamagate/llama-3.2-3b":[4e-8,8e-8,null,null],"llama-3.2-3b":[4e-8,8e-8,null,null],"llamagate/mistral-7b-v0.3":[1e-7,1.5e-7,null,null],"mistral-7b-v0.3":[1e-7,1.5e-7,null,null],"llamagate/qwen3-8b":[4e-8,1.4e-7,null,null],"qwen3-8b":[4e-8,1.4e-7,null,null],"llamagate/dolphin3-8b":[8e-8,1.5e-7,null,null],"dolphin3-8b":[8e-8,1.5e-7,null,null],"llamagate/deepseek-r1-8b":[1e-7,2e-7,null,null],"deepseek-r1-8b":[1e-7,2e-7,null,null],"llamagate/deepseek-r1-7b-qwen":[8e-8,1.5e-7,null,null],"deepseek-r1-7b-qwen":[8e-8,1.5e-7,null,null],"llamagate/openthinker-7b":[8e-8,1.5e-7,null,null],"openthinker-7b":[8e-8,1.5e-7,null,null],"llamagate/qwen2.5-coder-7b":[6e-8,1.2e-7,null,null],"qwen2.5-coder-7b":[6e-8,1.2e-7,null,null],"llamagate/deepseek-coder-6.7b":[6e-8,1.2e-7,null,null],"deepseek-coder-6.7b":[6e-8,1.2e-7,null,null],"llamagate/codellama-7b":[6e-8,1.2e-7,null,null],"codellama-7b":[6e-8,1.2e-7,null,null],"llamagate/qwen3-vl-8b":[1.5e-7,5.5e-7,null,null],"qwen3-vl-8b":[1.5e-7,5.5e-7,null,null],"llamagate/llava-7b":[1e-7,2e-7,null,null],"llava-7b":[1e-7,2e-7,null,null],"llamagate/gemma3-4b":[3e-8,8e-8,null,null],"gemma3-4b":[3e-8,8e-8,null,null],"llamagate/nomic-embed-text":[2e-8,0,null,null],"nomic-embed-text":[2e-8,0,null,null],"llamagate/qwen3-embedding-8b":[2e-8,0,null,null],"qwen3-embedding-8b":[2e-8,0,null,null],"sarvam/sarvam-m":[0,0,0,0],"sarvam-m":[0,0,0,0],"gemini/gemini-2.0-flash-exp-image-generation":[0,0,null,null],"gemini/gemini-2.0-flash-lite-001":[7.5e-8,3e-7,null,1.875e-8],"gemini/gemini-2.5-flash-native-audio-latest":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-flash-native-audio-preview-09-2025":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-flash-native-audio-preview-12-2025":[3e-7,0.0000025,null,null],"gemini/gemini-3.1-flash-live-preview":[7.5e-7,0.0000045,null,null],"gemini/gemini-pro-latest":[0.00000125,0.00001,null,1.25e-7],"vertex_ai/claude-sonnet-4-6@default":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-6@default":[0.000003,0.000015,0.00000375,3e-7],"bedrock_mantle/openai.gpt-oss-120b":[1.5e-7,6e-7,null,null],"openai.gpt-oss-120b":[1.5e-7,6e-7,null,null],"bedrock_mantle/openai.gpt-oss-20b":[7.5e-8,3e-7,null,null],"openai.gpt-oss-20b":[7.5e-8,3e-7,null,null],"bedrock_mantle/openai.gpt-oss-safeguard-120b":[1.5e-7,6e-7,null,null],"bedrock_mantle/openai.gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,null],"bedrock/us-east-1/zai.glm-5":[0.000001,0.0000032,null,null],"us-east-1/zai.glm-5":[0.000001,0.0000032,null,null],"bedrock/us-west-2/zai.glm-5":[0.000001,0.0000032,null,null],"us-west-2/zai.glm-5":[0.000001,0.0000032,null,null],"bedrock/us-gov-east-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"us-gov-east-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"bedrock/us-gov-west-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"us-gov-west-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"MiniMax-M2.7":[3e-7,0.0000012,3.75e-7,6e-8],"MiniMax-M2.7-highspeed":[6e-7,0.0000024,3.75e-7,6e-8]}
\ No newline at end of file
+{"ai21.j2-mid-v1":[0.0000125,0.0000125,null,null],"ai21.j2-ultra-v1":[0.0000188,0.0000188,null,null],"ai21.jamba-1-5-large-v1:0":[0.000002,0.000008,null,null],"ai21.jamba-1-5-mini-v1:0":[2e-7,4e-7,null,null],"ai21.jamba-instruct-v1:0":[5e-7,7e-7,null,null],"us.writer.palmyra-x4-v1:0":[0.0000025,0.00001,null,null],"us.writer.palmyra-x5-v1:0":[6e-7,0.000006,null,null],"writer.palmyra-x4-v1:0":[0.0000025,0.00001,null,null],"writer.palmyra-x5-v1:0":[6e-7,0.000006,null,null],"amazon.nova-lite-v1:0":[6e-8,2.4e-7,null,null],"amazon.nova-2-lite-v1:0":[3e-7,0.0000025,null,7.5e-8],"amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"apac.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"apac.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"eu.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"eu.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"us.amazon.nova-2-lite-v1:0":[3.3e-7,0.00000275,null,8.25e-8],"us.amazon.nova-2-pro-preview-20251202-v1:0":[0.0000021875,0.0000175,null,5.46875e-7],"amazon.nova-2-multimodal-embeddings-v1:0":[1.35e-7,0,null,null],"amazon.nova-micro-v1:0":[3.5e-8,1.4e-7,null,null],"amazon.nova-pro-v1:0":[8e-7,0.0000032,null,null],"amazon.rerank-v1:0":[0,0,null,null],"amazon.titan-embed-image-v1":[8e-7,0,null,null],"amazon.titan-embed-text-v1":[1e-7,0,null,null],"amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"us.twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"eu.twelvelabs.marengo-embed-2-7-v1:0":[0.00007,0,null,null],"amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"anthropic.claude-haiku-4-5-20251001-v1:0":[0.000001,0.000005,0.00000125,1e-7],"anthropic.claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-7-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"anthropic.claude-opus-4-6-v1":[0.000005,0.000025,0.00000625,5e-7],"global.anthropic.claude-opus-4-6-v1":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"eu.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"au.anthropic.claude-opus-4-6-v1":[0.0000055,0.0000275,0.000006875,5.5e-7],"anthropic.claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"anthropic.claude-mythos-preview":[0,0,null,null],"global.anthropic.claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"eu.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"au.anthropic.claude-opus-4-7":[0.0000055,0.0000275,0.000006875,5.5e-7],"anthropic.claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"eu.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"au.anthropic.claude-sonnet-4-6":[0.0000033,0.0000165,0.000004125,3.3e-7],"anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"anthropic.claude-v1":[0.000008,0.000024,null,null],"anthropic.claude-v2:1":[0.000008,0.000024,null,null],"apac.amazon.nova-lite-v1:0":[6.3e-8,2.52e-7,null,null],"apac.amazon.nova-micro-v1:0":[3.7e-8,1.48e-7,null,null],"apac.amazon.nova-pro-v1:0":[8.4e-7,0.00000336,null,null],"apac.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"apac.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"apac.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"apac.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"au.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"babbage-002":[4e-7,4e-7,null,null],"chatdolphin":[5e-7,5e-7,null,null],"chatgpt-4o-latest":[0.000005,0.000015,null,null],"gpt-4o-transcribe-diarize":[0.0000025,0.00001,null,null],"claude-haiku-4-5-20251001":[0.000001,0.000005,0.00000125,1e-7],"claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"claude-3-7-sonnet-20250219":[0.000003,0.000015,0.00000375,3e-7],"claude-3-haiku-20240307":[2.5e-7,0.00000125,3e-7,3e-8],"claude-3-opus-20240229":[0.000015,0.000075,0.00001875,0.0000015],"claude-4-opus-20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-4-sonnet-20250514":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5-20250929":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-1-20250805":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-5-20251101":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6-20260205":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7-20260416":[0.000005,0.000025,0.00000625,5e-7],"claude-sonnet-4-20250514":[0.000003,0.000015,0.00000375,3e-7],"codex-mini-latest":[0.0000015,0.000006,null,3.75e-7],"cohere.command-light-text-v14":[3e-7,6e-7,null,null],"cohere.command-r-plus-v1:0":[0.000003,0.000015,null,null],"cohere.command-r-v1:0":[5e-7,0.0000015,null,null],"cohere.command-text-v14":[0.0000015,0.000002,null,null],"cohere.embed-english-v3":[1e-7,0,null,null],"cohere.embed-multilingual-v3":[1e-7,0,null,null],"cohere.embed-v4:0":[1.2e-7,0,null,null],"cohere.rerank-v3-5:0":[0,0,null,null],"command":[0.000001,0.000002,null,null],"command-a-03-2025":[0.0000025,0.00001,null,null],"command-light":[3e-7,6e-7,null,null],"command-nightly":[0.000001,0.000002,null,null],"command-r":[1.5e-7,6e-7,null,null],"command-r-08-2024":[1.5e-7,6e-7,null,null],"command-r-plus":[0.0000025,0.00001,null,null],"command-r-plus-08-2024":[0.0000025,0.00001,null,null],"command-r7b-12-2024":[1.5e-7,3.75e-8,null,null],"computer-use-preview":[0.000003,0.000012,null,null],"deepseek-chat":[2.8e-7,4.2e-7,null,2.8e-8],"deepseek-reasoner":[2.8e-7,4.2e-7,null,2.8e-8],"davinci-002":[0.000002,0.000002,null,null],"deepseek.v3-v1:0":[5.8e-7,0.00000168,null,null],"deepseek.v3.2":[6.2e-7,0.00000185,null,null],"dolphin":[5e-7,5e-7,null,null],"deepseek-v3-2-251201":[0,0,null,null],"glm-4-7-251222":[0,0,null,null],"kimi-k2-thinking-251104":[0,0,null,null],"doubao-embedding":[0,0,null,null],"doubao-embedding-large":[0,0,null,null],"doubao-embedding-large-text-240915":[0,0,null,null],"doubao-embedding-large-text-250515":[0,0,null,null],"doubao-embedding-text-240715":[0,0,null,null],"embed-english-light-v2.0":[1e-7,0,null,null],"embed-english-light-v3.0":[1e-7,0,null,null],"embed-english-v2.0":[1e-7,0,null,null],"embed-english-v3.0":[1e-7,0,null,null],"embed-multilingual-v2.0":[1e-7,0,null,null],"embed-multilingual-v3.0":[1e-7,0,null,null],"embed-multilingual-light-v3.0":[0.0001,0,null,null],"eu.amazon.nova-lite-v1:0":[7.8e-8,3.12e-7,null,null],"eu.amazon.nova-micro-v1:0":[4.6e-8,1.84e-7,null,null],"eu.amazon.nova-pro-v1:0":[0.00000105,0.0000042,null,null],"eu.anthropic.claude-3-5-haiku-20241022-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"eu.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"eu.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"eu.anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"eu.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"eu.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"eu.meta.llama3-2-1b-instruct-v1:0":[1.3e-7,1.3e-7,null,null],"eu.meta.llama3-2-3b-instruct-v1:0":[1.9e-7,1.9e-7,null,null],"eu.mistral.pixtral-large-2502-v1:0":[0.000002,0.000006,null,null],"fireworks-ai-4.1b-to-16b":[2e-7,2e-7,null,null],"fireworks-ai-56b-to-176b":[0.0000012,0.0000012,null,null],"fireworks-ai-above-16b":[9e-7,9e-7,null,null],"fireworks-ai-default":[0,0,null,null],"fireworks-ai-embedding-150m-to-350m":[1.6e-8,0,null,null],"fireworks-ai-embedding-up-to-150m":[8e-9,0,null,null],"fireworks-ai-moe-up-to-56b":[5e-7,5e-7,null,null],"fireworks-ai-up-to-4b":[2e-7,2e-7,null,null],"ft:babbage-002":[0.0000016,0.0000016,null,null],"ft:davinci-002":[0.000012,0.000012,null,null],"ft:gpt-3.5-turbo":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-0125":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-0613":[0.000003,0.000006,null,null],"ft:gpt-3.5-turbo-1106":[0.000003,0.000006,null,null],"ft:gpt-4-0613":[0.00003,0.00006,null,null],"ft:gpt-4o-2024-08-06":[0.00000375,0.000015,null,0.000001875],"ft:gpt-4o-2024-11-20":[0.00000375,0.000015,0.000001875,null],"ft:gpt-4o-mini-2024-07-18":[3e-7,0.0000012,null,1.5e-7],"ft:gpt-4.1-2025-04-14":[0.000003,0.000012,null,7.5e-7],"ft:gpt-4.1-mini-2025-04-14":[8e-7,0.0000032,null,2e-7],"ft:gpt-4.1-nano-2025-04-14":[2e-7,8e-7,null,5e-8],"ft:o4-mini-2025-04-16":[0.000004,0.000016,null,0.000001],"gemini-2.0-flash":[1e-7,4e-7,null,2.5e-8],"gemini-2.0-flash-001":[1.5e-7,6e-7,null,3.75e-8],"gemini-2.0-flash-lite":[7.5e-8,3e-7,null,1.875e-8],"gemini-2.0-flash-lite-001":[7.5e-8,3e-7,null,1.875e-8],"gemini-2.5-flash":[3e-7,0.0000025,null,3e-8],"gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"gemini-3.1-flash-image-preview":[5e-7,0.000003,null,null],"gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"gemini-2.5-flash-lite":[1e-7,4e-7,null,1e-8],"gemini-2.5-flash-lite-preview-09-2025":[1e-7,4e-7,null,1e-8],"gemini-2.5-flash-preview-09-2025":[3e-7,0.0000025,null,7.5e-8],"gemini-live-2.5-flash-preview-native-audio-09-2025":[3e-7,0.000002,null,7.5e-8],"gemini-2.5-flash-lite-preview-06-17":[1e-7,4e-7,null,2.5e-8],"gemini-2.5-pro":[0.00000125,0.00001,null,1.25e-7],"gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini-2.5-pro-preview-tts":[0.00000125,0.00001,null,1.25e-7],"gemini-robotics-er-1.5-preview":[3e-7,0.0000025,null,0],"gemini-2.5-computer-use-preview-10-2025":[0.00000125,0.00001,null,null],"gemini-embedding-001":[1.5e-7,0,null,null],"gemini-embedding-2-preview":[2e-7,0,null,null],"gemini-embedding-2":[2e-7,0,null,null],"gemini-flash-experimental":[0,0,null,null],"gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"google.gemma-3-12b-it":[9e-8,2.9e-7,null,null],"google.gemma-3-27b-it":[2.3e-7,3.8e-7,null,null],"google.gemma-3-4b-it":[4e-8,8e-8,null,null],"global.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"global.anthropic.claude-haiku-4-5-20251001-v1:0":[0.000001,0.000005,0.00000125,1e-7],"global.amazon.nova-2-lite-v1:0":[3e-7,0.0000025,null,7.5e-8],"gpt-3.5-turbo":[5e-7,0.0000015,null,null],"gpt-3.5-turbo-0125":[5e-7,0.0000015,null,null],"gpt-3.5-turbo-1106":[0.000001,0.000002,null,null],"gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"gpt-3.5-turbo-instruct-0914":[0.0000015,0.000002,null,null],"gpt-4":[0.00003,0.00006,null,null],"gpt-4-0125-preview":[0.00001,0.00003,null,null],"gpt-4-0314":[0.00003,0.00006,null,null],"gpt-4-0613":[0.00003,0.00006,null,null],"gpt-4-1106-preview":[0.00001,0.00003,null,null],"gpt-4-turbo":[0.00001,0.00003,null,null],"gpt-4-turbo-2024-04-09":[0.00001,0.00003,null,null],"gpt-4-turbo-preview":[0.00001,0.00003,null,null],"gpt-4.1":[0.000002,0.000008,null,5e-7],"gpt-4.1-2025-04-14":[0.000002,0.000008,null,5e-7],"gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"gpt-4.1-mini-2025-04-14":[4e-7,0.0000016,null,1e-7],"gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"gpt-4.1-nano-2025-04-14":[1e-7,4e-7,null,2.5e-8],"gpt-4o":[0.0000025,0.00001,null,0.00000125],"gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"gpt-4o-audio-preview":[0.0000025,0.00001,null,null],"gpt-4o-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"gpt-4o-audio-preview-2025-06-03":[0.0000025,0.00001,null,null],"gpt-audio":[0.0000025,0.00001,null,null],"gpt-audio-1.5":[0.0000025,0.00001,null,null],"gpt-audio-2025-08-28":[0.0000025,0.00001,null,null],"gpt-audio-mini":[6e-7,0.0000024,null,null],"gpt-audio-mini-2025-10-06":[6e-7,0.0000024,null,null],"gpt-audio-mini-2025-12-15":[6e-7,0.0000024,null,null],"gpt-4o-mini":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-2024-07-18":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-audio-preview":[1.5e-7,6e-7,null,null],"gpt-4o-mini-audio-preview-2024-12-17":[1.5e-7,6e-7,null,null],"gpt-4o-mini-realtime-preview":[6e-7,0.0000024,null,3e-7],"gpt-4o-mini-realtime-preview-2024-12-17":[6e-7,0.0000024,null,3e-7],"gpt-4o-mini-search-preview":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-search-preview-2025-03-11":[1.5e-7,6e-7,null,7.5e-8],"gpt-4o-mini-transcribe":[0.00000125,0.000005,null,null],"gpt-4o-mini-tts":[0.0000025,0.00001,null,null],"gpt-4o-realtime-preview":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2024-12-17":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2025-06-03":[0.000005,0.00002,null,0.0000025],"gpt-4o-search-preview":[0.0000025,0.00001,null,0.00000125],"gpt-4o-search-preview-2025-03-11":[0.0000025,0.00001,null,0.00000125],"gpt-4o-transcribe":[0.0000025,0.00001,null,null],"gpt-image-1.5":[0.000005,0.00001,null,0.00000125],"gpt-image-1.5-2025-12-16":[0.000005,0.00001,null,0.00000125],"gpt-image-2":[0.000005,0.00001,null,0.00000125],"gpt-image-2-2026-04-21":[0.000005,0.00001,null,0.00000125],"gpt-5":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat-latest":[0.00000125,0.00001,null,1.25e-7],"gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat-latest":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-chat-latest":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-pro":[0.000021,0.000168,null,null],"gpt-5.2-pro-2025-12-11":[0.000021,0.000168,null,null],"gpt-5.5":[0.000005,0.00003,null,5e-7],"gpt-5.5-2026-04-23":[0.000005,0.00003,null,5e-7],"gpt-5.5-pro":[0.00003,0.00018,null,0.000003],"gpt-5.5-pro-2026-04-23":[0.00003,0.00018,null,0.000003],"gpt-5.4":[0.0000025,0.000015,null,2.5e-7],"gpt-5.4-2026-03-05":[0.0000025,0.000015,null,2.5e-7],"gpt-5.4-pro":[0.00003,0.00018,null,0.000003],"gpt-5.4-pro-2026-03-05":[0.00003,0.00018,null,0.000003],"gpt-5.4-mini":[7.5e-7,0.0000045,null,7.5e-8],"gpt-5.4-mini-2026-03-17":[7.5e-7,0.0000045,null,7.5e-8],"gpt-5.4-nano":[2e-7,0.00000125,null,2e-8],"gpt-5.4-nano-2026-03-17":[2e-7,0.00000125,null,2e-8],"gpt-5-pro":[0.000015,0.00012,null,null],"gpt-5-pro-2025-10-06":[0.000015,0.00012,null,null],"gpt-5-2025-08-07":[0.00000125,0.00001,null,1.25e-7],"gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"gpt-5-chat-latest":[0.00000125,0.00001,null,1.25e-7],"gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-codex":[0.00000175,0.000014,null,1.75e-7],"gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"gpt-5-mini-2025-08-07":[2.5e-7,0.000002,null,2.5e-8],"gpt-5-nano":[5e-8,4e-7,null,5e-9],"gpt-5-nano-2025-08-07":[5e-8,4e-7,null,5e-9],"gpt-realtime":[0.000004,0.000016,null,4e-7],"gpt-realtime-1.5":[0.000004,0.000016,null,4e-7],"gpt-realtime-2":[0.000004,0.000016,null,4e-7],"gpt-realtime-mini":[6e-7,0.0000024,null,null],"gpt-realtime-2025-08-28":[0.000004,0.000016,null,4e-7],"j2-light":[0.000003,0.000003,null,null],"j2-mid":[0.00001,0.00001,null,null],"j2-ultra":[0.000015,0.000015,null,null],"jamba-1.5":[2e-7,4e-7,null,null],"jamba-1.5-large":[0.000002,0.000008,null,null],"jamba-1.5-large@001":[0.000002,0.000008,null,null],"jamba-1.5-mini":[2e-7,4e-7,null,null],"jamba-1.5-mini@001":[2e-7,4e-7,null,null],"jamba-large-1.6":[0.000002,0.000008,null,null],"jamba-large-1.7":[0.000002,0.000008,null,null],"jamba-mini-1.6":[2e-7,4e-7,null,null],"jamba-mini-1.7":[2e-7,4e-7,null,null],"jina-reranker-v2-base-multilingual":[1.8e-8,1.8e-8,null,null],"jp.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"jp.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"meta.llama2-13b-chat-v1":[7.5e-7,0.000001,null,null],"meta.llama2-70b-chat-v1":[0.00000195,0.00000256,null,null],"meta.llama3-1-405b-instruct-v1:0":[0.00000532,0.000016,null,null],"meta.llama3-1-70b-instruct-v1:0":[9.9e-7,9.9e-7,null,null],"meta.llama3-1-8b-instruct-v1:0":[2.2e-7,2.2e-7,null,null],"meta.llama3-2-11b-instruct-v1:0":[3.5e-7,3.5e-7,null,null],"meta.llama3-2-1b-instruct-v1:0":[1e-7,1e-7,null,null],"meta.llama3-2-3b-instruct-v1:0":[1.5e-7,1.5e-7,null,null],"meta.llama3-2-90b-instruct-v1:0":[0.000002,0.000002,null,null],"meta.llama3-3-70b-instruct-v1:0":[7.2e-7,7.2e-7,null,null],"meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"meta.llama4-maverick-17b-instruct-v1:0":[2.4e-7,9.7e-7,null,null],"meta.llama4-scout-17b-instruct-v1:0":[1.7e-7,6.6e-7,null,null],"minimax.minimax-m2":[3e-7,0.0000012,null,null],"minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"mistral.devstral-2-123b":[4e-7,0.000002,null,null],"mistral.magistral-small-2509":[5e-7,0.0000015,null,null],"mistral.ministral-3-14b-instruct":[2e-7,2e-7,null,null],"mistral.ministral-3-3b-instruct":[1e-7,1e-7,null,null],"mistral.ministral-3-8b-instruct":[1.5e-7,1.5e-7,null,null],"mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"mistral.mistral-large-2407-v1:0":[0.000003,0.000009,null,null],"mistral.mistral-large-3-675b-instruct":[5e-7,0.0000015,null,null],"mistral.mistral-small-2402-v1:0":[0.000001,0.000003,null,null],"mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"mistral.voxtral-mini-3b-2507":[4e-8,4e-8,null,null],"mistral.voxtral-small-24b-2507":[1e-7,3e-7,null,null],"moonshot.kimi-k2-thinking":[6e-7,0.0000025,null,null],"moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"multimodalembedding":[8e-7,0,null,null],"multimodalembedding@001":[8e-7,0,null,null],"nvidia.nemotron-nano-12b-v2":[2e-7,6e-7,null,null],"nvidia.nemotron-nano-9b-v2":[6e-8,2.3e-7,null,null],"nvidia.nemotron-nano-3-30b":[6e-8,2.4e-7,null,null],"nvidia.nemotron-super-3-120b":[1.5e-7,6.5e-7,null,null],"o1":[0.000015,0.00006,null,0.0000075],"o1-2024-12-17":[0.000015,0.00006,null,0.0000075],"o1-pro":[0.00015,0.0006,null,null],"o1-pro-2025-03-19":[0.00015,0.0006,null,null],"o3":[0.000002,0.000008,null,5e-7],"o3-2025-04-16":[0.000002,0.000008,null,5e-7],"o3-deep-research":[0.00001,0.00004,null,0.0000025],"o3-deep-research-2025-06-26":[0.00001,0.00004,null,0.0000025],"o3-mini":[0.0000011,0.0000044,null,5.5e-7],"o3-mini-2025-01-31":[0.0000011,0.0000044,null,5.5e-7],"o3-pro":[0.00002,0.00008,null,null],"o3-pro-2025-06-10":[0.00002,0.00008,null,null],"o4-mini":[0.0000011,0.0000044,null,2.75e-7],"o4-mini-2025-04-16":[0.0000011,0.0000044,null,2.75e-7],"o4-mini-deep-research":[0.000002,0.000008,null,5e-7],"o4-mini-deep-research-2025-06-26":[0.000002,0.000008,null,5e-7],"omni-moderation-2024-09-26":[0,0,null,null],"omni-moderation-latest":[0,0,null,null],"openai.gpt-oss-120b-1:0":[1.5e-7,6e-7,null,null],"openai.gpt-oss-20b-1:0":[7e-8,3e-7,null,null],"openai.gpt-oss-safeguard-120b":[1.5e-7,6e-7,null,null],"openai.gpt-oss-safeguard-20b":[7e-8,2e-7,null,null],"qwen.qwen3-coder-480b-a35b-v1:0":[2.2e-7,0.0000018,null,null],"qwen.qwen3-235b-a22b-2507-v1:0":[2.2e-7,8.8e-7,null,null],"qwen.qwen3-coder-30b-a3b-v1:0":[1.5e-7,6e-7,null,null],"qwen.qwen3-32b-v1:0":[1.5e-7,6e-7,null,null],"qwen.qwen3-next-80b-a3b":[1.5e-7,0.0000012,null,null],"qwen.qwen3-vl-235b-a22b":[5.3e-7,0.00000266,null,null],"qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"rerank-english-v2.0":[0,0,null,null],"rerank-english-v3.0":[0,0,null,null],"rerank-multilingual-v2.0":[0,0,null,null],"rerank-multilingual-v3.0":[0,0,null,null],"rerank-v3.5":[0,0,null,null],"text-embedding-004":[1e-7,0,null,null],"text-embedding-005":[1e-7,0,null,null],"text-embedding-3-large":[1.3e-7,0,null,null],"text-embedding-3-small":[2e-8,0,null,null],"text-embedding-ada-002":[1e-7,0,null,null],"text-embedding-ada-002-v2":[1e-7,0,null,null],"text-embedding-large-exp-03-07":[1e-7,0,null,null],"text-embedding-preview-0409":[6.25e-9,0,null,null],"text-moderation-007":[0,0,null,null],"text-moderation-latest":[0,0,null,null],"text-moderation-stable":[0,0,null,null],"text-multilingual-embedding-002":[1e-7,0,null,null],"text-unicorn":[0.00001,0.000028,null,null],"text-unicorn@001":[0.00001,0.000028,null,null],"together-ai-21.1b-41b":[8e-7,8e-7,null,null],"together-ai-4.1b-8b":[2e-7,2e-7,null,null],"together-ai-41.1b-80b":[9e-7,9e-7,null,null],"together-ai-8.1b-21b":[3e-7,3e-7,null,null],"together-ai-81.1b-110b":[0.0000018,0.0000018,null,null],"together-ai-embedding-151m-to-350m":[1.6e-8,0,null,null],"together-ai-embedding-up-to-150m":[8e-9,0,null,null],"together-ai-up-to-4b":[1e-7,1e-7,null,null],"us.amazon.nova-lite-v1:0":[6e-8,2.4e-7,null,null],"us.amazon.nova-micro-v1:0":[3.5e-8,1.4e-7,null,null],"us.amazon.nova-premier-v1:0":[0.0000025,0.0000125,null,null],"us.amazon.nova-pro-v1:0":[8e-7,0.0000032,null,null],"us.anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"us.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"us.anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-5-sonnet-20241022-v2:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-7-sonnet-20250219-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-3-haiku-20240307-v1:0":[2.5e-7,0.00000125,3.125e-7,2.5e-8],"us.anthropic.claude-3-opus-20240229-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-3-sonnet-20240229-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.anthropic.claude-opus-4-1-20250805-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov.anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"au.anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000011,0.0000055,0.000001375,1.1e-7],"us.anthropic.claude-opus-4-20250514-v1:0":[0.000015,0.000075,0.00001875,0.0000015],"us.anthropic.claude-opus-4-5-20251101-v1:0":[0.0000055,0.0000275,0.000006875,5.5e-7],"global.anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"eu.anthropic.claude-opus-4-5-20251101-v1:0":[0.000005,0.000025,0.00000625,5e-7],"us.anthropic.claude-sonnet-4-20250514-v1:0":[0.000003,0.000015,0.00000375,3e-7],"us.deepseek.r1-v1:0":[0.00000135,0.0000054,null,null],"us.deepseek.v3.2":[6.2e-7,0.00000185,null,null],"eu.deepseek.v3.2":[7.4e-7,0.00000222,null,null],"us.meta.llama3-1-405b-instruct-v1:0":[0.00000532,0.000016,null,null],"us.meta.llama3-1-70b-instruct-v1:0":[9.9e-7,9.9e-7,null,null],"us.meta.llama3-1-8b-instruct-v1:0":[2.2e-7,2.2e-7,null,null],"us.meta.llama3-2-11b-instruct-v1:0":[3.5e-7,3.5e-7,null,null],"us.meta.llama3-2-1b-instruct-v1:0":[1e-7,1e-7,null,null],"us.meta.llama3-2-3b-instruct-v1:0":[1.5e-7,1.5e-7,null,null],"us.meta.llama3-2-90b-instruct-v1:0":[0.000002,0.000002,null,null],"us.meta.llama3-3-70b-instruct-v1:0":[7.2e-7,7.2e-7,null,null],"us.meta.llama4-maverick-17b-instruct-v1:0":[2.4e-7,9.7e-7,null,null],"us.meta.llama4-scout-17b-instruct-v1:0":[1.7e-7,6.6e-7,null,null],"us.mistral.pixtral-large-2502-v1:0":[0.000002,0.000006,null,null],"zai.glm-4.7":[6e-7,0.0000022,null,null],"zai.glm-5":[0.000001,0.0000032,null,null],"zai.glm-4.7-flash":[7e-8,4e-7,null,null],"gpt-4o-mini-tts-2025-03-20":[0.0000025,0.00001,null,null],"gpt-4o-mini-tts-2025-12-15":[0.0000025,0.00001,null,null],"gpt-4o-mini-transcribe-2025-03-20":[0.00000125,0.000005,null,null],"gpt-4o-mini-transcribe-2025-12-15":[0.00000125,0.000005,null,null],"gpt-5-search-api":[0.00000125,0.00001,null,1.25e-7],"gpt-5-search-api-2025-10-14":[0.00000125,0.00001,null,1.25e-7],"gpt-realtime-mini-2025-10-06":[6e-7,0.0000024,null,6e-8],"gpt-realtime-mini-2025-12-15":[6e-7,0.0000024,null,6e-8],"gemini-2.0-flash-exp-image-generation":[0,0,null,null],"gemini-2.5-flash-native-audio-latest":[3e-7,0.0000025,null,null],"gemini-2.5-flash-native-audio-preview-09-2025":[3e-7,0.0000025,null,null],"gemini-2.5-flash-native-audio-preview-12-2025":[3e-7,0.0000025,null,null],"gemini-3.1-flash-live-preview":[7.5e-7,0.0000045,null,null],"gemini-2.5-flash-preview-tts":[3e-7,0.0000025,null,null],"gemini-flash-latest":[3e-7,0.0000025,null,3e-8],"gemini-flash-lite-latest":[1e-7,4e-7,null,1e-8],"gemini-pro-latest":[0.00000125,0.00001,null,1.25e-7],"gemini-exp-1206":[3e-7,0.0000025,null,3e-8],"anyscale/HuggingFaceH4/zephyr-7b-beta":[1.5e-7,1.5e-7,null,null],"HuggingFaceH4/zephyr-7b-beta":[1.5e-7,1.5e-7,null,null],"anyscale/codellama/CodeLlama-34b-Instruct-hf":[0.000001,0.000001,null,null],"codellama/CodeLlama-34b-Instruct-hf":[0.000001,0.000001,null,null],"anyscale/codellama/CodeLlama-70b-Instruct-hf":[0.000001,0.000001,null,null],"codellama/CodeLlama-70b-Instruct-hf":[0.000001,0.000001,null,null],"anyscale/google/gemma-7b-it":[1.5e-7,1.5e-7,null,null],"google/gemma-7b-it":[1.5e-7,1.5e-7,null,null],"anyscale/meta-llama/Llama-2-13b-chat-hf":[2.5e-7,2.5e-7,null,null],"meta-llama/Llama-2-13b-chat-hf":[2.5e-7,2.5e-7,null,null],"anyscale/meta-llama/Llama-2-70b-chat-hf":[0.000001,0.000001,null,null],"meta-llama/Llama-2-70b-chat-hf":[0.000001,0.000001,null,null],"anyscale/meta-llama/Llama-2-7b-chat-hf":[1.5e-7,1.5e-7,null,null],"meta-llama/Llama-2-7b-chat-hf":[1.5e-7,1.5e-7,null,null],"anyscale/meta-llama/Meta-Llama-3-70B-Instruct":[0.000001,0.000001,null,null],"meta-llama/Meta-Llama-3-70B-Instruct":[0.000001,0.000001,null,null],"anyscale/meta-llama/Meta-Llama-3-8B-Instruct":[1.5e-7,1.5e-7,null,null],"meta-llama/Meta-Llama-3-8B-Instruct":[1.5e-7,1.5e-7,null,null],"anyscale/mistralai/Mistral-7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"mistralai/Mistral-7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"anyscale/mistralai/Mixtral-8x22B-Instruct-v0.1":[9e-7,9e-7,null,null],"mistralai/Mixtral-8x22B-Instruct-v0.1":[9e-7,9e-7,null,null],"anyscale/mistralai/Mixtral-8x7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"mistralai/Mixtral-8x7B-Instruct-v0.1":[1.5e-7,1.5e-7,null,null],"azure/ada":[1e-7,0,null,null],"ada":[1e-7,0,null,null],"azure/codex-mini":[0.0000015,0.000006,null,3.75e-7],"codex-mini":[0.0000015,0.000006,null,3.75e-7],"azure/command-r-plus":[0.000003,0.000015,null,null],"azure_ai/claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"azure_ai/claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"azure_ai/claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"azure_ai/claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"azure_ai/claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"azure/computer-use-preview":[0.000003,0.000012,null,null],"azure_ai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"gpt-oss-120b":[1.5e-7,6e-7,null,null],"azure_ai/model_router":[1.4e-7,0,null,null],"model_router":[1.4e-7,0,null,null],"azure/eu/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"eu/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"azure/eu/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"eu/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"azure/eu/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"eu/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"azure/eu/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"eu/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"azure/eu/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"eu/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"azure/eu/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"eu/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"azure/eu/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"eu/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"azure/eu/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"eu/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"azure/eu/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"eu/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"azure/eu/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"eu/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"azure/eu/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"eu/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"azure/eu/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"eu/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"azure/eu/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"eu/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"azure/eu/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"eu/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"azure/eu/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"eu/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"azure/global-standard/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"global-standard/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/global-standard/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"global-standard/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"azure/global-standard/gpt-4o-mini":[1.5e-7,6e-7,null,null],"global-standard/gpt-4o-mini":[1.5e-7,6e-7,null,null],"azure/global/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"global/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/global/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"global/gpt-4o-2024-11-20":[0.0000025,0.00001,null,0.00000125],"azure/global/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"global/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"azure/global/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"global/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-3.5-turbo":[5e-7,0.0000015,null,null],"azure/gpt-3.5-turbo-0125":[5e-7,0.0000015,null,null],"azure/gpt-3.5-turbo-instruct-0914":[0.0000015,0.000002,null,null],"azure/gpt-35-turbo":[5e-7,0.0000015,null,null],"gpt-35-turbo":[5e-7,0.0000015,null,null],"azure/gpt-35-turbo-0125":[5e-7,0.0000015,null,null],"gpt-35-turbo-0125":[5e-7,0.0000015,null,null],"azure/gpt-35-turbo-1106":[0.000001,0.000002,null,null],"gpt-35-turbo-1106":[0.000001,0.000002,null,null],"azure/gpt-35-turbo-16k":[0.000003,0.000004,null,null],"gpt-35-turbo-16k":[0.000003,0.000004,null,null],"azure/gpt-35-turbo-16k-0613":[0.000003,0.000004,null,null],"gpt-35-turbo-16k-0613":[0.000003,0.000004,null,null],"azure/gpt-35-turbo-instruct":[0.0000015,0.000002,null,null],"gpt-35-turbo-instruct":[0.0000015,0.000002,null,null],"azure/gpt-35-turbo-instruct-0914":[0.0000015,0.000002,null,null],"gpt-35-turbo-instruct-0914":[0.0000015,0.000002,null,null],"azure/gpt-4":[0.00003,0.00006,null,null],"azure/gpt-4-0125-preview":[0.00001,0.00003,null,null],"azure/gpt-4-0613":[0.00003,0.00006,null,null],"azure/gpt-4-1106-preview":[0.00001,0.00003,null,null],"azure/gpt-4-32k":[0.00006,0.00012,null,null],"gpt-4-32k":[0.00006,0.00012,null,null],"azure/gpt-4-32k-0613":[0.00006,0.00012,null,null],"gpt-4-32k-0613":[0.00006,0.00012,null,null],"azure/gpt-4-turbo":[0.00001,0.00003,null,null],"azure/gpt-4-turbo-2024-04-09":[0.00001,0.00003,null,null],"azure/gpt-4-turbo-vision-preview":[0.00001,0.00003,null,null],"gpt-4-turbo-vision-preview":[0.00001,0.00003,null,null],"azure/gpt-4.1":[0.000002,0.000008,null,5e-7],"azure/gpt-4.1-2025-04-14":[0.000002,0.000008,null,5e-7],"azure/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"azure/gpt-4.1-mini-2025-04-14":[4e-7,0.0000016,null,1e-7],"azure/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"azure/gpt-4.1-nano-2025-04-14":[1e-7,4e-7,null,2.5e-8],"azure/gpt-4.5-preview":[0.000075,0.00015,null,0.0000375],"gpt-4.5-preview":[0.000075,0.00015,null,0.0000375],"azure/gpt-4o":[0.0000025,0.00001,null,0.00000125],"azure/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"azure/gpt-4o-2024-08-06":[0.0000025,0.00001,null,0.00000125],"azure/gpt-4o-2024-11-20":[0.00000275,0.000011,null,0.00000125],"azure/gpt-audio-2025-08-28":[0.0000025,0.00001,null,null],"azure/gpt-audio-1.5-2026-02-23":[0.0000025,0.00001,null,null],"gpt-audio-1.5-2026-02-23":[0.0000025,0.00001,null,null],"azure/gpt-audio-mini-2025-10-06":[6e-7,0.0000024,null,null],"azure/gpt-4o-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"azure/gpt-4o-mini":[1.65e-7,6.6e-7,null,7.5e-8],"azure/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,7.5e-8],"azure/gpt-4o-mini-audio-preview-2024-12-17":[0.0000025,0.00001,null,null],"azure/gpt-4o-mini-realtime-preview-2024-12-17":[6e-7,0.0000024,null,3e-7],"azure/gpt-realtime-2025-08-28":[0.000004,0.000016,null,0.000004],"azure/gpt-realtime-1.5-2026-02-23":[0.000004,0.000016,null,0.000004],"gpt-realtime-1.5-2026-02-23":[0.000004,0.000016,null,0.000004],"azure/gpt-realtime-mini-2025-10-06":[6e-7,0.0000024,null,6e-8],"azure/gpt-4o-mini-transcribe":[0.00000125,0.000005,null,null],"azure/gpt-4o-mini-tts":[0.0000025,0.00001,null,null],"azure/gpt-4o-realtime-preview-2024-10-01":[0.000005,0.00002,null,0.0000025],"gpt-4o-realtime-preview-2024-10-01":[0.000005,0.00002,null,0.0000025],"azure/gpt-4o-realtime-preview-2024-12-17":[0.000005,0.00002,null,0.0000025],"azure/gpt-4o-transcribe":[0.0000025,0.00001,null,null],"azure/gpt-4o-transcribe-diarize":[0.0000025,0.00001,null,null],"azure/gpt-5.1-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-chat-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-codex-2025-11-13":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-mini-2025-11-13":[2.5e-7,0.000002,null,2.5e-8],"gpt-5.1-codex-mini-2025-11-13":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-2025-08-07":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-chat-latest":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5-mini-2025-08-07":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5-nano":[5e-8,4e-7,null,5e-9],"azure/gpt-5-nano-2025-08-07":[5e-8,4e-7,null,5e-9],"azure/gpt-5-pro":[0.000015,0.00012,null,null],"azure/gpt-5.1":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"gpt-5.1-chat":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"azure/gpt-5.1-codex-mini":[2.5e-7,0.000002,null,2.5e-8],"azure/gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-chat-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"gpt-5.2-chat-2025-12-11":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.3-chat":[0.00000175,0.000014,null,1.75e-7],"gpt-5.3-chat":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.3-codex":[0.00000175,0.000014,null,1.75e-7],"azure/gpt-5.2-pro":[0.000021,0.000168,null,null],"azure/gpt-5.2-pro-2025-12-11":[0.000021,0.000168,null,null],"azure/gpt-5.4":[0.0000025,0.000015,null,2.5e-7],"azure/gpt-5.4-2026-03-05":[0.0000025,0.000015,null,2.5e-7],"azure/gpt-5.4-pro":[0.00003,0.00018,null,0.000003],"azure/gpt-5.4-pro-2026-03-05":[0.00003,0.00018,null,0.000003],"azure/gpt-5.5":[0.000005,0.00003,null,5e-7],"azure/gpt-5.5-2026-04-23":[0.000005,0.00003,null,5e-7],"azure/gpt-5.5-pro":[0.00003,0.00018,null,0.000003],"azure/gpt-5.5-pro-2026-04-23":[0.00003,0.00018,null,0.000003],"azure/gpt-5.4-mini":[7.5e-7,0.0000045,null,7.5e-8],"azure/gpt-5.4-mini-2026-03-17":[7.5e-7,0.0000045,null,7.5e-8],"azure/gpt-5.4-nano":[2e-7,0.00000125,null,2e-8],"azure/gpt-5.4-nano-2026-03-17":[2e-7,0.00000125,null,2e-8],"azure/gpt-image-2":[0.000005,0.00001,null,0.00000125],"azure/gpt-image-2-2026-04-21":[0.000005,0.00001,null,0.00000125],"azure/mistral-large-2402":[0.000008,0.000024,null,null],"mistral-large-2402":[0.000008,0.000024,null,null],"azure/mistral-large-latest":[0.000008,0.000024,null,null],"mistral-large-latest":[0.000008,0.000024,null,null],"azure/o1":[0.000015,0.00006,null,0.0000075],"azure/o1-2024-12-17":[0.000015,0.00006,null,0.0000075],"azure/o1-mini":[0.00000121,0.00000484,null,6.05e-7],"o1-mini":[0.00000121,0.00000484,null,6.05e-7],"azure/o1-mini-2024-09-12":[0.0000011,0.0000044,null,5.5e-7],"o1-mini-2024-09-12":[0.0000011,0.0000044,null,5.5e-7],"azure/o1-preview":[0.000015,0.00006,null,0.0000075],"o1-preview":[0.000015,0.00006,null,0.0000075],"azure/o1-preview-2024-09-12":[0.000015,0.00006,null,0.0000075],"o1-preview-2024-09-12":[0.000015,0.00006,null,0.0000075],"azure/o3":[0.000002,0.000008,null,5e-7],"azure/o3-2025-04-16":[0.000002,0.000008,null,5e-7],"azure/o3-deep-research":[0.00001,0.00004,null,0.0000025],"azure/o3-mini":[0.0000011,0.0000044,null,5.5e-7],"azure/o3-mini-2025-01-31":[0.0000011,0.0000044,null,5.5e-7],"azure/o3-pro":[0.00002,0.00008,null,null],"azure/o3-pro-2025-06-10":[0.00002,0.00008,null,null],"azure/o4-mini":[0.0000011,0.0000044,null,2.75e-7],"azure/o4-mini-2025-04-16":[0.0000011,0.0000044,null,2.75e-7],"azure/text-embedding-3-large":[1.3e-7,0,null,null],"azure/text-embedding-3-small":[2e-8,0,null,null],"azure/text-embedding-ada-002":[1e-7,0,null,null],"azure/us/gpt-4.1-2025-04-14":[0.0000022,0.0000088,null,5.5e-7],"us/gpt-4.1-2025-04-14":[0.0000022,0.0000088,null,5.5e-7],"azure/us/gpt-4.1-mini-2025-04-14":[4.4e-7,0.00000176,null,1.1e-7],"us/gpt-4.1-mini-2025-04-14":[4.4e-7,0.00000176,null,1.1e-7],"azure/us/gpt-4.1-nano-2025-04-14":[1.1e-7,4.4e-7,null,2.5e-8],"us/gpt-4.1-nano-2025-04-14":[1.1e-7,4.4e-7,null,2.5e-8],"azure/us/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"us/gpt-4o-2024-08-06":[0.00000275,0.000011,null,0.000001375],"azure/us/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"us/gpt-4o-2024-11-20":[0.00000275,0.000011,0.00000138,null],"azure/us/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"us/gpt-4o-mini-2024-07-18":[1.65e-7,6.6e-7,null,8.3e-8],"azure/us/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"us/gpt-4o-mini-realtime-preview-2024-12-17":[6.6e-7,0.00000264,null,3.3e-7],"azure/us/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"us/gpt-4o-realtime-preview-2024-10-01":[0.0000055,0.000022,null,0.00000275],"azure/us/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"us/gpt-4o-realtime-preview-2024-12-17":[0.0000055,0.000022,null,0.00000275],"azure/us/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"us/gpt-5-2025-08-07":[0.000001375,0.000011,null,1.375e-7],"azure/us/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"us/gpt-5-mini-2025-08-07":[2.75e-7,0.0000022,null,2.75e-8],"azure/us/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"us/gpt-5-nano-2025-08-07":[5.5e-8,4.4e-7,null,5.5e-9],"azure/us/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1-chat":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"us/gpt-5.1-codex":[0.00000138,0.000011,null,1.4e-7],"azure/us/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"us/gpt-5.1-codex-mini":[2.75e-7,0.0000022,null,2.8e-8],"azure/us/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"us/o1-2024-12-17":[0.0000165,0.000066,null,0.00000825],"azure/us/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"us/o1-mini-2024-09-12":[0.00000121,0.00000484,null,6.05e-7],"azure/us/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"us/o1-preview-2024-09-12":[0.0000165,0.000066,null,0.00000825],"azure/us/o3-2025-04-16":[0.0000022,0.0000088,null,5.5e-7],"us/o3-2025-04-16":[0.0000022,0.0000088,null,5.5e-7],"azure/us/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"us/o3-mini-2025-01-31":[0.00000121,0.00000484,null,6.05e-7],"azure/us/o4-mini-2025-04-16":[0.00000121,0.00000484,null,3.1e-7],"us/o4-mini-2025-04-16":[0.00000121,0.00000484,null,3.1e-7],"azure_ai/Cohere-embed-v3-english":[1e-7,0,null,null],"Cohere-embed-v3-english":[1e-7,0,null,null],"azure_ai/Cohere-embed-v3-multilingual":[1e-7,0,null,null],"Cohere-embed-v3-multilingual":[1e-7,0,null,null],"azure_ai/Llama-3.2-11B-Vision-Instruct":[3.7e-7,3.7e-7,null,null],"Llama-3.2-11B-Vision-Instruct":[3.7e-7,3.7e-7,null,null],"azure_ai/Llama-3.2-90B-Vision-Instruct":[0.00000204,0.00000204,null,null],"Llama-3.2-90B-Vision-Instruct":[0.00000204,0.00000204,null,null],"azure_ai/Llama-3.3-70B-Instruct":[7.1e-7,7.1e-7,null,null],"Llama-3.3-70B-Instruct":[7.1e-7,7.1e-7,null,null],"azure_ai/Llama-4-Maverick-17B-128E-Instruct-FP8":[0.00000141,3.5e-7,null,null],"Llama-4-Maverick-17B-128E-Instruct-FP8":[0.00000141,3.5e-7,null,null],"azure_ai/Llama-4-Scout-17B-16E-Instruct":[2e-7,7.8e-7,null,null],"Llama-4-Scout-17B-16E-Instruct":[2e-7,7.8e-7,null,null],"azure_ai/Meta-Llama-3-70B-Instruct":[0.0000011,3.7e-7,null,null],"Meta-Llama-3-70B-Instruct":[0.0000011,3.7e-7,null,null],"azure_ai/Meta-Llama-3.1-405B-Instruct":[0.00000533,0.000016,null,null],"Meta-Llama-3.1-405B-Instruct":[0.00000533,0.000016,null,null],"azure_ai/Meta-Llama-3.1-70B-Instruct":[0.00000268,0.00000354,null,null],"Meta-Llama-3.1-70B-Instruct":[0.00000268,0.00000354,null,null],"azure_ai/Meta-Llama-3.1-8B-Instruct":[3e-7,6.1e-7,null,null],"Meta-Llama-3.1-8B-Instruct":[3e-7,6.1e-7,null,null],"azure_ai/Phi-3-medium-128k-instruct":[1.7e-7,6.8e-7,null,null],"Phi-3-medium-128k-instruct":[1.7e-7,6.8e-7,null,null],"azure_ai/Phi-3-medium-4k-instruct":[1.7e-7,6.8e-7,null,null],"Phi-3-medium-4k-instruct":[1.7e-7,6.8e-7,null,null],"azure_ai/Phi-3-mini-128k-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3-mini-128k-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3-mini-4k-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3-mini-4k-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3-small-128k-instruct":[1.5e-7,6e-7,null,null],"Phi-3-small-128k-instruct":[1.5e-7,6e-7,null,null],"azure_ai/Phi-3-small-8k-instruct":[1.5e-7,6e-7,null,null],"Phi-3-small-8k-instruct":[1.5e-7,6e-7,null,null],"azure_ai/Phi-3.5-MoE-instruct":[1.6e-7,6.4e-7,null,null],"Phi-3.5-MoE-instruct":[1.6e-7,6.4e-7,null,null],"azure_ai/Phi-3.5-mini-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3.5-mini-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-3.5-vision-instruct":[1.3e-7,5.2e-7,null,null],"Phi-3.5-vision-instruct":[1.3e-7,5.2e-7,null,null],"azure_ai/Phi-4":[1.25e-7,5e-7,null,null],"Phi-4":[1.25e-7,5e-7,null,null],"azure_ai/Phi-4-mini-instruct":[7.5e-8,3e-7,null,null],"Phi-4-mini-instruct":[7.5e-8,3e-7,null,null],"azure_ai/Phi-4-multimodal-instruct":[8e-8,3.2e-7,null,null],"Phi-4-multimodal-instruct":[8e-8,3.2e-7,null,null],"azure_ai/Phi-4-mini-reasoning":[8e-8,3.2e-7,null,null],"Phi-4-mini-reasoning":[8e-8,3.2e-7,null,null],"azure_ai/Phi-4-reasoning":[1.25e-7,5e-7,null,null],"Phi-4-reasoning":[1.25e-7,5e-7,null,null],"azure_ai/MAI-DS-R1":[0.00000135,0.0000054,null,null],"MAI-DS-R1":[0.00000135,0.0000054,null,null],"azure_ai/cohere-rerank-v3-english":[0,0,null,null],"cohere-rerank-v3-english":[0,0,null,null],"azure_ai/cohere-rerank-v3-multilingual":[0,0,null,null],"cohere-rerank-v3-multilingual":[0,0,null,null],"azure_ai/cohere-rerank-v3.5":[0,0,null,null],"cohere-rerank-v3.5":[0,0,null,null],"azure_ai/cohere-rerank-v4.0-pro":[0,0,null,null],"cohere-rerank-v4.0-pro":[0,0,null,null],"azure_ai/cohere-rerank-v4.0-fast":[0,0,null,null],"cohere-rerank-v4.0-fast":[0,0,null,null],"azure_ai/deepseek-v3.2":[5.8e-7,0.00000168,null,null],"deepseek-v3.2":[5.8e-7,0.00000168,null,null],"azure_ai/deepseek-v3.2-speciale":[5.8e-7,0.00000168,null,null],"deepseek-v3.2-speciale":[5.8e-7,0.00000168,null,null],"azure_ai/deepseek-r1":[0.00000135,0.0000054,null,null],"deepseek-r1":[0.00000135,0.0000054,null,null],"azure_ai/deepseek-v3":[0.00000114,0.00000456,null,null],"deepseek-v3":[0.00000114,0.00000456,null,null],"azure_ai/deepseek-v3-0324":[0.00000114,0.00000456,null,null],"deepseek-v3-0324":[0.00000114,0.00000456,null,null],"azure_ai/embed-v-4-0":[1.2e-7,0,null,null],"embed-v-4-0":[1.2e-7,0,null,null],"azure_ai/global/grok-3":[0.000003,0.000015,null,null],"global/grok-3":[0.000003,0.000015,null,null],"azure_ai/global/grok-3-mini":[2.5e-7,0.00000127,null,null],"global/grok-3-mini":[2.5e-7,0.00000127,null,null],"azure_ai/grok-3":[0.000003,0.000015,null,null],"grok-3":[0.000003,0.000015,null,null],"azure_ai/grok-3-mini":[2.5e-7,0.00000127,null,null],"grok-3-mini":[2.5e-7,0.00000127,null,null],"azure_ai/grok-4":[0.000003,0.000015,null,null],"grok-4":[0.000003,0.000015,null,null],"azure_ai/grok-4-fast-non-reasoning":[2e-7,5e-7,null,null],"grok-4-fast-non-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-fast-reasoning":[2e-7,5e-7,null,null],"grok-4-fast-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,null],"grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-4-1-fast-reasoning":[2e-7,5e-7,null,null],"grok-4-1-fast-reasoning":[2e-7,5e-7,null,null],"azure_ai/grok-code-fast-1":[2e-7,0.0000015,null,null],"grok-code-fast-1":[2e-7,0.0000015,null,null],"azure_ai/jais-30b-chat":[0.0032,0.00971,null,null],"jais-30b-chat":[0.0032,0.00971,null,null],"azure_ai/jamba-instruct":[5e-7,7e-7,null,null],"jamba-instruct":[5e-7,7e-7,null,null],"azure_ai/kimi-k2.5":[6e-7,0.000003,null,null],"kimi-k2.5":[6e-7,0.000003,null,null],"azure_ai/ministral-3b":[4e-8,4e-8,null,null],"ministral-3b":[4e-8,4e-8,null,null],"azure_ai/mistral-large":[0.000004,0.000012,null,null],"mistral-large":[0.000004,0.000012,null,null],"azure_ai/mistral-large-2407":[0.000002,0.000006,null,null],"mistral-large-2407":[0.000002,0.000006,null,null],"azure_ai/mistral-large-latest":[0.000002,0.000006,null,null],"azure_ai/mistral-large-3":[5e-7,0.0000015,null,null],"mistral-large-3":[5e-7,0.0000015,null,null],"azure_ai/mistral-medium-2505":[4e-7,0.000002,null,null],"mistral-medium-2505":[4e-7,0.000002,null,null],"azure_ai/mistral-nemo":[1.5e-7,1.5e-7,null,null],"mistral-nemo":[1.5e-7,1.5e-7,null,null],"azure_ai/mistral-small":[0.000001,0.000003,null,null],"mistral-small":[0.000001,0.000003,null,null],"azure_ai/mistral-small-2503":[1e-7,3e-7,null,null],"mistral-small-2503":[1e-7,3e-7,null,null],"bedrock/ap-northeast-1/anthropic.claude-instant-v1":[0.00000223,0.00000755,null,null],"ap-northeast-1/anthropic.claude-instant-v1":[0.00000223,0.00000755,null,null],"bedrock/ap-northeast-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"ap-northeast-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/ap-northeast-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"ap-northeast-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/ap-northeast-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-northeast-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-northeast-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-northeast-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-northeast-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-northeast-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-northeast-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"ap-northeast-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/ap-northeast-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-northeast-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-northeast-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-northeast-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/moonshotai.kimi-k2.5":[6e-7,0.00000303,null,null],"bedrock/ap-south-1/meta.llama3-70b-instruct-v1:0":[0.00000318,0.0000042,null,null],"ap-south-1/meta.llama3-70b-instruct-v1:0":[0.00000318,0.0000042,null,null],"bedrock/ap-south-1/meta.llama3-8b-instruct-v1:0":[3.6e-7,7.2e-7,null,null],"ap-south-1/meta.llama3-8b-instruct-v1:0":[3.6e-7,7.2e-7,null,null],"bedrock/ap-south-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-south-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-south-1/moonshotai.kimi-k2-thinking":[7.1e-7,0.00000294,null,null],"ap-south-1/moonshotai.kimi-k2-thinking":[7.1e-7,0.00000294,null,null],"bedrock/ap-south-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-south-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/ap-southeast-2/minimax.minimax-m2.5":[3.09e-7,0.000001236,null,null],"ap-southeast-2/minimax.minimax-m2.5":[3.09e-7,0.000001236,null,null],"bedrock/ap-southeast-3/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"ap-southeast-3/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/ap-southeast-3/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"ap-southeast-3/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/ap-southeast-3/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"ap-southeast-3/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/ap-southeast-3/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"ap-southeast-3/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/ap-southeast-3/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"ap-southeast-3/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/ca-central-1/meta.llama3-70b-instruct-v1:0":[0.00000305,0.00000403,null,null],"ca-central-1/meta.llama3-70b-instruct-v1:0":[0.00000305,0.00000403,null,null],"bedrock/ca-central-1/meta.llama3-8b-instruct-v1:0":[3.5e-7,6.9e-7,null,null],"ca-central-1/meta.llama3-8b-instruct-v1:0":[3.5e-7,6.9e-7,null,null],"bedrock/eu-north-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"eu-north-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/eu-north-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-north-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-north-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-north-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-north-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"eu-north-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/eu-central-1/anthropic.claude-instant-v1":[0.00000248,0.00000838,null,null],"eu-central-1/anthropic.claude-instant-v1":[0.00000248,0.00000838,null,null],"bedrock/eu-central-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"eu-central-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/eu-central-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"eu-central-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/eu-central-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-central-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-central-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-central-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-central-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-central-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/eu-west-1/meta.llama3-70b-instruct-v1:0":[0.00000286,0.00000378,null,null],"eu-west-1/meta.llama3-70b-instruct-v1:0":[0.00000286,0.00000378,null,null],"bedrock/eu-west-1/meta.llama3-8b-instruct-v1:0":[3.2e-7,6.5e-7,null,null],"eu-west-1/meta.llama3-8b-instruct-v1:0":[3.2e-7,6.5e-7,null,null],"bedrock/eu-west-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-west-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-west-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-west-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-west-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-west-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/eu-west-2/meta.llama3-70b-instruct-v1:0":[0.00000345,0.00000455,null,null],"eu-west-2/meta.llama3-70b-instruct-v1:0":[0.00000345,0.00000455,null,null],"bedrock/eu-west-2/meta.llama3-8b-instruct-v1:0":[3.9e-7,7.8e-7,null,null],"eu-west-2/meta.llama3-8b-instruct-v1:0":[3.9e-7,7.8e-7,null,null],"bedrock/eu-west-2/minimax.minimax-m2.1":[4.7e-7,0.00000186,null,null],"eu-west-2/minimax.minimax-m2.1":[4.7e-7,0.00000186,null,null],"bedrock/eu-west-2/minimax.minimax-m2.5":[4.7e-7,0.00000186,null,null],"eu-west-2/minimax.minimax-m2.5":[4.7e-7,0.00000186,null,null],"bedrock/eu-west-2/qwen.qwen3-coder-next":[7.8e-7,0.00000186,null,null],"eu-west-2/qwen.qwen3-coder-next":[7.8e-7,0.00000186,null,null],"bedrock/eu-west-3/mistral.mistral-7b-instruct-v0:2":[2e-7,2.6e-7,null,null],"eu-west-3/mistral.mistral-7b-instruct-v0:2":[2e-7,2.6e-7,null,null],"bedrock/eu-west-3/mistral.mistral-large-2402-v1:0":[0.0000104,0.0000312,null,null],"eu-west-3/mistral.mistral-large-2402-v1:0":[0.0000104,0.0000312,null,null],"bedrock/eu-west-3/mistral.mixtral-8x7b-instruct-v0:1":[5.9e-7,9.1e-7,null,null],"eu-west-3/mistral.mixtral-8x7b-instruct-v0:1":[5.9e-7,9.1e-7,null,null],"bedrock/eu-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"eu-south-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/eu-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"eu-south-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/eu-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"eu-south-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/invoke/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"invoke/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.000003,0.000015,0.00000375,3e-7],"bedrock/sa-east-1/meta.llama3-70b-instruct-v1:0":[0.00000445,0.00000588,null,null],"sa-east-1/meta.llama3-70b-instruct-v1:0":[0.00000445,0.00000588,null,null],"bedrock/sa-east-1/meta.llama3-8b-instruct-v1:0":[5e-7,0.00000101,null,null],"sa-east-1/meta.llama3-8b-instruct-v1:0":[5e-7,0.00000101,null,null],"bedrock/sa-east-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"sa-east-1/deepseek.v3.2":[7.4e-7,0.00000222,null,null],"bedrock/sa-east-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"sa-east-1/minimax.minimax-m2.1":[3.6e-7,0.00000144,null,null],"bedrock/sa-east-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"sa-east-1/minimax.minimax-m2.5":[3.6e-7,0.00000144,null,null],"bedrock/sa-east-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"sa-east-1/moonshotai.kimi-k2-thinking":[7.3e-7,0.00000303,null,null],"bedrock/sa-east-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"sa-east-1/moonshotai.kimi-k2.5":[7.2e-7,0.0000036,null,null],"bedrock/sa-east-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"sa-east-1/qwen.qwen3-coder-next":[6e-7,0.00000144,null,null],"bedrock/us-east-1/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"us-east-1/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"bedrock/us-east-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"us-east-1/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/us-east-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"us-east-1/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/us-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"us-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"bedrock/us-east-1/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"us-east-1/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"bedrock/us-east-1/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"us-east-1/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"bedrock/us-east-1/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"us-east-1/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"bedrock/us-east-1/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-east-1/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-east-1/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-east-1/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-east-1/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-east-1/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-east-1/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-east-1/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-east-1/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-east-1/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-east-1/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-east-1/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us-east-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-east-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-east-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-east-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-east-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-east-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-east-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-east-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-east-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-east-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-east-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-east-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us-gov-east-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"us-gov-east-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"bedrock/us-gov-east-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"us-gov-east-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"bedrock/us-gov-east-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"us-gov-east-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"bedrock/us-gov-east-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"us-gov-east-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"bedrock/us-gov-east-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"us-gov-east-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"bedrock/us-gov-east-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"us-gov-east-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"bedrock/us-gov-east-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-east-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-east-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"us-gov-east-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"bedrock/us-gov-east-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-east-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-east-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-east-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-gov-east-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-gov-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"us-gov-east-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"bedrock/us-gov-west-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"us-gov-west-1/amazon.nova-pro-v1:0":[9.6e-7,0.00000384,null,null],"bedrock/us-gov-west-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"us-gov-west-1/amazon.titan-embed-text-v1":[1e-7,0,null,null],"bedrock/us-gov-west-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"us-gov-west-1/amazon.titan-embed-text-v2:0":[2e-7,0,null,null],"bedrock/us-gov-west-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"us-gov-west-1/amazon.titan-text-express-v1":[0.0000013,0.0000017,null,null],"bedrock/us-gov-west-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"us-gov-west-1/amazon.titan-text-lite-v1":[3e-7,4e-7,null,null],"bedrock/us-gov-west-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"us-gov-west-1/amazon.titan-text-premier-v1:0":[5e-7,0.0000015,null,null],"bedrock/us-gov-west-1/anthropic.claude-3-7-sonnet-20250219-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-west-1/anthropic.claude-3-7-sonnet-20250219-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-west-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"us-gov-west-1/anthropic.claude-3-5-sonnet-20240620-v1:0":[0.0000036,0.000018,0.0000045,3.6e-7],"bedrock/us-gov-west-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"us-gov-west-1/anthropic.claude-3-haiku-20240307-v1:0":[3e-7,0.0000015,3.75e-7,3e-8],"bedrock/us-gov-west-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-west-1/anthropic.claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-west-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"us-gov-west-1/claude-sonnet-4-5-20250929-v1:0":[0.0000033,0.0000165,0.000004125,3.3e-7],"bedrock/us-gov-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-gov-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-gov-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"us-gov-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,0.00000265,null,null],"bedrock/us-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"us-west-1/meta.llama3-70b-instruct-v1:0":[0.00000265,0.0000035,null,null],"bedrock/us-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"us-west-1/meta.llama3-8b-instruct-v1:0":[3e-7,6e-7,null,null],"bedrock/us-west-2/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"us-west-2/anthropic.claude-instant-v1":[8e-7,0.0000024,null,null],"bedrock/us-west-2/anthropic.claude-v1":[0.000008,0.000024,null,null],"us-west-2/anthropic.claude-v1":[0.000008,0.000024,null,null],"bedrock/us-west-2/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"us-west-2/anthropic.claude-v2:1":[0.000008,0.000024,null,null],"bedrock/us-west-2/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"us-west-2/mistral.mistral-7b-instruct-v0:2":[1.5e-7,2e-7,null,null],"bedrock/us-west-2/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"us-west-2/mistral.mistral-large-2402-v1:0":[0.000008,0.000024,null,null],"bedrock/us-west-2/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"us-west-2/mistral.mixtral-8x7b-instruct-v0:1":[4.5e-7,7e-7,null,null],"bedrock/us-west-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"us-west-2/deepseek.v3.2":[6.2e-7,0.00000185,null,null],"bedrock/us-west-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"us-west-2/minimax.minimax-m2.1":[3e-7,0.0000012,null,null],"bedrock/us-west-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"us-west-2/minimax.minimax-m2.5":[3e-7,0.0000012,null,null],"bedrock/us-west-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"us-west-2/moonshotai.kimi-k2-thinking":[6e-7,0.0000025,null,null],"bedrock/us-west-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"us-west-2/moonshotai.kimi-k2.5":[6e-7,0.000003,null,null],"bedrock/us-west-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"us-west-2/qwen.qwen3-coder-next":[5e-7,0.0000012,null,null],"bedrock/us.anthropic.claude-3-5-haiku-20241022-v1:0":[8e-7,0.000004,0.000001,8e-8],"cerebras/llama-3.3-70b":[8.5e-7,0.0000012,null,null],"llama-3.3-70b":[8.5e-7,0.0000012,null,null],"cerebras/llama3.1-70b":[6e-7,6e-7,null,null],"llama3.1-70b":[6e-7,6e-7,null,null],"cerebras/llama3.1-8b":[1e-7,1e-7,null,null],"llama3.1-8b":[1e-7,1e-7,null,null],"cerebras/gpt-oss-120b":[3.5e-7,7.5e-7,null,null],"cerebras/qwen-3-32b":[4e-7,8e-7,null,null],"qwen-3-32b":[4e-7,8e-7,null,null],"cerebras/zai-glm-4.6":[0.00000225,0.00000275,null,null],"zai-glm-4.6":[0.00000225,0.00000275,null,null],"cerebras/zai-glm-4.7":[0.00000225,0.00000275,null,null],"zai-glm-4.7":[0.00000225,0.00000275,null,null],"cloudflare/@cf/meta/llama-2-7b-chat-fp16":[0.000001923,0.000001923,null,null],"@cf/meta/llama-2-7b-chat-fp16":[0.000001923,0.000001923,null,null],"cloudflare/@cf/meta/llama-2-7b-chat-int8":[0.000001923,0.000001923,null,null],"@cf/meta/llama-2-7b-chat-int8":[0.000001923,0.000001923,null,null],"cloudflare/@cf/mistral/mistral-7b-instruct-v0.1":[0.000001923,0.000001923,null,null],"@cf/mistral/mistral-7b-instruct-v0.1":[0.000001923,0.000001923,null,null],"cloudflare/@hf/thebloke/codellama-7b-instruct-awq":[0.000001923,0.000001923,null,null],"@hf/thebloke/codellama-7b-instruct-awq":[0.000001923,0.000001923,null,null],"codestral/codestral-2405":[0,0,null,null],"codestral-2405":[0,0,null,null],"codestral/codestral-latest":[0,0,null,null],"codestral-latest":[0,0,null,null],"cohere/embed-v4.0":[1.2e-7,0,null,null],"embed-v4.0":[1.2e-7,0,null,null],"dashscope/qwen-coder":[3e-7,0.0000015,null,null],"qwen-coder":[3e-7,0.0000015,null,null],"dashscope/qwen-max":[0.0000016,0.0000064,null,null],"qwen-max":[0.0000016,0.0000064,null,null],"dashscope/qwen-plus":[4e-7,0.0000012,null,null],"qwen-plus":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-01-25":[4e-7,0.0000012,null,null],"qwen-plus-2025-01-25":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-04-28":[4e-7,0.0000012,null,null],"qwen-plus-2025-04-28":[4e-7,0.0000012,null,null],"dashscope/qwen-plus-2025-07-14":[4e-7,0.0000012,null,null],"qwen-plus-2025-07-14":[4e-7,0.0000012,null,null],"dashscope/qwen-turbo":[5e-8,2e-7,null,null],"qwen-turbo":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-2024-11-01":[5e-8,2e-7,null,null],"qwen-turbo-2024-11-01":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-2025-04-28":[5e-8,2e-7,null,null],"qwen-turbo-2025-04-28":[5e-8,2e-7,null,null],"dashscope/qwen-turbo-latest":[5e-8,2e-7,null,null],"qwen-turbo-latest":[5e-8,2e-7,null,null],"dashscope/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000012,null,null],"qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000012,null,null],"dashscope/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000012,null,null],"qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000012,null,null],"dashscope/qwen3-vl-235b-a22b-instruct":[4e-7,0.0000016,null,null],"qwen3-vl-235b-a22b-instruct":[4e-7,0.0000016,null,null],"dashscope/qwen3-vl-235b-a22b-thinking":[4e-7,0.000004,null,null],"qwen3-vl-235b-a22b-thinking":[4e-7,0.000004,null,null],"dashscope/qwen3-vl-32b-instruct":[1.6e-7,6.4e-7,null,null],"qwen3-vl-32b-instruct":[1.6e-7,6.4e-7,null,null],"dashscope/qwen3-vl-32b-thinking":[1.6e-7,0.00000287,null,null],"qwen3-vl-32b-thinking":[1.6e-7,0.00000287,null,null],"dashscope/qwq-plus":[8e-7,0.0000024,null,null],"qwq-plus":[8e-7,0.0000024,null,null],"databricks/databricks-bge-large-en":[1.0003e-7,0,null,null],"databricks-bge-large-en":[1.0003e-7,0,null,null],"databricks/databricks-claude-3-7-sonnet":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-3-7-sonnet":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-haiku-4-5":[0.00000100002,0.00000500003,null,null],"databricks-claude-haiku-4-5":[0.00000100002,0.00000500003,null,null],"databricks/databricks-claude-opus-4":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks-claude-opus-4":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks/databricks-claude-opus-4-1":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks-claude-opus-4-1":[0.000015000020000000002,0.00007500003000000001,null,null],"databricks/databricks-claude-opus-4-5":[0.00000500003,0.000025000010000000002,null,null],"databricks-claude-opus-4-5":[0.00000500003,0.000025000010000000002,null,null],"databricks/databricks-claude-sonnet-4":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-sonnet-4-1":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4-1":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-claude-sonnet-4-5":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks-claude-sonnet-4-5":[0.0000029999900000000002,0.000015000020000000002,null,null],"databricks/databricks-gemini-2-5-flash":[3.0001999999999996e-7,0.00000249998,null,null],"databricks-gemini-2-5-flash":[3.0001999999999996e-7,0.00000249998,null,null],"databricks/databricks-gemini-2-5-pro":[0.00000124999,0.000009999990000000002,null,null],"databricks-gemini-2-5-pro":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gemma-3-12b":[1.5000999999999998e-7,5.0001e-7,null,null],"databricks-gemma-3-12b":[1.5000999999999998e-7,5.0001e-7,null,null],"databricks/databricks-gpt-5":[0.00000124999,0.000009999990000000002,null,null],"databricks-gpt-5":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gpt-5-1":[0.00000124999,0.000009999990000000002,null,null],"databricks-gpt-5-1":[0.00000124999,0.000009999990000000002,null,null],"databricks/databricks-gpt-5-mini":[2.4997000000000006e-7,0.0000019999700000000004,null,null],"databricks-gpt-5-mini":[2.4997000000000006e-7,0.0000019999700000000004,null,null],"databricks/databricks-gpt-5-nano":[4.998e-8,3.9998000000000007e-7,null,null],"databricks-gpt-5-nano":[4.998e-8,3.9998000000000007e-7,null,null],"databricks/databricks-gpt-oss-120b":[1.5000999999999998e-7,5.9997e-7,null,null],"databricks-gpt-oss-120b":[1.5000999999999998e-7,5.9997e-7,null,null],"databricks/databricks-gpt-oss-20b":[7e-8,3.0001999999999996e-7,null,null],"databricks-gpt-oss-20b":[7e-8,3.0001999999999996e-7,null,null],"databricks/databricks-gte-large-en":[1.2999000000000001e-7,0,null,null],"databricks-gte-large-en":[1.2999000000000001e-7,0,null,null],"databricks/databricks-llama-2-70b-chat":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-llama-2-70b-chat":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-llama-4-maverick":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-llama-4-maverick":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-meta-llama-3-1-405b-instruct":[0.00000500003,0.000015000020000000002,null,null],"databricks-meta-llama-3-1-405b-instruct":[0.00000500003,0.000015000020000000002,null,null],"databricks/databricks-meta-llama-3-1-8b-instruct":[1.5000999999999998e-7,4.5003000000000007e-7,null,null],"databricks-meta-llama-3-1-8b-instruct":[1.5000999999999998e-7,4.5003000000000007e-7,null,null],"databricks/databricks-meta-llama-3-3-70b-instruct":[5.0001e-7,0.0000015000300000000002,null,null],"databricks-meta-llama-3-3-70b-instruct":[5.0001e-7,0.0000015000300000000002,null,null],"databricks/databricks-meta-llama-3-70b-instruct":[0.00000100002,0.0000029999900000000002,null,null],"databricks-meta-llama-3-70b-instruct":[0.00000100002,0.0000029999900000000002,null,null],"databricks/databricks-mixtral-8x7b-instruct":[5.0001e-7,0.00000100002,null,null],"databricks-mixtral-8x7b-instruct":[5.0001e-7,0.00000100002,null,null],"databricks/databricks-mpt-30b-instruct":[0.00000100002,0.00000100002,null,null],"databricks-mpt-30b-instruct":[0.00000100002,0.00000100002,null,null],"databricks/databricks-mpt-7b-instruct":[5.0001e-7,0,null,null],"databricks-mpt-7b-instruct":[5.0001e-7,0,null,null],"deepinfra/Gryphe/MythoMax-L2-13b":[8e-8,9e-8,null,null],"Gryphe/MythoMax-L2-13b":[8e-8,9e-8,null,null],"deepinfra/NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000001,null,null],"NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000001,null,null],"deepinfra/NousResearch/Hermes-3-Llama-3.1-70B":[3e-7,3e-7,null,null],"NousResearch/Hermes-3-Llama-3.1-70B":[3e-7,3e-7,null,null],"deepinfra/Qwen/QwQ-32B":[1.5e-7,4e-7,null,null],"Qwen/QwQ-32B":[1.5e-7,4e-7,null,null],"deepinfra/Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3.9e-7,null,null],"Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3.9e-7,null,null],"deepinfra/Qwen/Qwen2.5-7B-Instruct":[4e-8,1e-7,null,null],"Qwen/Qwen2.5-7B-Instruct":[4e-8,1e-7,null,null],"deepinfra/Qwen/Qwen2.5-VL-32B-Instruct":[2e-7,6e-7,null,null],"Qwen/Qwen2.5-VL-32B-Instruct":[2e-7,6e-7,null,null],"deepinfra/Qwen/Qwen3-14B":[6e-8,2.4e-7,null,null],"Qwen/Qwen3-14B":[6e-8,2.4e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B":[1.8e-7,5.4e-7,null,null],"Qwen/Qwen3-235B-A22B":[1.8e-7,5.4e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B-Instruct-2507":[9e-8,6e-7,null,null],"Qwen/Qwen3-235B-A22B-Instruct-2507":[9e-8,6e-7,null,null],"deepinfra/Qwen/Qwen3-235B-A22B-Thinking-2507":[3e-7,0.0000029,null,null],"Qwen/Qwen3-235B-A22B-Thinking-2507":[3e-7,0.0000029,null,null],"deepinfra/Qwen/Qwen3-30B-A3B":[8e-8,2.9e-7,null,null],"Qwen/Qwen3-30B-A3B":[8e-8,2.9e-7,null,null],"deepinfra/Qwen/Qwen3-32B":[1e-7,2.8e-7,null,null],"Qwen/Qwen3-32B":[1e-7,2.8e-7,null,null],"deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct":[4e-7,0.0000016,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct":[4e-7,0.0000016,null,null],"deepinfra/Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo":[2.9e-7,0.0000012,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct-Turbo":[2.9e-7,0.0000012,null,null],"deepinfra/Qwen/Qwen3-Next-80B-A3B-Instruct":[1.4e-7,0.0000014,null,null],"Qwen/Qwen3-Next-80B-A3B-Instruct":[1.4e-7,0.0000014,null,null],"deepinfra/Qwen/Qwen3-Next-80B-A3B-Thinking":[1.4e-7,0.0000014,null,null],"Qwen/Qwen3-Next-80B-A3B-Thinking":[1.4e-7,0.0000014,null,null],"deepinfra/Sao10K/L3-8B-Lunaris-v1-Turbo":[4e-8,5e-8,null,null],"Sao10K/L3-8B-Lunaris-v1-Turbo":[4e-8,5e-8,null,null],"deepinfra/Sao10K/L3.1-70B-Euryale-v2.2":[6.5e-7,7.5e-7,null,null],"Sao10K/L3.1-70B-Euryale-v2.2":[6.5e-7,7.5e-7,null,null],"deepinfra/Sao10K/L3.3-70B-Euryale-v2.3":[6.5e-7,7.5e-7,null,null],"Sao10K/L3.3-70B-Euryale-v2.3":[6.5e-7,7.5e-7,null,null],"deepinfra/allenai/olmOCR-7B-0725-FP8":[2.7e-7,0.0000015,null,null],"allenai/olmOCR-7B-0725-FP8":[2.7e-7,0.0000015,null,null],"deepinfra/anthropic/claude-3-7-sonnet-latest":[0.0000033,0.0000165,null,3.3e-7],"anthropic/claude-3-7-sonnet-latest":[0.0000033,0.0000165,null,3.3e-7],"deepinfra/anthropic/claude-4-opus":[0.0000165,0.0000825,null,null],"anthropic/claude-4-opus":[0.0000165,0.0000825,null,null],"deepinfra/anthropic/claude-4-sonnet":[0.0000033,0.0000165,null,null],"anthropic/claude-4-sonnet":[0.0000033,0.0000165,null,null],"deepinfra/deepseek-ai/DeepSeek-R1":[7e-7,0.0000024,null,null],"deepseek-ai/DeepSeek-R1":[7e-7,0.0000024,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-0528":[5e-7,0.00000215,null,4e-7],"deepseek-ai/DeepSeek-R1-0528":[5e-7,0.00000215,null,4e-7],"deepinfra/deepseek-ai/DeepSeek-R1-0528-Turbo":[0.000001,0.000003,null,null],"deepseek-ai/DeepSeek-R1-0528-Turbo":[0.000001,0.000003,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2e-7,6e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2e-7,6e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[2.7e-7,2.7e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[2.7e-7,2.7e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-R1-Turbo":[0.000001,0.000003,null,null],"deepseek-ai/DeepSeek-R1-Turbo":[0.000001,0.000003,null,null],"deepinfra/deepseek-ai/DeepSeek-V3":[3.8e-7,8.9e-7,null,null],"deepseek-ai/DeepSeek-V3":[3.8e-7,8.9e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-V3-0324":[2.5e-7,8.8e-7,null,null],"deepseek-ai/DeepSeek-V3-0324":[2.5e-7,8.8e-7,null,null],"deepinfra/deepseek-ai/DeepSeek-V3.1":[2.7e-7,0.000001,null,2.16e-7],"deepseek-ai/DeepSeek-V3.1":[2.7e-7,0.000001,null,2.16e-7],"deepinfra/deepseek-ai/DeepSeek-V3.1-Terminus":[2.7e-7,0.000001,null,2.16e-7],"deepseek-ai/DeepSeek-V3.1-Terminus":[2.7e-7,0.000001,null,2.16e-7],"deepinfra/google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"deepinfra/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"deepinfra/google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"deepinfra/google/gemma-3-12b-it":[5e-8,1e-7,null,null],"google/gemma-3-12b-it":[5e-8,1e-7,null,null],"deepinfra/google/gemma-3-27b-it":[9e-8,1.6e-7,null,null],"google/gemma-3-27b-it":[9e-8,1.6e-7,null,null],"deepinfra/google/gemma-3-4b-it":[4e-8,8e-8,null,null],"google/gemma-3-4b-it":[4e-8,8e-8,null,null],"deepinfra/meta-llama/Llama-3.2-11B-Vision-Instruct":[4.9e-8,4.9e-8,null,null],"meta-llama/Llama-3.2-11B-Vision-Instruct":[4.9e-8,4.9e-8,null,null],"deepinfra/meta-llama/Llama-3.2-3B-Instruct":[2e-8,2e-8,null,null],"meta-llama/Llama-3.2-3B-Instruct":[2e-8,2e-8,null,null],"deepinfra/meta-llama/Llama-3.3-70B-Instruct":[2.3e-7,4e-7,null,null],"meta-llama/Llama-3.3-70B-Instruct":[2.3e-7,4e-7,null,null],"deepinfra/meta-llama/Llama-3.3-70B-Instruct-Turbo":[1.3e-7,3.9e-7,null,null],"meta-llama/Llama-3.3-70B-Instruct-Turbo":[1.3e-7,3.9e-7,null,null],"deepinfra/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[1.5e-7,6e-7,null,null],"meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[1.5e-7,6e-7,null,null],"deepinfra/meta-llama/Llama-4-Scout-17B-16E-Instruct":[8e-8,3e-7,null,null],"meta-llama/Llama-4-Scout-17B-16E-Instruct":[8e-8,3e-7,null,null],"deepinfra/meta-llama/Llama-Guard-3-8B":[5.5e-8,5.5e-8,null,null],"meta-llama/Llama-Guard-3-8B":[5.5e-8,5.5e-8,null,null],"deepinfra/meta-llama/Llama-Guard-4-12B":[1.8e-7,1.8e-7,null,null],"meta-llama/Llama-Guard-4-12B":[1.8e-7,1.8e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3-8B-Instruct":[3e-8,6e-8,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct":[4e-7,4e-7,null,null],"meta-llama/Meta-Llama-3.1-70B-Instruct":[4e-7,4e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[1e-7,2.8e-7,null,null],"meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[1e-7,2.8e-7,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct":[3e-8,5e-8,null,null],"meta-llama/Meta-Llama-3.1-8B-Instruct":[3e-8,5e-8,null,null],"deepinfra/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[2e-8,3e-8,null,null],"meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[2e-8,3e-8,null,null],"deepinfra/microsoft/WizardLM-2-8x22B":[4.8e-7,4.8e-7,null,null],"microsoft/WizardLM-2-8x22B":[4.8e-7,4.8e-7,null,null],"deepinfra/microsoft/phi-4":[7e-8,1.4e-7,null,null],"microsoft/phi-4":[7e-8,1.4e-7,null,null],"deepinfra/mistralai/Mistral-Nemo-Instruct-2407":[2e-8,4e-8,null,null],"mistralai/Mistral-Nemo-Instruct-2407":[2e-8,4e-8,null,null],"deepinfra/mistralai/Mistral-Small-24B-Instruct-2501":[5e-8,8e-8,null,null],"mistralai/Mistral-Small-24B-Instruct-2501":[5e-8,8e-8,null,null],"deepinfra/mistralai/Mistral-Small-3.2-24B-Instruct-2506":[7.5e-8,2e-7,null,null],"mistralai/Mistral-Small-3.2-24B-Instruct-2506":[7.5e-8,2e-7,null,null],"deepinfra/mistralai/Mixtral-8x7B-Instruct-v0.1":[4e-7,4e-7,null,null],"deepinfra/moonshotai/Kimi-K2-Instruct":[5e-7,0.000002,null,null],"moonshotai/Kimi-K2-Instruct":[5e-7,0.000002,null,null],"deepinfra/moonshotai/Kimi-K2-Instruct-0905":[5e-7,0.000002,null,4e-7],"moonshotai/Kimi-K2-Instruct-0905":[5e-7,0.000002,null,4e-7],"deepinfra/nvidia/Llama-3.1-Nemotron-70B-Instruct":[6e-7,6e-7,null,null],"nvidia/Llama-3.1-Nemotron-70B-Instruct":[6e-7,6e-7,null,null],"deepinfra/nvidia/Llama-3.3-Nemotron-Super-49B-v1.5":[1e-7,4e-7,null,null],"nvidia/Llama-3.3-Nemotron-Super-49B-v1.5":[1e-7,4e-7,null,null],"deepinfra/nvidia/NVIDIA-Nemotron-Nano-9B-v2":[4e-8,1.6e-7,null,null],"nvidia/NVIDIA-Nemotron-Nano-9B-v2":[4e-8,1.6e-7,null,null],"deepinfra/openai/gpt-oss-120b":[5e-8,4.5e-7,null,null],"openai/gpt-oss-120b":[5e-8,4.5e-7,null,null],"deepinfra/openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"deepinfra/zai-org/GLM-4.5":[4e-7,0.0000016,null,null],"zai-org/GLM-4.5":[4e-7,0.0000016,null,null],"deepseek/deepseek-chat":[2.8e-7,4.2e-7,0,2.8e-8],"deepseek/deepseek-coder":[1.4e-7,2.8e-7,null,null],"deepseek-coder":[1.4e-7,2.8e-7,null,null],"deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"deepseek/deepseek-reasoner":[2.8e-7,4.2e-7,null,2.8e-8],"deepseek/deepseek-v3":[2.7e-7,0.0000011,0,7e-8],"deepseek/deepseek-v3.2":[2.8e-7,4e-7,null,null],"fireworks_ai/WhereIsAI/UAE-Large-V1":[1.6e-8,0,null,null],"WhereIsAI/UAE-Large-V1":[1.6e-8,0,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-coder-v2-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1":[0.000003,0.000008,null,null],"accounts/fireworks/models/deepseek-r1":[0.000003,0.000008,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-0528":[0.000003,0.000008,null,null],"accounts/fireworks/models/deepseek-r1-0528":[0.000003,0.000008,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-basic":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/deepseek-r1-basic":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-v3":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3-0324":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-v3-0324":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p1":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p1":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p1-terminus":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p1-terminus":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v3p2":[5.6e-7,0.00000168,null,null],"accounts/fireworks/models/deepseek-v3p2":[5.6e-7,0.00000168,null,null],"fireworks_ai/accounts/fireworks/models/firefunction-v2":[9e-7,9e-7,null,null],"accounts/fireworks/models/firefunction-v2":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/glm-4p5":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5-air":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/glm-4p5-air":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p6":[5.5e-7,0.00000219,null,null],"accounts/fireworks/models/glm-4p6":[5.5e-7,0.00000219,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p7":[6e-7,0.0000022,null,3e-7],"accounts/fireworks/models/glm-4p7":[6e-7,0.0000022,null,3e-7],"fireworks_ai/accounts/fireworks/models/gpt-oss-120b":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/gpt-oss-120b":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-20b":[5e-8,2e-7,null,null],"accounts/fireworks/models/gpt-oss-20b":[5e-8,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-instruct":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-instruct":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-instruct-0905":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-instruct-0905":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2-thinking":[6e-7,0.0000025,null,null],"accounts/fireworks/models/kimi-k2-thinking":[6e-7,0.0000025,null,null],"fireworks_ai/accounts/fireworks/models/kimi-k2p5":[6e-7,0.000003,null,1e-7],"accounts/fireworks/models/kimi-k2p5":[6e-7,0.000003,null,1e-7],"fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct":[0.000003,0.000003,null,null],"accounts/fireworks/models/llama-v3p1-405b-instruct":[0.000003,0.000003,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-8b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-8b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-11b-vision-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3p2-11b-vision-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-1b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-1b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-3b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-3b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-90b-vision-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p2-90b-vision-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama4-maverick-instruct-basic":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/llama4-maverick-instruct-basic":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama4-scout-instruct-basic":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/llama4-scout-instruct-basic":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"accounts/fireworks/models/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct-hf":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b-instruct-hf":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-large":[0.000003,0.000003,null,null],"accounts/fireworks/models/yi-large":[0.000003,0.000003,null,null],"fireworks_ai/glm-4p7":[6e-7,0.0000022,null,3e-7],"glm-4p7":[6e-7,0.0000022,null,3e-7],"fireworks_ai/kimi-k2p5":[6e-7,0.000003,null,1e-7],"kimi-k2p5":[6e-7,0.000003,null,1e-7],"fireworks_ai/minimax-m2p1":[3e-7,0.0000012,null,3e-8],"minimax-m2p1":[3e-7,0.0000012,null,3e-8],"fireworks_ai/nomic-ai/nomic-embed-text-v1":[8e-9,0,null,null],"nomic-ai/nomic-embed-text-v1":[8e-9,0,null,null],"fireworks_ai/nomic-ai/nomic-embed-text-v1.5":[8e-9,0,null,null],"nomic-ai/nomic-embed-text-v1.5":[8e-9,0,null,null],"fireworks_ai/thenlper/gte-base":[8e-9,0,null,null],"thenlper/gte-base":[8e-9,0,null,null],"fireworks_ai/thenlper/gte-large":[1.6e-8,0,null,null],"thenlper/gte-large":[1.6e-8,0,null,null],"friendliai/meta-llama-3.1-70b-instruct":[6e-7,6e-7,null,null],"meta-llama-3.1-70b-instruct":[6e-7,6e-7,null,null],"friendliai/meta-llama-3.1-8b-instruct":[1e-7,1e-7,null,null],"meta-llama-3.1-8b-instruct":[1e-7,1e-7,null,null],"gemini/gemini-live-2.5-flash-preview-native-audio-09-2025":[3e-7,0.000002,null,7.5e-8],"vertex_ai/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"vertex_ai/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"vertex_ai/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"vertex_ai/gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini/gemini-robotics-er-1.5-preview":[3e-7,0.0000025,null,0],"vertex_ai/gemini-embedding-2-preview":[2e-7,0,null,null],"vertex_ai/gemini-embedding-2":[2e-7,0,null,null],"gemini/gemini-embedding-001":[1.5e-7,0,null,null],"gemini/gemini-embedding-2-preview":[2e-7,0,null,null],"gemini/gemini-embedding-2":[2e-7,0,null,null],"gemini/gemini-1.5-flash":[7.5e-8,0,null,null],"gemini-1.5-flash":[7.5e-8,0,null,null],"gemini/gemini-2.0-flash":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.0-flash-001":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,1.875e-8],"gemini/gemini-2.5-flash":[3e-7,0.0000025,null,3e-8],"gemini/gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"gemini/gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"gemini/gemini-3.1-flash-image-preview":[2.5e-7,0.0000015,null,null],"gemini/deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"gemini/gemini-2.5-flash-lite":[1e-7,4e-7,null,1e-8],"gemini/gemini-2.5-flash-lite-preview-09-2025":[1e-7,4e-7,null,1e-8],"gemini/gemini-2.5-flash-preview-09-2025":[3e-7,0.0000025,null,7.5e-8],"gemini/gemini-flash-latest":[3e-7,0.0000025,null,7.5e-8],"gemini/gemini-flash-lite-latest":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.5-flash-lite-preview-06-17":[1e-7,4e-7,null,2.5e-8],"gemini/gemini-2.5-flash-preview-tts":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-pro":[0.00000125,0.00001,null,1.25e-7],"gemini/gemini-2.5-computer-use-preview-10-2025":[0.00000125,0.00001,null,null],"gemini/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"gemini/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"gemini/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"gemini/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"gemini/gemini-3.1-pro-preview-customtools":[0.000002,0.000012,null,2e-7],"gemini/gemini-2.5-pro-preview-tts":[0.00000125,0.00001,null,1.25e-7],"gemini/gemini-exp-1114":[0,0,null,null],"gemini-exp-1114":[0,0,null,null],"gemini/gemini-exp-1206":[0,0,null,null],"gemini/gemini-gemma-2-27b-it":[3.5e-7,0.00000105,null,null],"gemini-gemma-2-27b-it":[3.5e-7,0.00000105,null,null],"gemini/gemini-gemma-2-9b-it":[3.5e-7,0.00000105,null,null],"gemini-gemma-2-9b-it":[3.5e-7,0.00000105,null,null],"gemini/gemma-3-27b-it":[0,0,null,null],"gemma-3-27b-it":[0,0,null,null],"gemini/learnlm-1.5-pro-experimental":[0,0,null,null],"learnlm-1.5-pro-experimental":[0,0,null,null],"gemini/lyria-3-clip-preview":[0,0,null,null],"lyria-3-clip-preview":[0,0,null,null],"gemini/lyria-3-pro-preview":[0,0,null,null],"lyria-3-pro-preview":[0,0,null,null],"gigachat/GigaChat-2-Lite":[0,0,null,null],"GigaChat-2-Lite":[0,0,null,null],"gigachat/GigaChat-2-Max":[0,0,null,null],"GigaChat-2-Max":[0,0,null,null],"gigachat/GigaChat-2-Pro":[0,0,null,null],"GigaChat-2-Pro":[0,0,null,null],"gigachat/Embeddings":[0,0,null,null],"Embeddings":[0,0,null,null],"gigachat/Embeddings-2":[0,0,null,null],"Embeddings-2":[0,0,null,null],"gigachat/EmbeddingsGigaR":[0,0,null,null],"EmbeddingsGigaR":[0,0,null,null],"gmi/anthropic/claude-opus-4.5":[0.000005,0.000025,null,null],"anthropic/claude-opus-4.5":[0.000005,0.000025,null,null],"gmi/anthropic/claude-sonnet-4.5":[0.000003,0.000015,null,null],"anthropic/claude-sonnet-4.5":[0.000003,0.000015,null,null],"gmi/anthropic/claude-sonnet-4":[0.000003,0.000015,null,null],"anthropic/claude-sonnet-4":[0.000003,0.000015,null,null],"gmi/anthropic/claude-opus-4":[0.000015,0.000075,null,null],"anthropic/claude-opus-4":[0.000015,0.000075,null,null],"gmi/openai/gpt-5.2":[0.00000175,0.000014,null,null],"openai/gpt-5.2":[0.00000175,0.000014,null,null],"gmi/openai/gpt-5.1":[0.00000125,0.00001,null,null],"openai/gpt-5.1":[0.00000125,0.00001,null,null],"gmi/openai/gpt-5":[0.00000125,0.00001,null,null],"openai/gpt-5":[0.00000125,0.00001,null,null],"gmi/openai/gpt-4o":[0.0000025,0.00001,null,null],"openai/gpt-4o":[0.0000025,0.00001,null,null],"gmi/openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"gmi/deepseek-ai/DeepSeek-V3.2":[2.8e-7,4e-7,null,null],"deepseek-ai/DeepSeek-V3.2":[2.8e-7,4e-7,null,null],"gmi/deepseek-ai/DeepSeek-V3-0324":[2.8e-7,8.8e-7,null,null],"gmi/google/gemini-3-pro-preview":[0.000002,0.000012,null,null],"google/gemini-3-pro-preview":[0.000002,0.000012,null,null],"gmi/google/gemini-3-flash-preview":[5e-7,0.000003,null,null],"google/gemini-3-flash-preview":[5e-7,0.000003,null,null],"gmi/moonshotai/Kimi-K2-Thinking":[8e-7,0.0000012,null,null],"moonshotai/Kimi-K2-Thinking":[8e-7,0.0000012,null,null],"gmi/MiniMaxAI/MiniMax-M2.1":[3e-7,0.0000012,null,null],"MiniMaxAI/MiniMax-M2.1":[3e-7,0.0000012,null,null],"baseten/MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"baseten/nvidia/Nemotron-120B-A12B":[3e-7,7.5e-7,null,null],"nvidia/Nemotron-120B-A12B":[3e-7,7.5e-7,null,null],"baseten/zai-org/GLM-5":[9.5e-7,0.00000315,null,null],"zai-org/GLM-5":[9.5e-7,0.00000315,null,null],"baseten/zai-org/GLM-4.7":[6e-7,0.0000022,null,null],"zai-org/GLM-4.7":[6e-7,0.0000022,null,null],"baseten/zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"baseten/moonshotai/Kimi-K2.5":[6e-7,0.000003,null,null],"moonshotai/Kimi-K2.5":[6e-7,0.000003,null,null],"baseten/moonshotai/Kimi-K2-Thinking":[6e-7,0.0000025,null,null],"baseten/moonshotai/Kimi-K2-Instruct-0905":[6e-7,0.0000025,null,null],"baseten/openai/gpt-oss-120b":[1e-7,5e-7,null,null],"baseten/deepseek-ai/DeepSeek-V3.1":[5e-7,0.0000015,null,null],"baseten/deepseek-ai/DeepSeek-V3-0324":[7.7e-7,7.7e-7,null,null],"gmi/Qwen/Qwen3-VL-235B-A22B-Instruct-FP8":[3e-7,0.0000014,null,null],"Qwen/Qwen3-VL-235B-A22B-Instruct-FP8":[3e-7,0.0000014,null,null],"gmi/zai-org/GLM-4.7-FP8":[4e-7,0.000002,null,null],"zai-org/GLM-4.7-FP8":[4e-7,0.000002,null,null],"gradient_ai/anthropic-claude-3-opus":[0.000015,0.000075,null,null],"anthropic-claude-3-opus":[0.000015,0.000075,null,null],"gradient_ai/anthropic-claude-3.5-haiku":[8e-7,0.000004,null,null],"anthropic-claude-3.5-haiku":[8e-7,0.000004,null,null],"gradient_ai/anthropic-claude-3.5-sonnet":[0.000003,0.000015,null,null],"anthropic-claude-3.5-sonnet":[0.000003,0.000015,null,null],"gradient_ai/anthropic-claude-3.7-sonnet":[0.000003,0.000015,null,null],"anthropic-claude-3.7-sonnet":[0.000003,0.000015,null,null],"gradient_ai/deepseek-r1-distill-llama-70b":[9.9e-7,9.9e-7,null,null],"deepseek-r1-distill-llama-70b":[9.9e-7,9.9e-7,null,null],"gradient_ai/llama3-8b-instruct":[2e-7,2e-7,null,null],"llama3-8b-instruct":[2e-7,2e-7,null,null],"gradient_ai/llama3.3-70b-instruct":[6.5e-7,6.5e-7,null,null],"llama3.3-70b-instruct":[6.5e-7,6.5e-7,null,null],"gradient_ai/mistral-nemo-instruct-2407":[3e-7,3e-7,null,null],"mistral-nemo-instruct-2407":[3e-7,3e-7,null,null],"gradient_ai/openai-o3":[0.000002,0.000008,null,null],"openai-o3":[0.000002,0.000008,null,null],"gradient_ai/openai-o3-mini":[0.0000011,0.0000044,null,null],"openai-o3-mini":[0.0000011,0.0000044,null,null],"lemonade/Qwen3-Coder-30B-A3B-Instruct-GGUF":[0,0,null,null],"Qwen3-Coder-30B-A3B-Instruct-GGUF":[0,0,null,null],"lemonade/gpt-oss-20b-mxfp4-GGUF":[0,0,null,null],"gpt-oss-20b-mxfp4-GGUF":[0,0,null,null],"lemonade/gpt-oss-120b-mxfp-GGUF":[0,0,null,null],"gpt-oss-120b-mxfp-GGUF":[0,0,null,null],"lemonade/Gemma-3-4b-it-GGUF":[0,0,null,null],"Gemma-3-4b-it-GGUF":[0,0,null,null],"lemonade/Qwen3-4B-Instruct-2507-GGUF":[0,0,null,null],"Qwen3-4B-Instruct-2507-GGUF":[0,0,null,null],"amazon-nova/nova-micro-v1":[3.5e-8,1.4e-7,null,null],"nova-micro-v1":[3.5e-8,1.4e-7,null,null],"amazon-nova/nova-lite-v1":[6e-8,2.4e-7,null,null],"nova-lite-v1":[6e-8,2.4e-7,null,null],"amazon-nova/nova-premier-v1":[0.0000025,0.0000125,null,null],"nova-premier-v1":[0.0000025,0.0000125,null,null],"amazon-nova/nova-pro-v1":[8e-7,0.0000032,null,null],"nova-pro-v1":[8e-7,0.0000032,null,null],"groq/llama-3.1-8b-instant":[5e-8,8e-8,null,null],"llama-3.1-8b-instant":[5e-8,8e-8,null,null],"groq/llama-3.3-70b-versatile":[5.9e-7,7.9e-7,null,null],"llama-3.3-70b-versatile":[5.9e-7,7.9e-7,null,null],"groq/gemma-7b-it":[5e-8,8e-8,null,null],"gemma-7b-it":[5e-8,8e-8,null,null],"groq/meta-llama/llama-guard-4-12b":[2e-7,2e-7,null,null],"meta-llama/llama-guard-4-12b":[2e-7,2e-7,null,null],"groq/meta-llama/llama-4-maverick-17b-128e-instruct":[2e-7,6e-7,null,null],"meta-llama/llama-4-maverick-17b-128e-instruct":[2e-7,6e-7,null,null],"groq/meta-llama/llama-4-scout-17b-16e-instruct":[1.1e-7,3.4e-7,null,null],"meta-llama/llama-4-scout-17b-16e-instruct":[1.1e-7,3.4e-7,null,null],"groq/moonshotai/kimi-k2-instruct-0905":[0.000001,0.000003,null,5e-7],"moonshotai/kimi-k2-instruct-0905":[0.000001,0.000003,null,5e-7],"groq/openai/gpt-oss-120b":[1.5e-7,6e-7,null,7.5e-8],"groq/openai/gpt-oss-20b":[7.5e-8,3e-7,null,3.75e-8],"groq/openai/gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,3.7e-8],"openai/gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,3.7e-8],"groq/qwen/qwen3-32b":[2.9e-7,5.9e-7,null,null],"qwen/qwen3-32b":[2.9e-7,5.9e-7,null,null],"hyperbolic/NousResearch/Hermes-3-Llama-3.1-70B":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/QwQ-32B":[2e-7,2e-7,null,null],"hyperbolic/Qwen/Qwen2.5-72B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/Qwen2.5-Coder-32B-Instruct":[1.2e-7,3e-7,null,null],"Qwen/Qwen2.5-Coder-32B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/Qwen/Qwen3-235B-A22B":[0.000002,0.000002,null,null],"hyperbolic/deepseek-ai/DeepSeek-R1":[4e-7,4e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-R1-0528":[2.5e-7,2.5e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-V3":[2e-7,2e-7,null,null],"hyperbolic/deepseek-ai/DeepSeek-V3-0324":[4e-7,4e-7,null,null],"hyperbolic/meta-llama/Llama-3.2-3B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Llama-3.3-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-405B-Instruct":[1.2e-7,3e-7,null,null],"meta-llama/Meta-Llama-3.1-405B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-70B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/meta-llama/Meta-Llama-3.1-8B-Instruct":[1.2e-7,3e-7,null,null],"hyperbolic/moonshotai/Kimi-K2-Instruct":[0.000002,0.000002,null,null],"crusoe/deepseek-ai/DeepSeek-R1-0528":[0.000003,0.000007,null,null],"crusoe/deepseek-ai/DeepSeek-V3-0324":[0.0000015,0.0000015,null,null],"crusoe/google/gemma-3-12b-it":[1e-7,1e-7,null,null],"crusoe/meta-llama/Llama-3.3-70B-Instruct":[2e-7,2e-7,null,null],"crusoe/moonshotai/Kimi-K2-Thinking":[0.0000025,0.0000025,null,null],"crusoe/openai/gpt-oss-120b":[8e-7,8e-7,null,null],"crusoe/Qwen/Qwen3-235B-A22B-Instruct-2507":[0.000003,0.000003,null,null],"lambda_ai/deepseek-llama3.3-70b":[2e-7,6e-7,null,null],"deepseek-llama3.3-70b":[2e-7,6e-7,null,null],"lambda_ai/deepseek-r1-0528":[2e-7,6e-7,null,null],"deepseek-r1-0528":[2e-7,6e-7,null,null],"lambda_ai/deepseek-r1-671b":[8e-7,8e-7,null,null],"deepseek-r1-671b":[8e-7,8e-7,null,null],"lambda_ai/deepseek-v3-0324":[2e-7,6e-7,null,null],"lambda_ai/hermes3-405b":[8e-7,8e-7,null,null],"hermes3-405b":[8e-7,8e-7,null,null],"lambda_ai/hermes3-70b":[1.2e-7,3e-7,null,null],"hermes3-70b":[1.2e-7,3e-7,null,null],"lambda_ai/hermes3-8b":[2.5e-8,4e-8,null,null],"hermes3-8b":[2.5e-8,4e-8,null,null],"lambda_ai/lfm-40b":[1e-7,2e-7,null,null],"lfm-40b":[1e-7,2e-7,null,null],"lambda_ai/lfm-7b":[2.5e-8,4e-8,null,null],"lfm-7b":[2.5e-8,4e-8,null,null],"lambda_ai/llama-4-maverick-17b-128e-instruct-fp8":[5e-8,1e-7,null,null],"llama-4-maverick-17b-128e-instruct-fp8":[5e-8,1e-7,null,null],"lambda_ai/llama-4-scout-17b-16e-instruct":[5e-8,1e-7,null,null],"llama-4-scout-17b-16e-instruct":[5e-8,1e-7,null,null],"lambda_ai/llama3.1-405b-instruct-fp8":[8e-7,8e-7,null,null],"llama3.1-405b-instruct-fp8":[8e-7,8e-7,null,null],"lambda_ai/llama3.1-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.1-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/llama3.1-8b-instruct":[2.5e-8,4e-8,null,null],"llama3.1-8b-instruct":[2.5e-8,4e-8,null,null],"lambda_ai/llama3.1-nemotron-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.1-nemotron-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/llama3.2-11b-vision-instruct":[1.5e-8,2.5e-8,null,null],"llama3.2-11b-vision-instruct":[1.5e-8,2.5e-8,null,null],"lambda_ai/llama3.2-3b-instruct":[1.5e-8,2.5e-8,null,null],"llama3.2-3b-instruct":[1.5e-8,2.5e-8,null,null],"lambda_ai/llama3.3-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"llama3.3-70b-instruct-fp8":[1.2e-7,3e-7,null,null],"lambda_ai/qwen25-coder-32b-instruct":[5e-8,1e-7,null,null],"qwen25-coder-32b-instruct":[5e-8,1e-7,null,null],"lambda_ai/qwen3-32b-fp8":[5e-8,1e-7,null,null],"qwen3-32b-fp8":[5e-8,1e-7,null,null],"minimax/MiniMax-M2.1":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2.1":[3e-7,0.0000012,3.75e-7,3e-8],"minimax/MiniMax-M2.1-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"MiniMax-M2.1-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"minimax/MiniMax-M2.5":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2.5":[3e-7,0.0000012,3.75e-7,3e-8],"minimax/MiniMax-M2.5-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"MiniMax-M2.5-lightning":[3e-7,0.0000024,3.75e-7,3e-8],"minimax/MiniMax-M2":[3e-7,0.0000012,3.75e-7,3e-8],"MiniMax-M2":[3e-7,0.0000012,3.75e-7,3e-8],"mistral/codestral-2405":[0.000001,0.000003,null,null],"mistral/codestral-2508":[3e-7,9e-7,null,null],"codestral-2508":[3e-7,9e-7,null,null],"mistral/codestral-latest":[0.000001,0.000003,null,null],"mistral/codestral-mamba-latest":[2.5e-7,2.5e-7,null,null],"codestral-mamba-latest":[2.5e-7,2.5e-7,null,null],"mistral/devstral-medium-2507":[4e-7,0.000002,null,null],"devstral-medium-2507":[4e-7,0.000002,null,null],"mistral/devstral-small-2505":[1e-7,3e-7,null,null],"devstral-small-2505":[1e-7,3e-7,null,null],"mistral/devstral-small-2507":[1e-7,3e-7,null,null],"devstral-small-2507":[1e-7,3e-7,null,null],"mistral/devstral-small-latest":[1e-7,3e-7,null,null],"devstral-small-latest":[1e-7,3e-7,null,null],"mistral/labs-devstral-small-2512":[1e-7,3e-7,null,null],"labs-devstral-small-2512":[1e-7,3e-7,null,null],"mistral/devstral-latest":[4e-7,0.000002,null,null],"devstral-latest":[4e-7,0.000002,null,null],"mistral/devstral-medium-latest":[4e-7,0.000002,null,null],"devstral-medium-latest":[4e-7,0.000002,null,null],"mistral/devstral-2512":[4e-7,0.000002,null,null],"devstral-2512":[4e-7,0.000002,null,null],"mistral/magistral-medium-2506":[0.000002,0.000005,null,null],"magistral-medium-2506":[0.000002,0.000005,null,null],"mistral/magistral-medium-2509":[0.000002,0.000005,null,null],"magistral-medium-2509":[0.000002,0.000005,null,null],"mistral/magistral-medium-1-2-2509":[0.000002,0.000005,null,null],"magistral-medium-1-2-2509":[0.000002,0.000005,null,null],"mistral/magistral-medium-latest":[0.000002,0.000005,null,null],"magistral-medium-latest":[0.000002,0.000005,null,null],"mistral/magistral-small-2506":[5e-7,0.0000015,null,null],"magistral-small-2506":[5e-7,0.0000015,null,null],"mistral/magistral-small-latest":[5e-7,0.0000015,null,null],"magistral-small-latest":[5e-7,0.0000015,null,null],"mistral/magistral-small-1-2-2509":[5e-7,0.0000015,null,null],"magistral-small-1-2-2509":[5e-7,0.0000015,null,null],"mistral/mistral-large-2402":[0.000004,0.000012,null,null],"mistral/mistral-large-2407":[0.000003,0.000009,null,null],"mistral/mistral-large-2411":[0.000002,0.000006,null,null],"mistral-large-2411":[0.000002,0.000006,null,null],"mistral/mistral-large-latest":[5e-7,0.0000015,null,null],"mistral/mistral-large-3":[5e-7,0.0000015,null,null],"mistral/mistral-large-2512":[5e-7,0.0000015,null,null],"mistral-large-2512":[5e-7,0.0000015,null,null],"mistral/mistral-medium":[0.0000027,0.0000081,null,null],"mistral-medium":[0.0000027,0.0000081,null,null],"mistral/mistral-medium-2312":[0.0000027,0.0000081,null,null],"mistral-medium-2312":[0.0000027,0.0000081,null,null],"mistral/mistral-medium-2505":[4e-7,0.000002,null,null],"mistral/mistral-medium-latest":[4e-7,0.000002,null,null],"mistral-medium-latest":[4e-7,0.000002,null,null],"mistral/mistral-medium-3-1-2508":[4e-7,0.000002,null,null],"mistral-medium-3-1-2508":[4e-7,0.000002,null,null],"mistral/mistral-small":[1e-7,3e-7,null,null],"mistral/mistral-small-latest":[6e-8,1.8e-7,null,null],"mistral-small-latest":[6e-8,1.8e-7,null,null],"mistral/mistral-small-3-2-2506":[6e-8,1.8e-7,null,null],"mistral-small-3-2-2506":[6e-8,1.8e-7,null,null],"mistral/ministral-3-3b-2512":[1e-7,1e-7,null,null],"ministral-3-3b-2512":[1e-7,1e-7,null,null],"mistral/ministral-3-8b-2512":[1.5e-7,1.5e-7,null,null],"ministral-3-8b-2512":[1.5e-7,1.5e-7,null,null],"mistral/ministral-3-14b-2512":[2e-7,2e-7,null,null],"ministral-3-14b-2512":[2e-7,2e-7,null,null],"mistral/mistral-tiny":[2.5e-7,2.5e-7,null,null],"mistral-tiny":[2.5e-7,2.5e-7,null,null],"mistral/open-codestral-mamba":[2.5e-7,2.5e-7,null,null],"open-codestral-mamba":[2.5e-7,2.5e-7,null,null],"mistral/open-mistral-7b":[2.5e-7,2.5e-7,null,null],"open-mistral-7b":[2.5e-7,2.5e-7,null,null],"mistral/open-mistral-nemo":[3e-7,3e-7,null,null],"open-mistral-nemo":[3e-7,3e-7,null,null],"mistral/open-mistral-nemo-2407":[3e-7,3e-7,null,null],"open-mistral-nemo-2407":[3e-7,3e-7,null,null],"mistral/open-mixtral-8x22b":[0.000002,0.000006,null,null],"open-mixtral-8x22b":[0.000002,0.000006,null,null],"mistral/open-mixtral-8x7b":[7e-7,7e-7,null,null],"open-mixtral-8x7b":[7e-7,7e-7,null,null],"mistral/pixtral-12b-2409":[1.5e-7,1.5e-7,null,null],"pixtral-12b-2409":[1.5e-7,1.5e-7,null,null],"mistral/pixtral-large-2411":[0.000002,0.000006,null,null],"pixtral-large-2411":[0.000002,0.000006,null,null],"mistral/pixtral-large-latest":[0.000002,0.000006,null,null],"pixtral-large-latest":[0.000002,0.000006,null,null],"moonshot/kimi-k2-0711-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-0711-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-0905-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-0905-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-turbo-preview":[0.00000115,0.000008,null,1.5e-7],"kimi-k2-turbo-preview":[0.00000115,0.000008,null,1.5e-7],"moonshot/kimi-k2.5":[6e-7,0.000003,null,1e-7],"moonshot/kimi-k2.6":[9.5e-7,0.000004,null,1.6e-7],"kimi-k2.6":[9.5e-7,0.000004,null,1.6e-7],"moonshot/kimi-latest":[0.000002,0.000005,null,1.5e-7],"kimi-latest":[0.000002,0.000005,null,1.5e-7],"moonshot/kimi-latest-128k":[0.000002,0.000005,null,1.5e-7],"kimi-latest-128k":[0.000002,0.000005,null,1.5e-7],"moonshot/kimi-latest-32k":[0.000001,0.000003,null,1.5e-7],"kimi-latest-32k":[0.000001,0.000003,null,1.5e-7],"moonshot/kimi-latest-8k":[2e-7,0.000002,null,1.5e-7],"kimi-latest-8k":[2e-7,0.000002,null,1.5e-7],"moonshot/kimi-thinking-preview":[6e-7,0.0000025,null,1.5e-7],"kimi-thinking-preview":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-thinking":[6e-7,0.0000025,null,1.5e-7],"kimi-k2-thinking":[6e-7,0.0000025,null,1.5e-7],"moonshot/kimi-k2-thinking-turbo":[0.00000115,0.000008,null,1.5e-7],"kimi-k2-thinking-turbo":[0.00000115,0.000008,null,1.5e-7],"moonshot/moonshot-v1-128k":[0.000002,0.000005,null,null],"moonshot-v1-128k":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-128k-0430":[0.000002,0.000005,null,null],"moonshot-v1-128k-0430":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-128k-vision-preview":[0.000002,0.000005,null,null],"moonshot-v1-128k-vision-preview":[0.000002,0.000005,null,null],"moonshot/moonshot-v1-32k":[0.000001,0.000003,null,null],"moonshot-v1-32k":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-32k-0430":[0.000001,0.000003,null,null],"moonshot-v1-32k-0430":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-32k-vision-preview":[0.000001,0.000003,null,null],"moonshot-v1-32k-vision-preview":[0.000001,0.000003,null,null],"moonshot/moonshot-v1-8k":[2e-7,0.000002,null,null],"moonshot-v1-8k":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-8k-0430":[2e-7,0.000002,null,null],"moonshot-v1-8k-0430":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-8k-vision-preview":[2e-7,0.000002,null,null],"moonshot-v1-8k-vision-preview":[2e-7,0.000002,null,null],"moonshot/moonshot-v1-auto":[0.000002,0.000005,null,null],"moonshot-v1-auto":[0.000002,0.000005,null,null],"morph/morph-v3-fast":[8e-7,0.0000012,null,null],"morph-v3-fast":[8e-7,0.0000012,null,null],"morph/morph-v3-large":[9e-7,0.0000019,null,null],"morph-v3-large":[9e-7,0.0000019,null,null],"nscale/Qwen/QwQ-32B":[1.8e-7,2e-7,null,null],"nscale/Qwen/Qwen2.5-Coder-32B-Instruct":[6e-8,2e-7,null,null],"nscale/Qwen/Qwen2.5-Coder-3B-Instruct":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-3B-Instruct":[1e-8,3e-8,null,null],"nscale/Qwen/Qwen2.5-Coder-7B-Instruct":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-7B-Instruct":[1e-8,3e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[3.75e-7,3.75e-7,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Llama-8B":[2.5e-8,2.5e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Llama-8B":[2.5e-8,2.5e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B":[9e-8,9e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-1.5B":[9e-8,9e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":[7e-8,7e-8,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-14B":[7e-8,7e-8,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-32B":[1.5e-7,1.5e-7,null,null],"nscale/deepseek-ai/DeepSeek-R1-Distill-Qwen-7B":[2e-7,2e-7,null,null],"deepseek-ai/DeepSeek-R1-Distill-Qwen-7B":[2e-7,2e-7,null,null],"nscale/meta-llama/Llama-3.1-8B-Instruct":[3e-8,3e-8,null,null],"meta-llama/Llama-3.1-8B-Instruct":[3e-8,3e-8,null,null],"nscale/meta-llama/Llama-3.3-70B-Instruct":[2e-7,2e-7,null,null],"nscale/meta-llama/Llama-4-Scout-17B-16E-Instruct":[9e-8,2.9e-7,null,null],"nscale/mistralai/mixtral-8x22b-instruct-v0.1":[6e-7,6e-7,null,null],"mistralai/mixtral-8x22b-instruct-v0.1":[6e-7,6e-7,null,null],"nebius/deepseek-ai/DeepSeek-R1":[8e-7,0.0000024,null,null],"nebius/deepseek-ai/DeepSeek-R1-0528":[8e-7,0.0000024,null,null],"nebius/deepseek-ai/DeepSeek-R1-Distill-Llama-70B":[2.5e-7,7.5e-7,null,null],"nebius/deepseek-ai/DeepSeek-V3":[5e-7,0.0000015,null,null],"nebius/deepseek-ai/DeepSeek-V3-0324":[5e-7,0.0000015,null,null],"nebius/google/gemma-3-27b-it":[6e-8,2e-7,null,null],"nebius/meta-llama/Llama-3.3-70B-Instruct":[1.3e-7,4e-7,null,null],"nebius/meta-llama/Llama-Guard-3-8B":[2e-8,6e-8,null,null],"nebius/meta-llama/Meta-Llama-3.1-8B-Instruct":[2e-8,6e-8,null,null],"nebius/meta-llama/Meta-Llama-3.1-70B-Instruct":[1.3e-7,4e-7,null,null],"nebius/meta-llama/Meta-Llama-3.1-405B-Instruct":[0.000001,0.000003,null,null],"nebius/mistralai/Mistral-Nemo-Instruct-2407":[4e-8,1.2e-7,null,null],"nebius/NousResearch/Hermes-3-Llama-3.1-405B":[0.000001,0.000003,null,null],"nebius/nvidia/Llama-3.1-Nemotron-Ultra-253B-v1":[6e-7,0.0000018,null,null],"nvidia/Llama-3.1-Nemotron-Ultra-253B-v1":[6e-7,0.0000018,null,null],"nebius/nvidia/Llama-3.3-Nemotron-Super-49B-v1":[1e-7,4e-7,null,null],"nvidia/Llama-3.3-Nemotron-Super-49B-v1":[1e-7,4e-7,null,null],"nebius/Qwen/Qwen3-235B-A22B":[2e-7,6e-7,null,null],"nebius/Qwen/Qwen3-32B":[1e-7,3e-7,null,null],"nebius/Qwen/Qwen3-30B-A3B":[1e-7,3e-7,null,null],"nebius/Qwen/Qwen3-14B":[8e-8,2.4e-7,null,null],"nebius/Qwen/Qwen3-4B":[8e-8,2.4e-7,null,null],"Qwen/Qwen3-4B":[8e-8,2.4e-7,null,null],"nebius/Qwen/QwQ-32B":[1.5e-7,4.5e-7,null,null],"nebius/Qwen/Qwen2.5-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2.5-32B-Instruct":[6e-8,2e-7,null,null],"Qwen/Qwen2.5-32B-Instruct":[6e-8,2e-7,null,null],"nebius/Qwen/Qwen2.5-Coder-7B":[1e-8,3e-8,null,null],"Qwen/Qwen2.5-Coder-7B":[1e-8,3e-8,null,null],"nebius/Qwen/Qwen2.5-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"Qwen/Qwen2.5-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"Qwen/Qwen2-VL-72B-Instruct":[1.3e-7,4e-7,null,null],"nebius/Qwen/Qwen2-VL-7B-Instruct":[2e-8,6e-8,null,null],"Qwen/Qwen2-VL-7B-Instruct":[2e-8,6e-8,null,null],"nebius/BAAI/bge-en-icl":[1e-8,0,null,null],"BAAI/bge-en-icl":[1e-8,0,null,null],"nebius/BAAI/bge-multilingual-gemma2":[1e-8,0,null,null],"BAAI/bge-multilingual-gemma2":[1e-8,0,null,null],"nebius/intfloat/e5-mistral-7b-instruct":[1e-8,0,null,null],"intfloat/e5-mistral-7b-instruct":[1e-8,0,null,null],"oci/meta.llama-3.1-405b-instruct":[0.00001068,0.00001068,null,null],"meta.llama-3.1-405b-instruct":[0.00001068,0.00001068,null,null],"oci/meta.llama-3.2-90b-vision-instruct":[0.000002,0.000002,null,null],"meta.llama-3.2-90b-vision-instruct":[0.000002,0.000002,null,null],"oci/meta.llama-3.3-70b-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-3.3-70b-instruct":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-4-maverick-17b-128e-instruct-fp8":[7.2e-7,7.2e-7,null,null],"meta.llama-4-maverick-17b-128e-instruct-fp8":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-4-scout-17b-16e-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-4-scout-17b-16e-instruct":[7.2e-7,7.2e-7,null,null],"oci/xai.grok-3":[0.000003,0.000015,null,null],"xai.grok-3":[0.000003,0.000015,null,null],"oci/xai.grok-3-fast":[0.000005,0.000025,null,null],"xai.grok-3-fast":[0.000005,0.000025,null,null],"oci/xai.grok-3-mini":[3e-7,5e-7,null,null],"xai.grok-3-mini":[3e-7,5e-7,null,null],"oci/xai.grok-3-mini-fast":[6e-7,0.000004,null,null],"xai.grok-3-mini-fast":[6e-7,0.000004,null,null],"oci/xai.grok-4":[0.000003,0.000015,null,null],"xai.grok-4":[0.000003,0.000015,null,null],"oci/cohere.command-latest":[0.00000156,0.00000156,null,null],"cohere.command-latest":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-03-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-03-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-plus-latest":[0.00000156,0.00000156,null,null],"cohere.command-plus-latest":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-reasoning-08-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-reasoning-08-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-vision-07-2025":[0.00000156,0.00000156,null,null],"cohere.command-a-vision-07-2025":[0.00000156,0.00000156,null,null],"oci/cohere.command-a-translate-08-2025":[9e-8,9e-8,null,null],"cohere.command-a-translate-08-2025":[9e-8,9e-8,null,null],"oci/cohere.command-r-08-2024":[1.5e-7,1.5e-7,null,null],"cohere.command-r-08-2024":[1.5e-7,1.5e-7,null,null],"oci/cohere.command-r-plus-08-2024":[0.00000156,0.00000156,null,null],"cohere.command-r-plus-08-2024":[0.00000156,0.00000156,null,null],"oci/meta.llama-3.2-11b-vision-instruct":[0.000002,0.000002,null,null],"meta.llama-3.2-11b-vision-instruct":[0.000002,0.000002,null,null],"oci/meta.llama-3.1-70b-instruct":[7.2e-7,7.2e-7,null,null],"meta.llama-3.1-70b-instruct":[7.2e-7,7.2e-7,null,null],"oci/meta.llama-3.3-70b-instruct-fp8-dynamic":[7.2e-7,7.2e-7,null,null],"meta.llama-3.3-70b-instruct-fp8-dynamic":[7.2e-7,7.2e-7,null,null],"oci/xai.grok-4-fast":[0.000005,0.000025,null,null],"xai.grok-4-fast":[0.000005,0.000025,null,null],"oci/xai.grok-4.1-fast":[0.000005,0.000025,null,null],"xai.grok-4.1-fast":[0.000005,0.000025,null,null],"oci/xai.grok-4.20":[0.000003,0.000015,null,null],"xai.grok-4.20":[0.000003,0.000015,null,null],"oci/xai.grok-4.20-multi-agent":[0.000003,0.000015,null,null],"xai.grok-4.20-multi-agent":[0.000003,0.000015,null,null],"oci/xai.grok-code-fast-1":[0.000005,0.000025,null,null],"xai.grok-code-fast-1":[0.000005,0.000025,null,null],"oci/google.gemini-2.5-pro":[0.00000125,0.00001,null,null],"google.gemini-2.5-pro":[0.00000125,0.00001,null,null],"oci/google.gemini-2.5-flash":[1.5e-7,6e-7,null,null],"google.gemini-2.5-flash":[1.5e-7,6e-7,null,null],"oci/google.gemini-2.5-flash-lite":[7.5e-8,3e-7,null,null],"google.gemini-2.5-flash-lite":[7.5e-8,3e-7,null,null],"oci/cohere.embed-english-v3.0":[1e-7,0,null,null],"cohere.embed-english-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-light-v3.0":[1e-7,0,null,null],"cohere.embed-english-light-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-light-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-light-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-image-v3.0":[1e-7,0,null,null],"cohere.embed-english-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-english-light-image-v3.0":[1e-7,0,null,null],"cohere.embed-english-light-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-multilingual-light-image-v3.0":[1e-7,0,null,null],"cohere.embed-multilingual-light-image-v3.0":[1e-7,0,null,null],"oci/cohere.embed-v4.0":[1.2e-7,0,null,null],"cohere.embed-v4.0":[1.2e-7,0,null,null],"ollama/codegeex4":[0,0,null,null],"codegeex4":[0,0,null,null],"ollama/codegemma":[0,0,null,null],"codegemma":[0,0,null,null],"ollama/codellama":[0,0,null,null],"codellama":[0,0,null,null],"ollama/deepseek-coder-v2-base":[0,0,null,null],"deepseek-coder-v2-base":[0,0,null,null],"ollama/deepseek-coder-v2-instruct":[0,0,null,null],"deepseek-coder-v2-instruct":[0,0,null,null],"ollama/deepseek-coder-v2-lite-base":[0,0,null,null],"deepseek-coder-v2-lite-base":[0,0,null,null],"ollama/deepseek-coder-v2-lite-instruct":[0,0,null,null],"deepseek-coder-v2-lite-instruct":[0,0,null,null],"ollama/deepseek-v3.1:671b-cloud":[0,0,null,null],"deepseek-v3.1:671b-cloud":[0,0,null,null],"ollama/gpt-oss:120b-cloud":[0,0,null,null],"gpt-oss:120b-cloud":[0,0,null,null],"ollama/gpt-oss:20b-cloud":[0,0,null,null],"gpt-oss:20b-cloud":[0,0,null,null],"ollama/internlm2_5-20b-chat":[0,0,null,null],"internlm2_5-20b-chat":[0,0,null,null],"ollama/llama2":[0,0,null,null],"llama2":[0,0,null,null],"ollama/llama2-uncensored":[0,0,null,null],"llama2-uncensored":[0,0,null,null],"ollama/llama2:13b":[0,0,null,null],"llama2:13b":[0,0,null,null],"ollama/llama2:70b":[0,0,null,null],"llama2:70b":[0,0,null,null],"ollama/llama2:7b":[0,0,null,null],"llama2:7b":[0,0,null,null],"ollama/llama3":[0,0,null,null],"llama3":[0,0,null,null],"ollama/llama3.1":[0,0,null,null],"llama3.1":[0,0,null,null],"ollama/llama3:70b":[0,0,null,null],"llama3:70b":[0,0,null,null],"ollama/llama3:8b":[0,0,null,null],"llama3:8b":[0,0,null,null],"ollama/mistral":[0,0,null,null],"mistral":[0,0,null,null],"ollama/mistral-7B-Instruct-v0.1":[0,0,null,null],"mistral-7B-Instruct-v0.1":[0,0,null,null],"ollama/mistral-7B-Instruct-v0.2":[0,0,null,null],"mistral-7B-Instruct-v0.2":[0,0,null,null],"ollama/mistral-large-instruct-2407":[0,0,null,null],"mistral-large-instruct-2407":[0,0,null,null],"ollama/mixtral-8x22B-Instruct-v0.1":[0,0,null,null],"mixtral-8x22B-Instruct-v0.1":[0,0,null,null],"ollama/mixtral-8x7B-Instruct-v0.1":[0,0,null,null],"mixtral-8x7B-Instruct-v0.1":[0,0,null,null],"ollama/orca-mini":[0,0,null,null],"orca-mini":[0,0,null,null],"ollama/qwen3-coder:480b-cloud":[0,0,null,null],"qwen3-coder:480b-cloud":[0,0,null,null],"ollama/vicuna":[0,0,null,null],"vicuna":[0,0,null,null],"openrouter/anthropic/claude-3-haiku":[2.5e-7,0.00000125,null,null],"anthropic/claude-3-haiku":[2.5e-7,0.00000125,null,null],"openrouter/anthropic/claude-3.5-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-3.5-sonnet":[0.000003,0.000015,null,null],"openrouter/anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"openrouter/anthropic/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"openrouter/anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"openrouter/anthropic/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-sonnet-4.6":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-sonnet-4.6":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-opus-4.5":[0.000005,0.000025,0.00000625,5e-7],"openrouter/anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"openrouter/anthropic/claude-sonnet-4.5":[0.000003,0.000015,0.00000375,3e-7],"openrouter/anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"openrouter/anthropic/claude-opus-4.7":[0.000005,0.000025,0.00000625,5e-7],"anthropic/claude-opus-4.7":[0.000005,0.000025,0.00000625,5e-7],"openrouter/bytedance/ui-tars-1.5-7b":[1e-7,2e-7,null,null],"bytedance/ui-tars-1.5-7b":[1e-7,2e-7,null,null],"openrouter/deepseek/deepseek-chat":[1.4e-7,2.8e-7,null,null],"openrouter/deepseek/deepseek-chat-v3-0324":[1.4e-7,2.8e-7,null,null],"deepseek/deepseek-chat-v3-0324":[1.4e-7,2.8e-7,null,null],"openrouter/deepseek/deepseek-chat-v3.1":[2e-7,8e-7,null,null],"deepseek/deepseek-chat-v3.1":[2e-7,8e-7,null,null],"openrouter/deepseek/deepseek-v3.2":[2.8e-7,4e-7,null,null],"openrouter/deepseek/deepseek-v3.2-exp":[2e-7,4e-7,null,null],"deepseek/deepseek-v3.2-exp":[2e-7,4e-7,null,null],"openrouter/deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"openrouter/deepseek/deepseek-r1-0528":[5e-7,0.00000215,null,null],"deepseek/deepseek-r1-0528":[5e-7,0.00000215,null,null],"openrouter/google/gemini-2.0-flash-001":[1e-7,4e-7,null,null],"openrouter/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"openrouter/google/gemini-2.5-pro":[0.00000125,0.00001,null,null],"openrouter/google/gemini-3-pro-preview":[0.000002,0.000012,null,2e-7],"openrouter/google/gemini-3-flash-preview":[5e-7,0.000003,null,5e-8],"openrouter/google/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"google/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"openrouter/google/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"google/gemini-3.1-pro-preview":[0.000002,0.000012,null,2e-7],"openrouter/gryphe/mythomax-l2-13b":[0.000001875,0.000001875,null,null],"gryphe/mythomax-l2-13b":[0.000001875,0.000001875,null,null],"openrouter/mancer/weaver":[0.000005625,0.000005625,null,null],"mancer/weaver":[0.000005625,0.000005625,null,null],"openrouter/meta-llama/llama-3-70b-instruct":[5.9e-7,7.9e-7,null,null],"meta-llama/llama-3-70b-instruct":[5.9e-7,7.9e-7,null,null],"openrouter/minimax/minimax-m2":[2.55e-7,0.00000102,null,null],"minimax/minimax-m2":[2.55e-7,0.00000102,null,null],"openrouter/mistralai/devstral-2512":[1.5e-7,6e-7,null,null],"mistralai/devstral-2512":[1.5e-7,6e-7,null,null],"openrouter/mistralai/ministral-3b-2512":[1e-7,1e-7,null,null],"mistralai/ministral-3b-2512":[1e-7,1e-7,null,null],"openrouter/mistralai/ministral-8b-2512":[1.5e-7,1.5e-7,null,null],"mistralai/ministral-8b-2512":[1.5e-7,1.5e-7,null,null],"openrouter/mistralai/ministral-14b-2512":[2e-7,2e-7,null,null],"mistralai/ministral-14b-2512":[2e-7,2e-7,null,null],"openrouter/mistralai/mistral-large-2512":[5e-7,0.0000015,null,null],"mistralai/mistral-large-2512":[5e-7,0.0000015,null,null],"openrouter/mistralai/mistral-7b-instruct":[1.3e-7,1.3e-7,null,null],"mistralai/mistral-7b-instruct":[1.3e-7,1.3e-7,null,null],"openrouter/mistralai/mistral-large":[0.000008,0.000024,null,null],"mistralai/mistral-large":[0.000008,0.000024,null,null],"openrouter/mistralai/mistral-small-3.1-24b-instruct":[1e-7,3e-7,null,null],"mistralai/mistral-small-3.1-24b-instruct":[1e-7,3e-7,null,null],"openrouter/mistralai/mistral-small-3.2-24b-instruct":[1e-7,3e-7,null,null],"mistralai/mistral-small-3.2-24b-instruct":[1e-7,3e-7,null,null],"openrouter/mistralai/mixtral-8x22b-instruct":[6.5e-7,6.5e-7,null,null],"mistralai/mixtral-8x22b-instruct":[6.5e-7,6.5e-7,null,null],"openrouter/moonshotai/kimi-k2.5":[6e-7,0.000003,null,1e-7],"moonshotai/kimi-k2.5":[6e-7,0.000003,null,1e-7],"openrouter/openai/gpt-3.5-turbo":[0.0000015,0.000002,null,null],"openai/gpt-3.5-turbo":[0.0000015,0.000002,null,null],"openrouter/openai/gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"openai/gpt-3.5-turbo-16k":[0.000003,0.000004,null,null],"openrouter/openai/gpt-4":[0.00003,0.00006,null,null],"openai/gpt-4":[0.00003,0.00006,null,null],"openrouter/openai/gpt-4.1":[0.000002,0.000008,null,5e-7],"openai/gpt-4.1":[0.000002,0.000008,null,5e-7],"openrouter/openai/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"openai/gpt-4.1-mini":[4e-7,0.0000016,null,1e-7],"openrouter/openai/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"openai/gpt-4.1-nano":[1e-7,4e-7,null,2.5e-8],"openrouter/openai/gpt-4o":[0.0000025,0.00001,null,null],"openrouter/openai/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"openai/gpt-4o-2024-05-13":[0.000005,0.000015,null,null],"openrouter/openai/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5-chat":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5-codex":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"openai/gpt-5.2-codex":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"openai/gpt-5-mini":[2.5e-7,0.000002,null,2.5e-8],"openrouter/openai/gpt-5-nano":[5e-8,4e-7,null,5e-9],"openai/gpt-5-nano":[5e-8,4e-7,null,5e-9],"openrouter/openai/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"openai/gpt-5.1-codex-max":[0.00000125,0.00001,null,1.25e-7],"openrouter/openai/gpt-5.2":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"openai/gpt-5.2-chat":[0.00000175,0.000014,null,1.75e-7],"openrouter/openai/gpt-5.2-pro":[0.000021,0.000168,null,null],"openai/gpt-5.2-pro":[0.000021,0.000168,null,null],"openrouter/openai/gpt-oss-120b":[1.8e-7,8e-7,null,null],"openrouter/openai/gpt-oss-20b":[2e-8,1e-7,null,null],"openrouter/openai/o1":[0.000015,0.00006,null,0.0000075],"openai/o1":[0.000015,0.00006,null,0.0000075],"openrouter/openai/o3-mini":[0.0000011,0.0000044,null,null],"openai/o3-mini":[0.0000011,0.0000044,null,null],"openrouter/openai/o3-mini-high":[0.0000011,0.0000044,null,null],"openai/o3-mini-high":[0.0000011,0.0000044,null,null],"openrouter/qwen/qwen-2.5-coder-32b-instruct":[1.8e-7,1.8e-7,null,null],"qwen/qwen-2.5-coder-32b-instruct":[1.8e-7,1.8e-7,null,null],"openrouter/qwen/qwen-vl-plus":[2.1e-7,6.3e-7,null,null],"qwen/qwen-vl-plus":[2.1e-7,6.3e-7,null,null],"openrouter/qwen/qwen3-coder":[2.2e-7,9.5e-7,null,null],"qwen/qwen3-coder":[2.2e-7,9.5e-7,null,null],"openrouter/qwen/qwen3-coder-plus":[0.000001,0.000005,null,null],"qwen/qwen3-coder-plus":[0.000001,0.000005,null,null],"openrouter/qwen/qwen3-235b-a22b-2507":[7.1e-8,1e-7,null,null],"qwen/qwen3-235b-a22b-2507":[7.1e-8,1e-7,null,null],"openrouter/qwen/qwen3-235b-a22b-thinking-2507":[1.1e-7,6e-7,null,null],"qwen/qwen3-235b-a22b-thinking-2507":[1.1e-7,6e-7,null,null],"openrouter/qwen/qwen3.6-plus":[3.25e-7,0.00000195,null,null],"qwen/qwen3.6-plus":[3.25e-7,0.00000195,null,null],"openrouter/qwen/qwen3.5-35b-a3b":[2.5e-7,0.000002,null,null],"qwen/qwen3.5-35b-a3b":[2.5e-7,0.000002,null,null],"openrouter/qwen/qwen3.5-27b":[3e-7,0.0000024,null,null],"qwen/qwen3.5-27b":[3e-7,0.0000024,null,null],"openrouter/qwen/qwen3.5-122b-a10b":[4e-7,0.000002,null,null],"qwen/qwen3.5-122b-a10b":[4e-7,0.000002,null,null],"openrouter/qwen/qwen3.5-flash-02-23":[1e-7,4e-7,null,null],"qwen/qwen3.5-flash-02-23":[1e-7,4e-7,null,null],"openrouter/qwen/qwen3.5-plus-02-15":[4e-7,0.0000024,null,null],"qwen/qwen3.5-plus-02-15":[4e-7,0.0000024,null,null],"openrouter/qwen/qwen3.5-397b-a17b":[6e-7,0.0000036,null,null],"qwen/qwen3.5-397b-a17b":[6e-7,0.0000036,null,null],"openrouter/switchpoint/router":[8.5e-7,0.0000034,null,null],"switchpoint/router":[8.5e-7,0.0000034,null,null],"openrouter/undi95/remm-slerp-l2-13b":[0.000001875,0.000001875,null,null],"undi95/remm-slerp-l2-13b":[0.000001875,0.000001875,null,null],"openrouter/x-ai/grok-4":[0.000003,0.000015,null,null],"x-ai/grok-4":[0.000003,0.000015,null,null],"openrouter/z-ai/glm-4.6":[4e-7,0.00000175,null,null],"z-ai/glm-4.6":[4e-7,0.00000175,null,null],"openrouter/z-ai/glm-4.6:exacto":[4.5e-7,0.0000019,null,null],"z-ai/glm-4.6:exacto":[4.5e-7,0.0000019,null,null],"openrouter/xiaomi/mimo-v2-flash":[9e-8,2.9e-7,0,0],"xiaomi/mimo-v2-flash":[9e-8,2.9e-7,0,0],"openrouter/z-ai/glm-4.7":[4e-7,0.0000015,0,0],"z-ai/glm-4.7":[4e-7,0.0000015,0,0],"openrouter/z-ai/glm-4.7-flash":[7e-8,4e-7,0,0],"z-ai/glm-4.7-flash":[7e-8,4e-7,0,0],"openrouter/z-ai/glm-5":[8e-7,0.00000256,null,null],"z-ai/glm-5":[8e-7,0.00000256,null,null],"openrouter/minimax/minimax-m2.1":[2.7e-7,0.0000012,0,0],"minimax/minimax-m2.1":[2.7e-7,0.0000012,0,0],"openrouter/minimax/minimax-m2.5":[3e-7,0.0000011,null,1.5e-7],"minimax/minimax-m2.5":[3e-7,0.0000011,null,1.5e-7],"openrouter/openrouter/auto":[0,0,null,null],"openrouter/auto":[0,0,null,null],"openrouter/openrouter/free":[0,0,null,null],"openrouter/free":[0,0,null,null],"openrouter/openrouter/bodybuilder":[0,0,null,null],"openrouter/bodybuilder":[0,0,null,null],"ovhcloud/DeepSeek-R1-Distill-Llama-70B":[6.7e-7,6.7e-7,null,null],"DeepSeek-R1-Distill-Llama-70B":[6.7e-7,6.7e-7,null,null],"ovhcloud/Llama-3.1-8B-Instruct":[1e-7,1e-7,null,null],"Llama-3.1-8B-Instruct":[1e-7,1e-7,null,null],"ovhcloud/Meta-Llama-3_1-70B-Instruct":[6.7e-7,6.7e-7,null,null],"Meta-Llama-3_1-70B-Instruct":[6.7e-7,6.7e-7,null,null],"ovhcloud/Meta-Llama-3_3-70B-Instruct":[6.7e-7,6.7e-7,null,null],"Meta-Llama-3_3-70B-Instruct":[6.7e-7,6.7e-7,null,null],"ovhcloud/Mistral-7B-Instruct-v0.3":[1e-7,1e-7,null,null],"Mistral-7B-Instruct-v0.3":[1e-7,1e-7,null,null],"ovhcloud/Mistral-Nemo-Instruct-2407":[1.3e-7,1.3e-7,null,null],"Mistral-Nemo-Instruct-2407":[1.3e-7,1.3e-7,null,null],"ovhcloud/Mistral-Small-3.2-24B-Instruct-2506":[9e-8,2.8e-7,null,null],"Mistral-Small-3.2-24B-Instruct-2506":[9e-8,2.8e-7,null,null],"ovhcloud/Mixtral-8x7B-Instruct-v0.1":[6.3e-7,6.3e-7,null,null],"Mixtral-8x7B-Instruct-v0.1":[6.3e-7,6.3e-7,null,null],"ovhcloud/Qwen2.5-Coder-32B-Instruct":[8.7e-7,8.7e-7,null,null],"Qwen2.5-Coder-32B-Instruct":[8.7e-7,8.7e-7,null,null],"ovhcloud/Qwen2.5-VL-72B-Instruct":[9.1e-7,9.1e-7,null,null],"Qwen2.5-VL-72B-Instruct":[9.1e-7,9.1e-7,null,null],"ovhcloud/Qwen3-32B":[8e-8,2.3e-7,null,null],"Qwen3-32B":[8e-8,2.3e-7,null,null],"ovhcloud/gpt-oss-120b":[8e-8,4e-7,null,null],"ovhcloud/gpt-oss-20b":[4e-8,1.5e-7,null,null],"gpt-oss-20b":[4e-8,1.5e-7,null,null],"ovhcloud/llava-v1.6-mistral-7b-hf":[2.9e-7,2.9e-7,null,null],"llava-v1.6-mistral-7b-hf":[2.9e-7,2.9e-7,null,null],"ovhcloud/mamba-codestral-7B-v0.1":[1.9e-7,1.9e-7,null,null],"mamba-codestral-7B-v0.1":[1.9e-7,1.9e-7,null,null],"palm/chat-bison":[1.25e-7,1.25e-7,null,null],"chat-bison":[1.25e-7,1.25e-7,null,null],"palm/chat-bison-001":[1.25e-7,1.25e-7,null,null],"chat-bison-001":[1.25e-7,1.25e-7,null,null],"palm/text-bison":[1.25e-7,1.25e-7,null,null],"text-bison":[1.25e-7,1.25e-7,null,null],"palm/text-bison-001":[1.25e-7,1.25e-7,null,null],"text-bison-001":[1.25e-7,1.25e-7,null,null],"palm/text-bison-safety-off":[1.25e-7,1.25e-7,null,null],"text-bison-safety-off":[1.25e-7,1.25e-7,null,null],"palm/text-bison-safety-recitation-off":[1.25e-7,1.25e-7,null,null],"text-bison-safety-recitation-off":[1.25e-7,1.25e-7,null,null],"perplexity/codellama-34b-instruct":[3.5e-7,0.0000014,null,null],"codellama-34b-instruct":[3.5e-7,0.0000014,null,null],"perplexity/codellama-70b-instruct":[7e-7,0.0000028,null,null],"codellama-70b-instruct":[7e-7,0.0000028,null,null],"perplexity/llama-2-70b-chat":[7e-7,0.0000028,null,null],"llama-2-70b-chat":[7e-7,0.0000028,null,null],"perplexity/llama-3.1-70b-instruct":[0.000001,0.000001,null,null],"llama-3.1-70b-instruct":[0.000001,0.000001,null,null],"perplexity/llama-3.1-8b-instruct":[2e-7,2e-7,null,null],"llama-3.1-8b-instruct":[2e-7,2e-7,null,null],"perplexity/mistral-7b-instruct":[7e-8,2.8e-7,null,null],"mistral-7b-instruct":[7e-8,2.8e-7,null,null],"perplexity/mixtral-8x7b-instruct":[7e-8,2.8e-7,null,null],"mixtral-8x7b-instruct":[7e-8,2.8e-7,null,null],"perplexity/pplx-70b-chat":[7e-7,0.0000028,null,null],"pplx-70b-chat":[7e-7,0.0000028,null,null],"perplexity/pplx-70b-online":[0,0.0000028,null,null],"pplx-70b-online":[0,0.0000028,null,null],"perplexity/pplx-7b-chat":[7e-8,2.8e-7,null,null],"pplx-7b-chat":[7e-8,2.8e-7,null,null],"perplexity/pplx-7b-online":[0,2.8e-7,null,null],"pplx-7b-online":[0,2.8e-7,null,null],"perplexity/sonar":[0.000001,0.000001,null,null],"sonar":[0.000001,0.000001,null,null],"perplexity/sonar-deep-research":[0.000002,0.000008,null,null],"sonar-deep-research":[0.000002,0.000008,null,null],"perplexity/sonar-medium-chat":[6e-7,0.0000018,null,null],"sonar-medium-chat":[6e-7,0.0000018,null,null],"perplexity/sonar-medium-online":[0,0.0000018,null,null],"sonar-medium-online":[0,0.0000018,null,null],"perplexity/sonar-pro":[0.000003,0.000015,null,null],"sonar-pro":[0.000003,0.000015,null,null],"perplexity/sonar-reasoning":[0.000001,0.000005,null,null],"sonar-reasoning":[0.000001,0.000005,null,null],"perplexity/sonar-reasoning-pro":[0.000002,0.000008,null,null],"sonar-reasoning-pro":[0.000002,0.000008,null,null],"perplexity/sonar-small-chat":[7e-8,2.8e-7,null,null],"sonar-small-chat":[7e-8,2.8e-7,null,null],"perplexity/sonar-small-online":[0,2.8e-7,null,null],"sonar-small-online":[0,2.8e-7,null,null],"publicai/swiss-ai/apertus-8b-instruct":[0,0,null,null],"swiss-ai/apertus-8b-instruct":[0,0,null,null],"publicai/swiss-ai/apertus-70b-instruct":[0,0,null,null],"swiss-ai/apertus-70b-instruct":[0,0,null,null],"publicai/aisingapore/Gemma-SEA-LION-v4-27B-IT":[0,0,null,null],"aisingapore/Gemma-SEA-LION-v4-27B-IT":[0,0,null,null],"publicai/BSC-LT/salamandra-7b-instruct-tools-16k":[0,0,null,null],"BSC-LT/salamandra-7b-instruct-tools-16k":[0,0,null,null],"publicai/BSC-LT/ALIA-40b-instruct_Q8_0":[0,0,null,null],"BSC-LT/ALIA-40b-instruct_Q8_0":[0,0,null,null],"publicai/allenai/Olmo-3-7B-Instruct":[0,0,null,null],"allenai/Olmo-3-7B-Instruct":[0,0,null,null],"perplexity/pplx-embed-v1-0.6b":[4e-9,0,null,null],"pplx-embed-v1-0.6b":[4e-9,0,null,null],"perplexity/pplx-embed-v1-4b":[3e-8,0,null,null],"pplx-embed-v1-4b":[3e-8,0,null,null],"publicai/aisingapore/Qwen-SEA-LION-v4-32B-IT":[0,0,null,null],"aisingapore/Qwen-SEA-LION-v4-32B-IT":[0,0,null,null],"publicai/allenai/Olmo-3-7B-Think":[0,0,null,null],"allenai/Olmo-3-7B-Think":[0,0,null,null],"publicai/allenai/Olmo-3-32B-Think":[0,0,null,null],"allenai/Olmo-3-32B-Think":[0,0,null,null],"replicate/meta/llama-2-13b":[1e-7,5e-7,null,null],"meta/llama-2-13b":[1e-7,5e-7,null,null],"replicate/meta/llama-2-13b-chat":[1e-7,5e-7,null,null],"meta/llama-2-13b-chat":[1e-7,5e-7,null,null],"replicate/meta/llama-2-70b":[6.5e-7,0.00000275,null,null],"meta/llama-2-70b":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-2-70b-chat":[6.5e-7,0.00000275,null,null],"meta/llama-2-70b-chat":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-2-7b":[5e-8,2.5e-7,null,null],"meta/llama-2-7b":[5e-8,2.5e-7,null,null],"replicate/meta/llama-2-7b-chat":[5e-8,2.5e-7,null,null],"meta/llama-2-7b-chat":[5e-8,2.5e-7,null,null],"replicate/meta/llama-3-70b":[6.5e-7,0.00000275,null,null],"meta/llama-3-70b":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-3-70b-instruct":[6.5e-7,0.00000275,null,null],"meta/llama-3-70b-instruct":[6.5e-7,0.00000275,null,null],"replicate/meta/llama-3-8b":[5e-8,2.5e-7,null,null],"meta/llama-3-8b":[5e-8,2.5e-7,null,null],"replicate/meta/llama-3-8b-instruct":[5e-8,2.5e-7,null,null],"meta/llama-3-8b-instruct":[5e-8,2.5e-7,null,null],"replicate/mistralai/mistral-7b-instruct-v0.2":[5e-8,2.5e-7,null,null],"mistralai/mistral-7b-instruct-v0.2":[5e-8,2.5e-7,null,null],"replicate/mistralai/mistral-7b-v0.1":[5e-8,2.5e-7,null,null],"mistralai/mistral-7b-v0.1":[5e-8,2.5e-7,null,null],"replicate/mistralai/mixtral-8x7b-instruct-v0.1":[3e-7,0.000001,null,null],"mistralai/mixtral-8x7b-instruct-v0.1":[3e-7,0.000001,null,null],"replicate/openai/gpt-5":[0.00000125,0.00001,null,null],"replicateopenai/gpt-oss-20b":[9e-8,3.6e-7,null,null],"replicate/anthropic/claude-4.5-haiku":[0.000001,0.000005,null,null],"anthropic/claude-4.5-haiku":[0.000001,0.000005,null,null],"replicate/ibm-granite/granite-3.3-8b-instruct":[3e-8,2.5e-7,null,null],"ibm-granite/granite-3.3-8b-instruct":[3e-8,2.5e-7,null,null],"replicate/openai/gpt-4o":[0.0000025,0.00001,null,null],"replicate/openai/o4-mini":[0.000001,0.000004,null,null],"openai/o4-mini":[0.000001,0.000004,null,null],"replicate/openai/o1-mini":[0.0000011,0.0000044,null,null],"openai/o1-mini":[0.0000011,0.0000044,null,null],"replicate/openai/o1":[0.000015,0.00006,null,null],"replicate/openai/gpt-4o-mini":[1.5e-7,6e-7,null,null],"replicate/qwen/qwen3-235b-a22b-instruct-2507":[2.64e-7,0.00000106,null,null],"qwen/qwen3-235b-a22b-instruct-2507":[2.64e-7,0.00000106,null,null],"replicate/anthropic/claude-4-sonnet":[0.000003,0.000015,null,null],"replicate/deepseek-ai/deepseek-v3":[0.00000145,0.00000145,null,null],"deepseek-ai/deepseek-v3":[0.00000145,0.00000145,null,null],"replicate/anthropic/claude-3.7-sonnet":[0.000003,0.000015,null,null],"replicate/anthropic/claude-3.5-haiku":[0.000001,0.000005,null,null],"anthropic/claude-3.5-haiku":[0.000001,0.000005,null,null],"replicate/anthropic/claude-3.5-sonnet":[0.00000375,0.00001875,null,null],"replicate/google/gemini-3-pro":[0.000002,0.000012,null,null],"google/gemini-3-pro":[0.000002,0.000012,null,null],"replicate/anthropic/claude-4.5-sonnet":[0.000003,0.000015,null,null],"anthropic/claude-4.5-sonnet":[0.000003,0.000015,null,null],"replicate/openai/gpt-4.1":[0.000002,0.000008,null,null],"replicate/openai/gpt-4.1-nano":[1e-7,4e-7,null,null],"replicate/openai/gpt-4.1-mini":[4e-7,0.0000016,null,null],"replicate/openai/gpt-5-nano":[5e-8,4e-7,null,null],"replicate/openai/gpt-5-mini":[2.5e-7,0.000002,null,null],"replicate/google/gemini-2.5-flash":[0.0000025,0.0000025,null,null],"replicate/openai/gpt-oss-120b":[1.8e-7,7.2e-7,null,null],"replicate/deepseek-ai/deepseek-v3.1":[6.72e-7,0.000002016,null,null],"deepseek-ai/deepseek-v3.1":[6.72e-7,0.000002016,null,null],"replicate/xai/grok-4":[0.0000072,0.000036,null,null],"xai/grok-4":[0.0000072,0.000036,null,null],"replicate/deepseek-ai/deepseek-r1":[0.00000375,0.00001,null,null],"deepseek-ai/deepseek-r1":[0.00000375,0.00001,null,null],"nvidia_nim/nvidia/nv-rerankqa-mistral-4b-v3":[0,0,null,null],"nvidia/nv-rerankqa-mistral-4b-v3":[0,0,null,null],"nvidia_nim/nvidia/llama-3_2-nv-rerankqa-1b-v2":[0,0,null,null],"nvidia/llama-3_2-nv-rerankqa-1b-v2":[0,0,null,null],"nvidia_nim/ranking/nvidia/llama-3.2-nv-rerankqa-1b-v2":[0,0,null,null],"ranking/nvidia/llama-3.2-nv-rerankqa-1b-v2":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-13b":[0,0,null,null],"meta-textgeneration-llama-2-13b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-13b-f":[0,0,null,null],"meta-textgeneration-llama-2-13b-f":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-70b":[0,0,null,null],"meta-textgeneration-llama-2-70b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-70b-b-f":[0,0,null,null],"meta-textgeneration-llama-2-70b-b-f":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-7b":[0,0,null,null],"meta-textgeneration-llama-2-7b":[0,0,null,null],"sagemaker/meta-textgeneration-llama-2-7b-f":[0,0,null,null],"meta-textgeneration-llama-2-7b-f":[0,0,null,null],"sambanova/MiniMax-M2.7":[3e-7,0.0000012,null,null],"MiniMax-M2.7":[3e-7,0.0000012,3.75e-7,6e-8],"sambanova/DeepSeek-R1":[0.000005,0.000007,null,null],"DeepSeek-R1":[0.000005,0.000007,null,null],"sambanova/DeepSeek-R1-Distill-Llama-70B":[7e-7,0.0000014,null,null],"sambanova/DeepSeek-V3-0324":[0.000003,0.0000045,null,null],"DeepSeek-V3-0324":[0.000003,0.0000045,null,null],"sambanova/Llama-4-Maverick-17B-128E-Instruct":[6.3e-7,0.0000018,null,null],"Llama-4-Maverick-17B-128E-Instruct":[6.3e-7,0.0000018,null,null],"sambanova/Llama-4-Scout-17B-16E-Instruct":[4e-7,7e-7,null,null],"sambanova/Meta-Llama-3.1-405B-Instruct":[0.000005,0.00001,null,null],"sambanova/Meta-Llama-3.1-8B-Instruct":[1e-7,2e-7,null,null],"sambanova/Meta-Llama-3.2-1B-Instruct":[4e-8,8e-8,null,null],"Meta-Llama-3.2-1B-Instruct":[4e-8,8e-8,null,null],"sambanova/Meta-Llama-3.2-3B-Instruct":[8e-8,1.6e-7,null,null],"Meta-Llama-3.2-3B-Instruct":[8e-8,1.6e-7,null,null],"sambanova/Meta-Llama-3.3-70B-Instruct":[6e-7,0.0000012,null,null],"Meta-Llama-3.3-70B-Instruct":[6e-7,0.0000012,null,null],"sambanova/Meta-Llama-Guard-3-8B":[3e-7,3e-7,null,null],"Meta-Llama-Guard-3-8B":[3e-7,3e-7,null,null],"sambanova/QwQ-32B":[5e-7,0.000001,null,null],"QwQ-32B":[5e-7,0.000001,null,null],"sambanova/Qwen2-Audio-7B-Instruct":[5e-7,0.0001,null,null],"Qwen2-Audio-7B-Instruct":[5e-7,0.0001,null,null],"sambanova/Qwen3-32B":[4e-7,8e-7,null,null],"sambanova/DeepSeek-V3.1":[0.000003,0.0000045,null,null],"DeepSeek-V3.1":[0.000003,0.0000045,null,null],"sambanova/gpt-oss-120b":[0.000003,0.0000045,null,null],"text-completion-codestral/codestral-2405":[0,0,null,null],"text-completion-codestral/codestral-latest":[0,0,null,null],"together_ai/baai/bge-base-en-v1.5":[8e-9,0,null,null],"baai/bge-base-en-v1.5":[8e-9,0,null,null],"together_ai/BAAI/bge-base-en-v1.5":[8e-9,0,null,null],"BAAI/bge-base-en-v1.5":[8e-9,0,null,null],"together_ai/Qwen/Qwen3-235B-A22B-Instruct-2507-tput":[2e-7,0.000006,null,null],"Qwen/Qwen3-235B-A22B-Instruct-2507-tput":[2e-7,0.000006,null,null],"together_ai/Qwen/Qwen3-235B-A22B-Thinking-2507":[6.5e-7,0.000003,null,null],"together_ai/Qwen/Qwen3-235B-A22B-fp8-tput":[2e-7,6e-7,null,null],"Qwen/Qwen3-235B-A22B-fp8-tput":[2e-7,6e-7,null,null],"together_ai/Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":[0.000002,0.000002,null,null],"Qwen/Qwen3-Coder-480B-A35B-Instruct-FP8":[0.000002,0.000002,null,null],"together_ai/deepseek-ai/DeepSeek-R1":[0.000003,0.000007,null,null],"together_ai/deepseek-ai/DeepSeek-R1-0528-tput":[5.5e-7,0.00000219,null,null],"deepseek-ai/DeepSeek-R1-0528-tput":[5.5e-7,0.00000219,null,null],"together_ai/deepseek-ai/DeepSeek-V3":[0.00000125,0.00000125,null,null],"together_ai/deepseek-ai/DeepSeek-V3.1":[6e-7,0.0000017,null,null],"together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo":[8.8e-7,8.8e-7,null,null],"together_ai/meta-llama/Llama-3.3-70B-Instruct-Turbo-Free":[0,0,null,null],"meta-llama/Llama-3.3-70B-Instruct-Turbo-Free":[0,0,null,null],"together_ai/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8":[2.7e-7,8.5e-7,null,null],"together_ai/meta-llama/Llama-4-Scout-17B-16E-Instruct":[1.8e-7,5.9e-7,null,null],"together_ai/meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo":[0.0000035,0.0000035,null,null],"meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo":[0.0000035,0.0000035,null,null],"together_ai/meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo":[8.8e-7,8.8e-7,null,null],"together_ai/meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo":[1.8e-7,1.8e-7,null,null],"together_ai/mistralai/Mixtral-8x7B-Instruct-v0.1":[6e-7,6e-7,null,null],"together_ai/moonshotai/Kimi-K2-Instruct":[0.000001,0.000003,null,null],"together_ai/openai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"together_ai/openai/gpt-oss-20b":[5e-8,2e-7,null,null],"together_ai/zai-org/GLM-4.5-Air-FP8":[2e-7,0.0000011,null,null],"zai-org/GLM-4.5-Air-FP8":[2e-7,0.0000011,null,null],"together_ai/zai-org/GLM-4.6":[6e-7,0.0000022,null,null],"together_ai/zai-org/GLM-4.7":[4.5e-7,0.000002,null,null],"together_ai/moonshotai/Kimi-K2.5":[5e-7,0.0000028,null,null],"together_ai/moonshotai/Kimi-K2-Instruct-0905":[0.000001,0.000003,null,null],"together_ai/Qwen/Qwen3-Next-80B-A3B-Instruct":[1.5e-7,0.0000015,null,null],"together_ai/Qwen/Qwen3-Next-80B-A3B-Thinking":[1.5e-7,0.0000015,null,null],"together_ai/Qwen/Qwen3.5-397B-A17B":[6e-7,0.0000036,null,null],"Qwen/Qwen3.5-397B-A17B":[6e-7,0.0000036,null,null],"v0/v0-1.0-md":[0.000003,0.000015,null,null],"v0-1.0-md":[0.000003,0.000015,null,null],"v0/v0-1.5-lg":[0.000015,0.000075,null,null],"v0-1.5-lg":[0.000015,0.000075,null,null],"v0/v0-1.5-md":[0.000003,0.000015,null,null],"v0-1.5-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/alibaba/qwen-3-14b":[8e-8,2.4e-7,null,null],"alibaba/qwen-3-14b":[8e-8,2.4e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-235b":[2e-7,6e-7,null,null],"alibaba/qwen-3-235b":[2e-7,6e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-30b":[1e-7,3e-7,null,null],"alibaba/qwen-3-30b":[1e-7,3e-7,null,null],"vercel_ai_gateway/alibaba/qwen-3-32b":[1e-7,3e-7,null,null],"alibaba/qwen-3-32b":[1e-7,3e-7,null,null],"vercel_ai_gateway/alibaba/qwen3-coder":[4e-7,0.0000016,null,null],"alibaba/qwen3-coder":[4e-7,0.0000016,null,null],"vercel_ai_gateway/amazon/nova-lite":[6e-8,2.4e-7,null,null],"amazon/nova-lite":[6e-8,2.4e-7,null,null],"vercel_ai_gateway/amazon/nova-micro":[3.5e-8,1.4e-7,null,null],"amazon/nova-micro":[3.5e-8,1.4e-7,null,null],"vercel_ai_gateway/amazon/nova-pro":[8e-7,0.0000032,null,null],"amazon/nova-pro":[8e-7,0.0000032,null,null],"vercel_ai_gateway/amazon/titan-embed-text-v2":[2e-8,0,null,null],"amazon/titan-embed-text-v2":[2e-8,0,null,null],"vercel_ai_gateway/anthropic/claude-3-haiku":[2.5e-7,0.00000125,3e-7,3e-8],"vercel_ai_gateway/anthropic/claude-3-opus":[0.000015,0.000075,0.00001875,0.0000015],"anthropic/claude-3-opus":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-3.5-haiku":[8e-7,0.000004,0.000001,8e-8],"vercel_ai_gateway/anthropic/claude-3.5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3.7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-4-opus":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-4-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-5-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-5-sonnet-20241022":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-5-sonnet-20241022":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-3-7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"anthropic/claude-3-7-sonnet":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-haiku-4.5":[0.000001,0.000005,0.00000125,1e-7],"vercel_ai_gateway/anthropic/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-opus-4.1":[0.000015,0.000075,0.00001875,0.0000015],"vercel_ai_gateway/anthropic/claude-opus-4.5":[0.000005,0.000025,0.00000625,5e-7],"vercel_ai_gateway/anthropic/claude-opus-4.6":[0.000005,0.000025,0.00000625,5e-7],"vercel_ai_gateway/anthropic/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/anthropic/claude-sonnet-4.5":[0.000003,0.000015,0.00000375,3e-7],"vercel_ai_gateway/cohere/command-a":[0.0000025,0.00001,null,null],"cohere/command-a":[0.0000025,0.00001,null,null],"vercel_ai_gateway/cohere/command-r":[1.5e-7,6e-7,null,null],"cohere/command-r":[1.5e-7,6e-7,null,null],"vercel_ai_gateway/cohere/command-r-plus":[0.0000025,0.00001,null,null],"cohere/command-r-plus":[0.0000025,0.00001,null,null],"vercel_ai_gateway/cohere/embed-v4.0":[1.2e-7,0,null,null],"vercel_ai_gateway/deepseek/deepseek-r1":[5.5e-7,0.00000219,null,null],"vercel_ai_gateway/deepseek/deepseek-r1-distill-llama-70b":[7.5e-7,9.9e-7,null,null],"deepseek/deepseek-r1-distill-llama-70b":[7.5e-7,9.9e-7,null,null],"vercel_ai_gateway/deepseek/deepseek-v3":[9e-7,9e-7,null,null],"vercel_ai_gateway/google/gemini-2.0-flash":[1.5e-7,6e-7,null,null],"google/gemini-2.0-flash":[1.5e-7,6e-7,null,null],"vercel_ai_gateway/google/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,null],"google/gemini-2.0-flash-lite":[7.5e-8,3e-7,null,null],"vercel_ai_gateway/google/gemini-2.5-flash":[3e-7,0.0000025,null,null],"vercel_ai_gateway/google/gemini-2.5-pro":[0.0000025,0.00001,null,null],"vercel_ai_gateway/google/gemini-embedding-001":[1.5e-7,0,null,null],"google/gemini-embedding-001":[1.5e-7,0,null,null],"vercel_ai_gateway/google/gemma-2-9b":[2e-7,2e-7,null,null],"google/gemma-2-9b":[2e-7,2e-7,null,null],"vercel_ai_gateway/google/text-embedding-005":[2.5e-8,0,null,null],"google/text-embedding-005":[2.5e-8,0,null,null],"vercel_ai_gateway/google/text-multilingual-embedding-002":[2.5e-8,0,null,null],"google/text-multilingual-embedding-002":[2.5e-8,0,null,null],"vercel_ai_gateway/inception/mercury-coder-small":[2.5e-7,0.000001,null,null],"inception/mercury-coder-small":[2.5e-7,0.000001,null,null],"vercel_ai_gateway/meta/llama-3-70b":[5.9e-7,7.9e-7,null,null],"vercel_ai_gateway/meta/llama-3-8b":[5e-8,8e-8,null,null],"vercel_ai_gateway/meta/llama-3.1-70b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.1-70b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-3.1-8b":[5e-8,8e-8,null,null],"meta/llama-3.1-8b":[5e-8,8e-8,null,null],"vercel_ai_gateway/meta/llama-3.2-11b":[1.6e-7,1.6e-7,null,null],"meta/llama-3.2-11b":[1.6e-7,1.6e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-1b":[1e-7,1e-7,null,null],"meta/llama-3.2-1b":[1e-7,1e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-3b":[1.5e-7,1.5e-7,null,null],"meta/llama-3.2-3b":[1.5e-7,1.5e-7,null,null],"vercel_ai_gateway/meta/llama-3.2-90b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.2-90b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-3.3-70b":[7.2e-7,7.2e-7,null,null],"meta/llama-3.3-70b":[7.2e-7,7.2e-7,null,null],"vercel_ai_gateway/meta/llama-4-maverick":[2e-7,6e-7,null,null],"meta/llama-4-maverick":[2e-7,6e-7,null,null],"vercel_ai_gateway/meta/llama-4-scout":[1e-7,3e-7,null,null],"meta/llama-4-scout":[1e-7,3e-7,null,null],"vercel_ai_gateway/mistral/codestral":[3e-7,9e-7,null,null],"mistral/codestral":[3e-7,9e-7,null,null],"vercel_ai_gateway/mistral/codestral-embed":[1.5e-7,0,null,null],"mistral/codestral-embed":[1.5e-7,0,null,null],"vercel_ai_gateway/mistral/devstral-small":[7e-8,2.8e-7,null,null],"mistral/devstral-small":[7e-8,2.8e-7,null,null],"vercel_ai_gateway/mistral/magistral-medium":[0.000002,0.000005,null,null],"mistral/magistral-medium":[0.000002,0.000005,null,null],"vercel_ai_gateway/mistral/magistral-small":[5e-7,0.0000015,null,null],"mistral/magistral-small":[5e-7,0.0000015,null,null],"vercel_ai_gateway/mistral/ministral-3b":[4e-8,4e-8,null,null],"mistral/ministral-3b":[4e-8,4e-8,null,null],"vercel_ai_gateway/mistral/ministral-8b":[1e-7,1e-7,null,null],"mistral/ministral-8b":[1e-7,1e-7,null,null],"vercel_ai_gateway/mistral/mistral-embed":[1e-7,0,null,null],"mistral/mistral-embed":[1e-7,0,null,null],"vercel_ai_gateway/mistral/mistral-large":[0.000002,0.000006,null,null],"mistral/mistral-large":[0.000002,0.000006,null,null],"vercel_ai_gateway/mistral/mistral-saba-24b":[7.9e-7,7.9e-7,null,null],"mistral/mistral-saba-24b":[7.9e-7,7.9e-7,null,null],"vercel_ai_gateway/mistral/mistral-small":[1e-7,3e-7,null,null],"vercel_ai_gateway/mistral/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"mistral/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"vercel_ai_gateway/mistral/pixtral-12b":[1.5e-7,1.5e-7,null,null],"mistral/pixtral-12b":[1.5e-7,1.5e-7,null,null],"vercel_ai_gateway/mistral/pixtral-large":[0.000002,0.000006,null,null],"mistral/pixtral-large":[0.000002,0.000006,null,null],"vercel_ai_gateway/moonshotai/kimi-k2":[5.5e-7,0.0000022,null,null],"moonshotai/kimi-k2":[5.5e-7,0.0000022,null,null],"vercel_ai_gateway/morph/morph-v3-fast":[8e-7,0.0000012,null,null],"vercel_ai_gateway/morph/morph-v3-large":[9e-7,0.0000019,null,null],"vercel_ai_gateway/openai/gpt-3.5-turbo":[5e-7,0.0000015,null,null],"vercel_ai_gateway/openai/gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"openai/gpt-3.5-turbo-instruct":[0.0000015,0.000002,null,null],"vercel_ai_gateway/openai/gpt-4-turbo":[0.00001,0.00003,null,null],"openai/gpt-4-turbo":[0.00001,0.00003,null,null],"vercel_ai_gateway/openai/gpt-4.1":[0.000002,0.000008,0,5e-7],"vercel_ai_gateway/openai/gpt-4.1-mini":[4e-7,0.0000016,0,1e-7],"vercel_ai_gateway/openai/gpt-4.1-nano":[1e-7,4e-7,0,2.5e-8],"vercel_ai_gateway/openai/gpt-4o":[0.0000025,0.00001,0,0.00000125],"vercel_ai_gateway/openai/gpt-4o-mini":[1.5e-7,6e-7,0,7.5e-8],"vercel_ai_gateway/openai/o1":[0.000015,0.00006,0,0.0000075],"vercel_ai_gateway/openai/o3":[0.000002,0.000008,0,5e-7],"openai/o3":[0.000002,0.000008,0,5e-7],"vercel_ai_gateway/openai/o3-mini":[0.0000011,0.0000044,0,5.5e-7],"vercel_ai_gateway/openai/o4-mini":[0.0000011,0.0000044,0,2.75e-7],"vercel_ai_gateway/openai/text-embedding-3-large":[1.3e-7,0,null,null],"openai/text-embedding-3-large":[1.3e-7,0,null,null],"vercel_ai_gateway/openai/text-embedding-3-small":[2e-8,0,null,null],"openai/text-embedding-3-small":[2e-8,0,null,null],"vercel_ai_gateway/openai/text-embedding-ada-002":[1e-7,0,null,null],"openai/text-embedding-ada-002":[1e-7,0,null,null],"vercel_ai_gateway/perplexity/sonar":[0.000001,0.000001,null,null],"vercel_ai_gateway/perplexity/sonar-pro":[0.000003,0.000015,null,null],"vercel_ai_gateway/perplexity/sonar-reasoning":[0.000001,0.000005,null,null],"vercel_ai_gateway/perplexity/sonar-reasoning-pro":[0.000002,0.000008,null,null],"vercel_ai_gateway/vercel/v0-1.0-md":[0.000003,0.000015,null,null],"vercel/v0-1.0-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/vercel/v0-1.5-md":[0.000003,0.000015,null,null],"vercel/v0-1.5-md":[0.000003,0.000015,null,null],"vercel_ai_gateway/xai/grok-2":[0.000002,0.00001,null,null],"xai/grok-2":[0.000002,0.00001,null,null],"vercel_ai_gateway/xai/grok-2-vision":[0.000002,0.00001,null,null],"xai/grok-2-vision":[0.000002,0.00001,null,null],"vercel_ai_gateway/xai/grok-3":[0.000003,0.000015,null,null],"xai/grok-3":[0.000003,0.000015,null,null],"vercel_ai_gateway/xai/grok-3-fast":[0.000005,0.000025,null,null],"xai/grok-3-fast":[0.000005,0.000025,null,null],"vercel_ai_gateway/xai/grok-3-mini":[3e-7,5e-7,null,null],"xai/grok-3-mini":[3e-7,5e-7,null,null],"vercel_ai_gateway/xai/grok-3-mini-fast":[6e-7,0.000004,null,null],"xai/grok-3-mini-fast":[6e-7,0.000004,null,null],"vercel_ai_gateway/xai/grok-4":[0.000003,0.000015,null,null],"vercel_ai_gateway/zai/glm-4.5":[6e-7,0.0000022,null,null],"zai/glm-4.5":[6e-7,0.0000022,null,null],"vercel_ai_gateway/zai/glm-4.5-air":[2e-7,0.0000011,null,null],"zai/glm-4.5-air":[2e-7,0.0000011,null,null],"vercel_ai_gateway/zai/glm-4.6":[4.5e-7,0.0000018,null,1.1e-7],"zai/glm-4.6":[4.5e-7,0.0000018,null,1.1e-7],"vertex_ai/claude-3-5-haiku":[0.000001,0.000005,null,null],"claude-3-5-haiku":[0.000001,0.000005,null,null],"vertex_ai/claude-3-5-haiku@20241022":[0.000001,0.000005,null,null],"claude-3-5-haiku@20241022":[0.000001,0.000005,null,null],"vertex_ai/claude-haiku-4-5":[0.000001,0.000005,0.00000125,1e-7],"vertex_ai/claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"claude-haiku-4-5@20251001":[0.000001,0.000005,0.00000125,1e-7],"vertex_ai/claude-3-5-sonnet":[0.000003,0.000015,null,null],"claude-3-5-sonnet":[0.000003,0.000015,null,null],"vertex_ai/claude-3-5-sonnet@20240620":[0.000003,0.000015,null,null],"claude-3-5-sonnet@20240620":[0.000003,0.000015,null,null],"vertex_ai/claude-3-7-sonnet@20250219":[0.000003,0.000015,0.00000375,3e-7],"claude-3-7-sonnet@20250219":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-3-haiku":[2.5e-7,0.00000125,null,null],"claude-3-haiku":[2.5e-7,0.00000125,null,null],"vertex_ai/claude-3-haiku@20240307":[2.5e-7,0.00000125,null,null],"claude-3-haiku@20240307":[2.5e-7,0.00000125,null,null],"vertex_ai/claude-3-opus":[0.000015,0.000075,null,null],"claude-3-opus":[0.000015,0.000075,null,null],"vertex_ai/claude-3-opus@20240229":[0.000015,0.000075,null,null],"claude-3-opus@20240229":[0.000015,0.000075,null,null],"vertex_ai/claude-3-sonnet":[0.000003,0.000015,null,null],"claude-3-sonnet":[0.000003,0.000015,null,null],"vertex_ai/claude-3-sonnet@20240229":[0.000003,0.000015,null,null],"claude-3-sonnet@20240229":[0.000003,0.000015,null,null],"vertex_ai/claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-1":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-1@20250805":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4-1@20250805":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-opus-4-5":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-5@20251101":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-5@20251101":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-6":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-6@default":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-6@default":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-7":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-opus-4-7@default":[0.000005,0.000025,0.00000625,5e-7],"claude-opus-4-7@default":[0.000005,0.000025,0.00000625,5e-7],"vertex_ai/claude-sonnet-4-5":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4-6":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4-5@20250929":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-5@20250929":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-opus-4@20250514":[0.000015,0.000075,0.00001875,0.0000015],"claude-opus-4@20250514":[0.000015,0.000075,0.00001875,0.0000015],"vertex_ai/claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/claude-sonnet-4@20250514":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4@20250514":[0.000003,0.000015,0.00000375,3e-7],"vertex_ai/mistralai/codestral-2@001":[3e-7,9e-7,null,null],"mistralai/codestral-2@001":[3e-7,9e-7,null,null],"vertex_ai/codestral-2":[3e-7,9e-7,null,null],"codestral-2":[3e-7,9e-7,null,null],"vertex_ai/codestral-2@001":[3e-7,9e-7,null,null],"codestral-2@001":[3e-7,9e-7,null,null],"vertex_ai/mistralai/codestral-2":[3e-7,9e-7,null,null],"mistralai/codestral-2":[3e-7,9e-7,null,null],"vertex_ai/codestral-2501":[2e-7,6e-7,null,null],"codestral-2501":[2e-7,6e-7,null,null],"vertex_ai/codestral@2405":[2e-7,6e-7,null,null],"codestral@2405":[2e-7,6e-7,null,null],"vertex_ai/codestral@latest":[2e-7,6e-7,null,null],"codestral@latest":[2e-7,6e-7,null,null],"vertex_ai/deepseek-ai/deepseek-v3.1-maas":[0.00000135,0.0000054,null,null],"deepseek-ai/deepseek-v3.1-maas":[0.00000135,0.0000054,null,null],"vertex_ai/deepseek-ai/deepseek-v3.2-maas":[5.6e-7,0.00000168,null,null],"deepseek-ai/deepseek-v3.2-maas":[5.6e-7,0.00000168,null,null],"vertex_ai/deepseek-ai/deepseek-r1-0528-maas":[0.00000135,0.0000054,null,null],"deepseek-ai/deepseek-r1-0528-maas":[0.00000135,0.0000054,null,null],"vertex_ai/gemini-2.5-flash-image":[3e-7,0.0000025,null,3e-8],"vertex_ai/gemini-3-pro-image-preview":[0.000002,0.000012,null,null],"vertex_ai/gemini-3.1-flash-image-preview":[5e-7,0.000003,null,null],"vertex_ai/gemini-3.1-flash-lite-preview":[2.5e-7,0.0000015,null,2.5e-8],"vertex_ai/deep-research-pro-preview-12-2025":[0.000002,0.000012,null,null],"vertex_ai/jamba-1.5":[2e-7,4e-7,null,null],"vertex_ai/jamba-1.5-large":[0.000002,0.000008,null,null],"vertex_ai/jamba-1.5-large@001":[0.000002,0.000008,null,null],"vertex_ai/jamba-1.5-mini":[2e-7,4e-7,null,null],"vertex_ai/jamba-1.5-mini@001":[2e-7,4e-7,null,null],"vertex_ai/meta/llama-3.1-405b-instruct-maas":[0.000005,0.000016,null,null],"meta/llama-3.1-405b-instruct-maas":[0.000005,0.000016,null,null],"vertex_ai/meta/llama-3.1-70b-instruct-maas":[0,0,null,null],"meta/llama-3.1-70b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-3.1-8b-instruct-maas":[0,0,null,null],"meta/llama-3.1-8b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-3.2-90b-vision-instruct-maas":[0,0,null,null],"meta/llama-3.2-90b-vision-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama-4-maverick-17b-128e-instruct-maas":[3.5e-7,0.00000115,null,null],"meta/llama-4-maverick-17b-128e-instruct-maas":[3.5e-7,0.00000115,null,null],"vertex_ai/meta/llama-4-maverick-17b-16e-instruct-maas":[3.5e-7,0.00000115,null,null],"meta/llama-4-maverick-17b-16e-instruct-maas":[3.5e-7,0.00000115,null,null],"vertex_ai/meta/llama-4-scout-17b-128e-instruct-maas":[2.5e-7,7e-7,null,null],"meta/llama-4-scout-17b-128e-instruct-maas":[2.5e-7,7e-7,null,null],"vertex_ai/meta/llama-4-scout-17b-16e-instruct-maas":[2.5e-7,7e-7,null,null],"meta/llama-4-scout-17b-16e-instruct-maas":[2.5e-7,7e-7,null,null],"vertex_ai/meta/llama3-405b-instruct-maas":[0,0,null,null],"meta/llama3-405b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama3-70b-instruct-maas":[0,0,null,null],"meta/llama3-70b-instruct-maas":[0,0,null,null],"vertex_ai/meta/llama3-8b-instruct-maas":[0,0,null,null],"meta/llama3-8b-instruct-maas":[0,0,null,null],"vertex_ai/minimaxai/minimax-m2-maas":[3e-7,0.0000012,null,null],"minimaxai/minimax-m2-maas":[3e-7,0.0000012,null,null],"vertex_ai/moonshotai/kimi-k2-thinking-maas":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-thinking-maas":[6e-7,0.0000025,null,null],"vertex_ai/zai-org/glm-4.7-maas":[6e-7,0.0000022,null,null],"zai-org/glm-4.7-maas":[6e-7,0.0000022,null,null],"vertex_ai/zai-org/glm-5-maas":[0.000001,0.0000032,null,1e-7],"zai-org/glm-5-maas":[0.000001,0.0000032,null,1e-7],"vertex_ai/mistral-medium-3":[4e-7,0.000002,null,null],"mistral-medium-3":[4e-7,0.000002,null,null],"vertex_ai/mistral-medium-3@001":[4e-7,0.000002,null,null],"mistral-medium-3@001":[4e-7,0.000002,null,null],"vertex_ai/mistralai/mistral-medium-3":[4e-7,0.000002,null,null],"mistralai/mistral-medium-3":[4e-7,0.000002,null,null],"vertex_ai/mistralai/mistral-medium-3@001":[4e-7,0.000002,null,null],"mistralai/mistral-medium-3@001":[4e-7,0.000002,null,null],"vertex_ai/mistral-large-2411":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@2407":[0.000002,0.000006,null,null],"mistral-large@2407":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@2411-001":[0.000002,0.000006,null,null],"mistral-large@2411-001":[0.000002,0.000006,null,null],"vertex_ai/mistral-large@latest":[0.000002,0.000006,null,null],"mistral-large@latest":[0.000002,0.000006,null,null],"vertex_ai/mistral-nemo@2407":[0.000003,0.000003,null,null],"mistral-nemo@2407":[0.000003,0.000003,null,null],"vertex_ai/mistral-nemo@latest":[1.5e-7,1.5e-7,null,null],"mistral-nemo@latest":[1.5e-7,1.5e-7,null,null],"vertex_ai/mistral-small-2503":[0.000001,0.000003,null,null],"vertex_ai/mistral-small-2503@001":[0.000001,0.000003,null,null],"mistral-small-2503@001":[0.000001,0.000003,null,null],"vertex_ai/deepseek-ai/deepseek-ocr-maas":[3e-7,0.0000012,null,null],"deepseek-ai/deepseek-ocr-maas":[3e-7,0.0000012,null,null],"vertex_ai/openai/gpt-oss-120b-maas":[1.5e-7,6e-7,null,null],"openai/gpt-oss-120b-maas":[1.5e-7,6e-7,null,null],"vertex_ai/openai/gpt-oss-20b-maas":[7.5e-8,3e-7,null,null],"openai/gpt-oss-20b-maas":[7.5e-8,3e-7,null,null],"vertex_ai/xai/grok-4.1-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4.1-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"vertex_ai/xai/grok-4.1-fast-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4.1-fast-reasoning":[2e-7,5e-7,null,5e-8],"vertex_ai/xai/grok-4.20-non-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-non-reasoning":[0.000002,0.000006,null,2e-7],"vertex_ai/xai/grok-4.20-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-reasoning":[0.000002,0.000006,null,2e-7],"vertex_ai/qwen/qwen3-235b-a22b-instruct-2507-maas":[2.5e-7,0.000001,null,null],"qwen/qwen3-235b-a22b-instruct-2507-maas":[2.5e-7,0.000001,null,null],"vertex_ai/qwen/qwen3-coder-480b-a35b-instruct-maas":[0.000001,0.000004,null,null],"qwen/qwen3-coder-480b-a35b-instruct-maas":[0.000001,0.000004,null,null],"vertex_ai/qwen/qwen3-next-80b-a3b-instruct-maas":[1.5e-7,0.0000012,null,null],"qwen/qwen3-next-80b-a3b-instruct-maas":[1.5e-7,0.0000012,null,null],"vertex_ai/qwen/qwen3-next-80b-a3b-thinking-maas":[1.5e-7,0.0000012,null,null],"qwen/qwen3-next-80b-a3b-thinking-maas":[1.5e-7,0.0000012,null,null],"voyage/rerank-2":[5e-8,0,null,null],"rerank-2":[5e-8,0,null,null],"voyage/rerank-2-lite":[2e-8,0,null,null],"rerank-2-lite":[2e-8,0,null,null],"voyage/rerank-2.5":[5e-8,0,null,null],"rerank-2.5":[5e-8,0,null,null],"voyage/rerank-2.5-lite":[2e-8,0,null,null],"rerank-2.5-lite":[2e-8,0,null,null],"voyage/voyage-2":[1e-7,0,null,null],"voyage-2":[1e-7,0,null,null],"voyage/voyage-3":[6e-8,0,null,null],"voyage-3":[6e-8,0,null,null],"voyage/voyage-3-large":[1.8e-7,0,null,null],"voyage-3-large":[1.8e-7,0,null,null],"voyage/voyage-3-lite":[2e-8,0,null,null],"voyage-3-lite":[2e-8,0,null,null],"voyage/voyage-3.5":[6e-8,0,null,null],"voyage-3.5":[6e-8,0,null,null],"voyage/voyage-3.5-lite":[2e-8,0,null,null],"voyage-3.5-lite":[2e-8,0,null,null],"voyage/voyage-code-2":[1.2e-7,0,null,null],"voyage-code-2":[1.2e-7,0,null,null],"voyage/voyage-code-3":[1.8e-7,0,null,null],"voyage-code-3":[1.8e-7,0,null,null],"voyage/voyage-context-3":[1.8e-7,0,null,null],"voyage-context-3":[1.8e-7,0,null,null],"voyage/voyage-finance-2":[1.2e-7,0,null,null],"voyage-finance-2":[1.2e-7,0,null,null],"voyage/voyage-large-2":[1.2e-7,0,null,null],"voyage-large-2":[1.2e-7,0,null,null],"voyage/voyage-law-2":[1.2e-7,0,null,null],"voyage-law-2":[1.2e-7,0,null,null],"voyage/voyage-lite-01":[1e-7,0,null,null],"voyage-lite-01":[1e-7,0,null,null],"voyage/voyage-lite-02-instruct":[1e-7,0,null,null],"voyage-lite-02-instruct":[1e-7,0,null,null],"voyage/voyage-multimodal-3":[1.2e-7,0,null,null],"voyage-multimodal-3":[1.2e-7,0,null,null],"wandb/openai/gpt-oss-120b":[0.015,0.06,null,null],"wandb/openai/gpt-oss-20b":[0.005,0.02,null,null],"wandb/zai-org/GLM-4.5":[0.055,0.2,null,null],"wandb/Qwen/Qwen3-235B-A22B-Instruct-2507":[0.01,0.01,null,null],"wandb/Qwen/Qwen3-Coder-480B-A35B-Instruct":[0.1,0.15,null,null],"wandb/Qwen/Qwen3-235B-A22B-Thinking-2507":[0.01,0.01,null,null],"wandb/moonshotai/Kimi-K2-Instruct":[6e-7,0.0000025,null,null],"wandb/moonshotai/Kimi-K2.5":[6e-7,0.000003,null,1e-7],"wandb/MiniMaxAI/MiniMax-M2.5":[3e-7,0.0000012,null,null],"wandb/meta-llama/Llama-3.1-8B-Instruct":[0.022,0.022,null,null],"wandb/deepseek-ai/DeepSeek-V3.1":[0.055,0.165,null,null],"wandb/deepseek-ai/DeepSeek-R1-0528":[0.135,0.54,null,null],"wandb/deepseek-ai/DeepSeek-V3-0324":[0.114,0.275,null,null],"wandb/meta-llama/Llama-3.3-70B-Instruct":[0.071,0.071,null,null],"wandb/meta-llama/Llama-4-Scout-17B-16E-Instruct":[0.017,0.066,null,null],"wandb/microsoft/Phi-4-mini-instruct":[0.008,0.035,null,null],"microsoft/Phi-4-mini-instruct":[0.008,0.035,null,null],"watsonx/ibm/granite-3-8b-instruct":[2e-7,2e-7,null,null],"ibm/granite-3-8b-instruct":[2e-7,2e-7,null,null],"watsonx/mistralai/mistral-large":[0.000003,0.00001,null,null],"watsonx/bigscience/mt0-xxl-13b":[0.0005,0.002,null,null],"bigscience/mt0-xxl-13b":[0.0005,0.002,null,null],"watsonx/core42/jais-13b-chat":[0.0005,0.002,null,null],"core42/jais-13b-chat":[0.0005,0.002,null,null],"watsonx/google/flan-t5-xl-3b":[6e-7,6e-7,null,null],"google/flan-t5-xl-3b":[6e-7,6e-7,null,null],"watsonx/ibm/granite-13b-chat-v2":[6e-7,6e-7,null,null],"ibm/granite-13b-chat-v2":[6e-7,6e-7,null,null],"watsonx/ibm/granite-13b-instruct-v2":[6e-7,6e-7,null,null],"ibm/granite-13b-instruct-v2":[6e-7,6e-7,null,null],"watsonx/ibm/granite-3-3-8b-instruct":[2e-7,2e-7,null,null],"ibm/granite-3-3-8b-instruct":[2e-7,2e-7,null,null],"watsonx/ibm/granite-4-h-small":[6e-8,2.5e-7,null,null],"ibm/granite-4-h-small":[6e-8,2.5e-7,null,null],"watsonx/ibm/granite-guardian-3-2-2b":[1e-7,1e-7,null,null],"ibm/granite-guardian-3-2-2b":[1e-7,1e-7,null,null],"watsonx/ibm/granite-guardian-3-3-8b":[2e-7,2e-7,null,null],"ibm/granite-guardian-3-3-8b":[2e-7,2e-7,null,null],"watsonx/ibm/granite-ttm-1024-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-1024-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-ttm-1536-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-1536-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-ttm-512-96-r2":[3.8e-7,3.8e-7,null,null],"ibm/granite-ttm-512-96-r2":[3.8e-7,3.8e-7,null,null],"watsonx/ibm/granite-vision-3-2-2b":[1e-7,1e-7,null,null],"ibm/granite-vision-3-2-2b":[1e-7,1e-7,null,null],"watsonx/meta-llama/llama-3-2-11b-vision-instruct":[3.5e-7,3.5e-7,null,null],"meta-llama/llama-3-2-11b-vision-instruct":[3.5e-7,3.5e-7,null,null],"watsonx/meta-llama/llama-3-2-1b-instruct":[1e-7,1e-7,null,null],"meta-llama/llama-3-2-1b-instruct":[1e-7,1e-7,null,null],"watsonx/meta-llama/llama-3-2-3b-instruct":[1.5e-7,1.5e-7,null,null],"meta-llama/llama-3-2-3b-instruct":[1.5e-7,1.5e-7,null,null],"watsonx/meta-llama/llama-3-2-90b-vision-instruct":[0.000002,0.000002,null,null],"meta-llama/llama-3-2-90b-vision-instruct":[0.000002,0.000002,null,null],"watsonx/meta-llama/llama-3-3-70b-instruct":[7.1e-7,7.1e-7,null,null],"meta-llama/llama-3-3-70b-instruct":[7.1e-7,7.1e-7,null,null],"watsonx/meta-llama/llama-4-maverick-17b":[3.5e-7,0.0000014,null,null],"meta-llama/llama-4-maverick-17b":[3.5e-7,0.0000014,null,null],"watsonx/meta-llama/llama-guard-3-11b-vision":[3.5e-7,3.5e-7,null,null],"meta-llama/llama-guard-3-11b-vision":[3.5e-7,3.5e-7,null,null],"watsonx/mistralai/mistral-medium-2505":[0.000003,0.00001,null,null],"mistralai/mistral-medium-2505":[0.000003,0.00001,null,null],"watsonx/mistralai/mistral-small-2503":[1e-7,3e-7,null,null],"mistralai/mistral-small-2503":[1e-7,3e-7,null,null],"watsonx/mistralai/mistral-small-3-1-24b-instruct-2503":[1e-7,3e-7,null,null],"mistralai/mistral-small-3-1-24b-instruct-2503":[1e-7,3e-7,null,null],"watsonx/mistralai/pixtral-12b-2409":[3.5e-7,3.5e-7,null,null],"mistralai/pixtral-12b-2409":[3.5e-7,3.5e-7,null,null],"watsonx/openai/gpt-oss-120b":[1.5e-7,6e-7,null,null],"watsonx/sdaia/allam-1-13b-instruct":[0.0000018,0.0000018,null,null],"sdaia/allam-1-13b-instruct":[0.0000018,0.0000018,null,null],"grok-2":[0.000002,0.00001,null,null],"xai/grok-2-1212":[0.000002,0.00001,null,null],"grok-2-1212":[0.000002,0.00001,null,null],"xai/grok-2-latest":[0.000002,0.00001,null,null],"grok-2-latest":[0.000002,0.00001,null,null],"grok-2-vision":[0.000002,0.00001,null,null],"xai/grok-2-vision-1212":[0.000002,0.00001,null,null],"grok-2-vision-1212":[0.000002,0.00001,null,null],"xai/grok-2-vision-latest":[0.000002,0.00001,null,null],"grok-2-vision-latest":[0.000002,0.00001,null,null],"xai/grok-3-beta":[0.000003,0.000015,null,7.5e-7],"grok-3-beta":[0.000003,0.000015,null,7.5e-7],"xai/grok-3-fast-beta":[0.000005,0.000025,null,0.00000125],"grok-3-fast-beta":[0.000005,0.000025,null,0.00000125],"xai/grok-3-fast-latest":[0.000005,0.000025,null,0.00000125],"grok-3-fast-latest":[0.000005,0.000025,null,0.00000125],"xai/grok-3-latest":[0.000003,0.000015,null,7.5e-7],"grok-3-latest":[0.000003,0.000015,null,7.5e-7],"xai/grok-3-mini-beta":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-beta":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-fast":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-fast-beta":[6e-7,0.000004,null,1.5e-7],"grok-3-mini-fast-beta":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-fast-latest":[6e-7,0.000004,null,1.5e-7],"grok-3-mini-fast-latest":[6e-7,0.000004,null,1.5e-7],"xai/grok-3-mini-latest":[3e-7,5e-7,null,7.5e-8],"grok-3-mini-latest":[3e-7,5e-7,null,7.5e-8],"xai/grok-4-fast-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-0709":[0.000003,0.000015,null,null],"grok-4-0709":[0.000003,0.000015,null,null],"xai/grok-4-latest":[0.000003,0.000015,null,null],"grok-4-latest":[0.000003,0.000015,null,null],"xai/grok-4-1-fast":[2e-7,5e-7,null,5e-8],"grok-4-1-fast":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-reasoning-latest":[2e-7,5e-7,null,5e-8],"grok-4-1-fast-reasoning-latest":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-non-reasoning":[2e-7,5e-7,null,5e-8],"xai/grok-4-1-fast-non-reasoning-latest":[2e-7,5e-7,null,5e-8],"grok-4-1-fast-non-reasoning-latest":[2e-7,5e-7,null,5e-8],"xai/grok-4.20-multi-agent-beta-0309":[0.000002,0.000006,null,2e-7],"grok-4.20-multi-agent-beta-0309":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-beta-0309-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-beta-0309-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-0309-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-0309-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.20-beta-0309-non-reasoning":[0.000002,0.000006,null,2e-7],"grok-4.20-beta-0309-non-reasoning":[0.000002,0.000006,null,2e-7],"xai/grok-4.3":[0.00000125,0.0000025,null,2e-7],"grok-4.3":[0.00000125,0.0000025,null,2e-7],"xai/grok-4.3-latest":[0.00000125,0.0000025,null,2e-7],"grok-4.3-latest":[0.00000125,0.0000025,null,2e-7],"xai/grok-beta":[0.000005,0.000015,null,null],"grok-beta":[0.000005,0.000015,null,null],"xai/grok-code-fast":[2e-7,0.0000015,null,2e-8],"grok-code-fast":[2e-7,0.0000015,null,2e-8],"xai/grok-code-fast-1":[2e-7,0.0000015,null,2e-8],"xai/grok-code-fast-1-0825":[2e-7,0.0000015,null,2e-8],"grok-code-fast-1-0825":[2e-7,0.0000015,null,2e-8],"xai/grok-vision-beta":[0.000005,0.000015,null,null],"grok-vision-beta":[0.000005,0.000015,null,null],"zai/glm-5":[0.000001,0.0000032,0,2e-7],"glm-5":[0.000001,0.0000032,0,2e-7],"zai/glm-5-code":[0.0000012,0.000005,0,3e-7],"glm-5-code":[0.0000012,0.000005,0,3e-7],"zai/glm-4.7":[6e-7,0.0000022,0,1.1e-7],"glm-4.7":[6e-7,0.0000022,0,1.1e-7],"glm-4.6":[6e-7,0.0000022,0,1.1e-7],"glm-4.5":[6e-7,0.0000022,null,null],"zai/glm-4.5v":[6e-7,0.0000018,null,null],"glm-4.5v":[6e-7,0.0000018,null,null],"zai/glm-4.5-x":[0.0000022,0.0000089,null,null],"glm-4.5-x":[0.0000022,0.0000089,null,null],"glm-4.5-air":[2e-7,0.0000011,null,null],"zai/glm-4.5-airx":[0.0000011,0.0000045,null,null],"glm-4.5-airx":[0.0000011,0.0000045,null,null],"zai/glm-4-32b-0414-128k":[1e-7,1e-7,null,null],"glm-4-32b-0414-128k":[1e-7,1e-7,null,null],"zai/glm-4.5-flash":[0,0,null,null],"glm-4.5-flash":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":[4.5e-7,0.0000018,null,null],"accounts/fireworks/models/qwen3-coder-480b-a35b-instruct":[4.5e-7,0.0000018,null,null],"fireworks_ai/accounts/fireworks/models/flux-kontext-pro":[4e-8,4e-8,null,null],"accounts/fireworks/models/flux-kontext-pro":[4e-8,4e-8,null,null],"fireworks_ai/accounts/fireworks/models/SSD-1B":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/SSD-1B":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/chronos-hermes-13b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/chronos-hermes-13b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-13b-python":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-13b-python":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-34b-python":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-34b-python":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-70b-python":[9e-7,9e-7,null,null],"accounts/fireworks/models/code-llama-70b-python":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-llama-7b-python":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-llama-7b-python":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/code-qwen-1p5-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/code-qwen-1p5-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/codegemma-2b":[1e-7,1e-7,null,null],"accounts/fireworks/models/codegemma-2b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/codegemma-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/codegemma-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-671b-v2-p1":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/cogito-671b-v2-p1":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-llama-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-llama-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-qwen-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/cogito-v1-preview-qwen-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/cogito-v1-preview-qwen-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-kontext-max":[8e-8,8e-8,null,null],"accounts/fireworks/models/flux-kontext-max":[8e-8,8e-8,null,null],"fireworks_ai/accounts/fireworks/models/dbrx-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/dbrx-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-1b-base":[1e-7,1e-7,null,null],"accounts/fireworks/models/deepseek-coder-1b-base":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-33b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-coder-33b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-base":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-base-v1p5":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-base-v1p5":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-coder-7b-instruct-v1p5":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-base":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-coder-v2-lite-base":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-coder-v2-lite-instruct":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-coder-v2-lite-instruct":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-prover-v2":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-prover-v2":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-0528-distill-qwen3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-llama-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-llama-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-llama-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-1p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-r1-distill-qwen-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/deepseek-r1-distill-qwen-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v2-lite-chat":[5e-7,5e-7,null,null],"accounts/fireworks/models/deepseek-v2-lite-chat":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/deepseek-v2p5":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/deepseek-v2p5":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/devstral-small-2505":[9e-7,9e-7,null,null],"accounts/fireworks/models/devstral-small-2505":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dobby-mini-unhinged-plus-llama-3-1-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/dobby-mini-unhinged-plus-llama-3-1-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/dobby-unhinged-llama-3-3-70b-new":[9e-7,9e-7,null,null],"accounts/fireworks/models/dobby-unhinged-llama-3-3-70b-new":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dolphin-2-9-2-qwen2-72b":[9e-7,9e-7,null,null],"accounts/fireworks/models/dolphin-2-9-2-qwen2-72b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/dolphin-2p6-mixtral-8x7b":[5e-7,5e-7,null,null],"accounts/fireworks/models/dolphin-2p6-mixtral-8x7b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/ernie-4p5-21b-a3b-pt":[1e-7,1e-7,null,null],"accounts/fireworks/models/ernie-4p5-21b-a3b-pt":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/ernie-4p5-300b-a47b-pt":[1e-7,1e-7,null,null],"accounts/fireworks/models/ernie-4p5-300b-a47b-pt":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/fare-20b":[9e-7,9e-7,null,null],"accounts/fireworks/models/fare-20b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/firefunction-v1":[5e-7,5e-7,null,null],"accounts/fireworks/models/firefunction-v1":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/firellava-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/firellava-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/firesearch-ocr-v6":[2e-7,2e-7,null,null],"accounts/fireworks/models/firesearch-ocr-v6":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/fireworks-asr-large":[0,0,null,null],"accounts/fireworks/models/fireworks-asr-large":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/fireworks-asr-v2":[0,0,null,null],"accounts/fireworks/models/fireworks-asr-v2":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev":[1e-7,1e-7,null,null],"accounts/fireworks/models/flux-1-dev":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev-controlnet-union":[1e-9,1e-9,null,null],"accounts/fireworks/models/flux-1-dev-controlnet-union":[1e-9,1e-9,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-dev-fp8":[5e-10,5e-10,null,null],"accounts/fireworks/models/flux-1-dev-fp8":[5e-10,5e-10,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-schnell":[1e-7,1e-7,null,null],"accounts/fireworks/models/flux-1-schnell":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/flux-1-schnell-fp8":[3.5e-10,3.5e-10,null,null],"accounts/fireworks/models/flux-1-schnell-fp8":[3.5e-10,3.5e-10,null,null],"fireworks_ai/accounts/fireworks/models/gemma-2b-it":[1e-7,1e-7,null,null],"accounts/fireworks/models/gemma-2b-it":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-3-27b-it":[9e-7,9e-7,null,null],"accounts/fireworks/models/gemma-3-27b-it":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma-7b-it":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma-7b-it":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/gemma2-9b-it":[2e-7,2e-7,null,null],"accounts/fireworks/models/gemma2-9b-it":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/glm-4p5v":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/glm-4p5v":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-120b":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/gpt-oss-safeguard-120b":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/gpt-oss-safeguard-20b":[5e-7,5e-7,null,null],"accounts/fireworks/models/gpt-oss-safeguard-20b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/hermes-2-pro-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/hermes-2-pro-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-38b":[9e-7,9e-7,null,null],"accounts/fireworks/models/internvl3-38b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-78b":[9e-7,9e-7,null,null],"accounts/fireworks/models/internvl3-78b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/internvl3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/internvl3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/japanese-stable-diffusion-xl":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/japanese-stable-diffusion-xl":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/kat-coder":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-coder":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/kat-dev-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-dev-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/kat-dev-72b-exp":[9e-7,9e-7,null,null],"accounts/fireworks/models/kat-dev-72b-exp":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-2-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-guard-2-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-3-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-guard-3-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-guard-3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-guard-3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-13b-chat":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-13b-chat":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-70b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v2-70b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-70b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v2-70b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v2-7b-chat":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v2-7b-chat":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-70b-instruct-hf":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3-70b-instruct-hf":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3-8b-instruct-hf":[2e-7,2e-7,null,null],"accounts/fireworks/models/llama-v3-8b-instruct-hf":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-405b-instruct-long":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-405b-instruct-long":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p1-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-70b-instruct-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p1-70b-instruct-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p1-nemotron-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-1b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-1b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/llama-v3p2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/llama-v3p3-70b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/llama-v3p3-70b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/llamaguard-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/llamaguard-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/llava-yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/llava-yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m1-80k":[1e-7,1e-7,null,null],"accounts/fireworks/models/minimax-m1-80k":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/minimax-m2":[3e-7,0.0000012,null,null],"accounts/fireworks/models/minimax-m2":[3e-7,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-14b-instruct-2512":[2e-7,2e-7,null,null],"accounts/fireworks/models/ministral-3-14b-instruct-2512":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-3b-instruct-2512":[1e-7,1e-7,null,null],"accounts/fireworks/models/ministral-3-3b-instruct-2512":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/ministral-3-8b-instruct-2512":[2e-7,2e-7,null,null],"accounts/fireworks/models/ministral-3-8b-instruct-2512":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-4k":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-4k":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v0p2":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-v0p2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-instruct-v3":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-instruct-v3":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-7b-v0p2":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-7b-v0p2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-large-3-fp8":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mistral-large-3-fp8":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mistral-nemo-base-2407":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-nemo-base-2407":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-nemo-instruct-2407":[2e-7,2e-7,null,null],"accounts/fireworks/models/mistral-nemo-instruct-2407":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/mistral-small-24b-instruct-2501":[9e-7,9e-7,null,null],"accounts/fireworks/models/mistral-small-24b-instruct-2501":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"accounts/fireworks/models/mixtral-8x22b-instruct":[0.0000012,0.0000012,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b-instruct":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mixtral-8x7b-instruct-hf":[5e-7,5e-7,null,null],"accounts/fireworks/models/mixtral-8x7b-instruct-hf":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/mythomax-l2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/mythomax-l2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nemotron-nano-v2-12b-vl":[1e-7,1e-7,null,null],"accounts/fireworks/models/nemotron-nano-v2-12b-vl":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-capybara-7b-v1p9":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-capybara-7b-v1p9":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo":[5e-7,5e-7,null,null],"accounts/fireworks/models/nous-hermes-2-mixtral-8x7b-dpo":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-2-yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/nous-hermes-2-yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-13b":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-13b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-70b":[9e-7,9e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-70b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/nous-hermes-llama2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/nous-hermes-llama2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-12b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/nvidia-nemotron-nano-12b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/nvidia-nemotron-nano-9b-v2":[2e-7,2e-7,null,null],"accounts/fireworks/models/nvidia-nemotron-nano-9b-v2":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openchat-3p5-0106-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openchat-3p5-0106-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openhermes-2-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openhermes-2-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openhermes-2p5-mistral-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openhermes-2p5-mistral-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/openorca-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/openorca-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/phi-2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-3-mini-128k-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/phi-3-mini-128k-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/phi-3-vision-128k-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/phi-3-vision-128k-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-python-v1":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-python-v1":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v1":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-v1":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/phind-code-llama-34b-v2":[9e-7,9e-7,null,null],"accounts/fireworks/models/phind-code-llama-34b-v2":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/playground-v2-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/playground-v2-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/playground-v2-5-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/playground-v2-5-1024px-aesthetic":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/pythia-12b":[2e-7,2e-7,null,null],"accounts/fireworks/models/pythia-12b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-qwq-32b-preview":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen-qwq-32b-preview":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-v2p5-14b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen-v2p5-14b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen-v2p5-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen-v2p5-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen1p5-72b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen1p5-72b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-2b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2-vl-2b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2-vl-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2-vl-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2-vl-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-0p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-0p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-1p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-1p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-72b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-72b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-0p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-0p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-0p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-14b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-14b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-1p5b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-1p5b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-1p5b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-128k":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-32k-rope":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-32b-instruct-64k":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-3b-instruct":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-3b-instruct":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-coder-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-coder-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-math-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-math-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-3b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-3b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-72b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-72b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen2p5-vl-7b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen2p5-vl-7b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-0p6b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-0p6b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-14b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-14b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft-131072":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960":[1e-7,1e-7,null,null],"accounts/fireworks/models/qwen3-1p7b-fp8-draft-40960":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-instruct-2507":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b-instruct-2507":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-235b-a22b-thinking-2507":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-235b-a22b-thinking-2507":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-instruct-2507":[5e-7,5e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b-instruct-2507":[5e-7,5e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-30b-a3b-thinking-2507":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-30b-a3b-thinking-2507":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-4b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-4b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-4b-instruct-2507":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-4b-instruct-2507":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-8b":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-8b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-coder-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-coder-480b-instruct-bf16":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-coder-480b-instruct-bf16":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-embedding-0p6b":[0,0,null,null],"accounts/fireworks/models/qwen3-embedding-0p6b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-embedding-4b":[0,0,null,null],"accounts/fireworks/models/qwen3-embedding-4b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/":[1e-7,0,null,null],"accounts/fireworks/models/":[1e-7,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-next-80b-a3b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-next-80b-a3b-thinking":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-next-80b-a3b-thinking":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-0p6b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-0p6b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-4b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-4b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-reranker-8b":[0,0,null,null],"accounts/fireworks/models/qwen3-reranker-8b":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-instruct":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-vl-235b-a22b-instruct":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-235b-a22b-thinking":[2.2e-7,8.8e-7,null,null],"accounts/fireworks/models/qwen3-vl-235b-a22b-thinking":[2.2e-7,8.8e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-vl-30b-a3b-instruct":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-30b-a3b-thinking":[1.5e-7,6e-7,null,null],"accounts/fireworks/models/qwen3-vl-30b-a3b-thinking":[1.5e-7,6e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-32b-instruct":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwen3-vl-32b-instruct":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwen3-vl-8b-instruct":[2e-7,2e-7,null,null],"accounts/fireworks/models/qwen3-vl-8b-instruct":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/qwq-32b":[9e-7,9e-7,null,null],"accounts/fireworks/models/qwq-32b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/rolm-ocr":[2e-7,2e-7,null,null],"accounts/fireworks/models/rolm-ocr":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo":[2e-7,2e-7,null,null],"accounts/fireworks/models/snorkel-mistral-7b-pairrm-dpo":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/stable-diffusion-xl-1024-v1-0":[1.3e-10,1.3e-10,null,null],"accounts/fireworks/models/stable-diffusion-xl-1024-v1-0":[1.3e-10,1.3e-10,null,null],"fireworks_ai/accounts/fireworks/models/stablecode-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/stablecode-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder-16b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder-16b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-15b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder2-15b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-3b":[1e-7,1e-7,null,null],"accounts/fireworks/models/starcoder2-3b":[1e-7,1e-7,null,null],"fireworks_ai/accounts/fireworks/models/starcoder2-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/starcoder2-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/toppy-m-7b":[2e-7,2e-7,null,null],"accounts/fireworks/models/toppy-m-7b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/whisper-v3":[0,0,null,null],"accounts/fireworks/models/whisper-v3":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/whisper-v3-turbo":[0,0,null,null],"accounts/fireworks/models/whisper-v3-turbo":[0,0,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b-200k-capybara":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b-200k-capybara":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-34b-chat":[9e-7,9e-7,null,null],"accounts/fireworks/models/yi-34b-chat":[9e-7,9e-7,null,null],"fireworks_ai/accounts/fireworks/models/yi-6b":[2e-7,2e-7,null,null],"accounts/fireworks/models/yi-6b":[2e-7,2e-7,null,null],"fireworks_ai/accounts/fireworks/models/zephyr-7b-beta":[2e-7,2e-7,null,null],"accounts/fireworks/models/zephyr-7b-beta":[2e-7,2e-7,null,null],"novita/deepseek/deepseek-v3.2":[2.69e-7,4e-7,null,1.345e-7],"novita/minimax/minimax-m2.1":[3e-7,0.0000012,null,3e-8],"novita/zai-org/glm-4.7":[6e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.7":[6e-7,0.0000022,null,1.1e-7],"novita/xiaomimimo/mimo-v2-flash":[1e-7,3e-7,null,2e-8],"xiaomimimo/mimo-v2-flash":[1e-7,3e-7,null,2e-8],"novita/zai-org/autoglm-phone-9b-multilingual":[3.5e-8,1.38e-7,null,null],"zai-org/autoglm-phone-9b-multilingual":[3.5e-8,1.38e-7,null,null],"novita/moonshotai/kimi-k2-thinking":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-thinking":[6e-7,0.0000025,null,null],"novita/minimax/minimax-m2":[3e-7,0.0000012,null,3e-8],"novita/paddlepaddle/paddleocr-vl":[2e-8,2e-8,null,null],"paddlepaddle/paddleocr-vl":[2e-8,2e-8,null,null],"novita/deepseek/deepseek-v3.2-exp":[2.7e-7,4.1e-7,null,null],"novita/qwen/qwen3-vl-235b-a22b-thinking":[9.8e-7,0.00000395,null,null],"qwen/qwen3-vl-235b-a22b-thinking":[9.8e-7,0.00000395,null,null],"novita/zai-org/glm-4.6v":[3e-7,9e-7,null,5.5e-8],"zai-org/glm-4.6v":[3e-7,9e-7,null,5.5e-8],"novita/zai-org/glm-4.6":[5.5e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.6":[5.5e-7,0.0000022,null,1.1e-7],"novita/kwaipilot/kat-coder-pro":[3e-7,0.0000012,null,6e-8],"kwaipilot/kat-coder-pro":[3e-7,0.0000012,null,6e-8],"novita/qwen/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000015,null,null],"qwen/qwen3-next-80b-a3b-instruct":[1.5e-7,0.0000015,null,null],"novita/qwen/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000015,null,null],"qwen/qwen3-next-80b-a3b-thinking":[1.5e-7,0.0000015,null,null],"novita/deepseek/deepseek-ocr":[3e-8,3e-8,null,null],"deepseek/deepseek-ocr":[3e-8,3e-8,null,null],"novita/deepseek/deepseek-v3.1-terminus":[2.7e-7,0.000001,null,1.35e-7],"deepseek/deepseek-v3.1-terminus":[2.7e-7,0.000001,null,1.35e-7],"novita/qwen/qwen3-vl-235b-a22b-instruct":[3e-7,0.0000015,null,null],"qwen/qwen3-vl-235b-a22b-instruct":[3e-7,0.0000015,null,null],"novita/qwen/qwen3-max":[0.00000211,0.00000845,null,null],"qwen/qwen3-max":[0.00000211,0.00000845,null,null],"novita/skywork/r1v4-lite":[2e-7,6e-7,null,null],"skywork/r1v4-lite":[2e-7,6e-7,null,null],"novita/deepseek/deepseek-v3.1":[2.7e-7,0.000001,null,1.35e-7],"deepseek/deepseek-v3.1":[2.7e-7,0.000001,null,1.35e-7],"novita/moonshotai/kimi-k2-0905":[6e-7,0.0000025,null,null],"moonshotai/kimi-k2-0905":[6e-7,0.0000025,null,null],"novita/qwen/qwen3-coder-480b-a35b-instruct":[3e-7,0.0000013,null,null],"qwen/qwen3-coder-480b-a35b-instruct":[3e-7,0.0000013,null,null],"novita/qwen/qwen3-coder-30b-a3b-instruct":[7e-8,2.7e-7,null,null],"qwen/qwen3-coder-30b-a3b-instruct":[7e-8,2.7e-7,null,null],"novita/openai/gpt-oss-120b":[5e-8,2.5e-7,null,null],"novita/moonshotai/kimi-k2-instruct":[5.7e-7,0.0000023,null,null],"moonshotai/kimi-k2-instruct":[5.7e-7,0.0000023,null,null],"novita/deepseek/deepseek-v3-0324":[2.7e-7,0.00000112,null,1.35e-7],"deepseek/deepseek-v3-0324":[2.7e-7,0.00000112,null,1.35e-7],"novita/zai-org/glm-4.5":[6e-7,0.0000022,null,1.1e-7],"zai-org/glm-4.5":[6e-7,0.0000022,null,1.1e-7],"novita/qwen/qwen3-235b-a22b-thinking-2507":[3e-7,0.000003,null,null],"novita/meta-llama/llama-3.1-8b-instruct":[2e-8,5e-8,null,null],"meta-llama/llama-3.1-8b-instruct":[2e-8,5e-8,null,null],"novita/google/gemma-3-12b-it":[5e-8,1e-7,null,null],"novita/zai-org/glm-4.5v":[6e-7,0.0000018,null,1.1e-7],"zai-org/glm-4.5v":[6e-7,0.0000018,null,1.1e-7],"novita/openai/gpt-oss-20b":[4e-8,1.5e-7,null,null],"novita/qwen/qwen3-235b-a22b-instruct-2507":[9e-8,5.8e-7,null,null],"novita/deepseek/deepseek-r1-distill-qwen-14b":[1.5e-7,1.5e-7,null,null],"deepseek/deepseek-r1-distill-qwen-14b":[1.5e-7,1.5e-7,null,null],"novita/meta-llama/llama-3.3-70b-instruct":[1.35e-7,4e-7,null,null],"meta-llama/llama-3.3-70b-instruct":[1.35e-7,4e-7,null,null],"novita/qwen/qwen-2.5-72b-instruct":[3.8e-7,4e-7,null,null],"qwen/qwen-2.5-72b-instruct":[3.8e-7,4e-7,null,null],"novita/mistralai/mistral-nemo":[4e-8,1.7e-7,null,null],"mistralai/mistral-nemo":[4e-8,1.7e-7,null,null],"novita/minimaxai/minimax-m1-80k":[5.5e-7,0.0000022,null,null],"minimaxai/minimax-m1-80k":[5.5e-7,0.0000022,null,null],"novita/deepseek/deepseek-r1-0528":[7e-7,0.0000025,null,3.5e-7],"novita/deepseek/deepseek-r1-distill-qwen-32b":[3e-7,3e-7,null,null],"deepseek/deepseek-r1-distill-qwen-32b":[3e-7,3e-7,null,null],"novita/meta-llama/llama-3-8b-instruct":[4e-8,4e-8,null,null],"meta-llama/llama-3-8b-instruct":[4e-8,4e-8,null,null],"novita/microsoft/wizardlm-2-8x22b":[6.2e-7,6.2e-7,null,null],"microsoft/wizardlm-2-8x22b":[6.2e-7,6.2e-7,null,null],"novita/deepseek/deepseek-r1-0528-qwen3-8b":[6e-8,9e-8,null,null],"deepseek/deepseek-r1-0528-qwen3-8b":[6e-8,9e-8,null,null],"novita/deepseek/deepseek-r1-distill-llama-70b":[8e-7,8e-7,null,null],"novita/meta-llama/llama-3-70b-instruct":[5.1e-7,7.4e-7,null,null],"novita/qwen/qwen3-235b-a22b-fp8":[2e-7,8e-7,null,null],"qwen/qwen3-235b-a22b-fp8":[2e-7,8e-7,null,null],"novita/meta-llama/llama-4-maverick-17b-128e-instruct-fp8":[2.7e-7,8.5e-7,null,null],"meta-llama/llama-4-maverick-17b-128e-instruct-fp8":[2.7e-7,8.5e-7,null,null],"novita/meta-llama/llama-4-scout-17b-16e-instruct":[1.8e-7,5.9e-7,null,null],"novita/nousresearch/hermes-2-pro-llama-3-8b":[1.4e-7,1.4e-7,null,null],"nousresearch/hermes-2-pro-llama-3-8b":[1.4e-7,1.4e-7,null,null],"novita/qwen/qwen2.5-vl-72b-instruct":[8e-7,8e-7,null,null],"qwen/qwen2.5-vl-72b-instruct":[8e-7,8e-7,null,null],"novita/sao10k/l3-70b-euryale-v2.1":[0.00000148,0.00000148,null,null],"sao10k/l3-70b-euryale-v2.1":[0.00000148,0.00000148,null,null],"novita/baidu/ernie-4.5-21B-a3b-thinking":[7e-8,2.8e-7,null,null],"baidu/ernie-4.5-21B-a3b-thinking":[7e-8,2.8e-7,null,null],"novita/sao10k/l3-8b-lunaris":[5e-8,5e-8,null,null],"sao10k/l3-8b-lunaris":[5e-8,5e-8,null,null],"novita/baichuan/baichuan-m2-32b":[7e-8,7e-8,null,null],"baichuan/baichuan-m2-32b":[7e-8,7e-8,null,null],"novita/baidu/ernie-4.5-vl-424b-a47b":[4.2e-7,0.00000125,null,null],"baidu/ernie-4.5-vl-424b-a47b":[4.2e-7,0.00000125,null,null],"novita/baidu/ernie-4.5-300b-a47b-paddle":[2.8e-7,0.0000011,null,null],"baidu/ernie-4.5-300b-a47b-paddle":[2.8e-7,0.0000011,null,null],"novita/deepseek/deepseek-prover-v2-671b":[7e-7,0.0000025,null,null],"deepseek/deepseek-prover-v2-671b":[7e-7,0.0000025,null,null],"novita/qwen/qwen3-32b-fp8":[1e-7,4.5e-7,null,null],"qwen/qwen3-32b-fp8":[1e-7,4.5e-7,null,null],"novita/qwen/qwen3-30b-a3b-fp8":[9e-8,4.5e-7,null,null],"qwen/qwen3-30b-a3b-fp8":[9e-8,4.5e-7,null,null],"novita/google/gemma-3-27b-it":[1.19e-7,2e-7,null,null],"novita/deepseek/deepseek-v3-turbo":[4e-7,0.0000013,null,null],"deepseek/deepseek-v3-turbo":[4e-7,0.0000013,null,null],"novita/deepseek/deepseek-r1-turbo":[7e-7,0.0000025,null,null],"deepseek/deepseek-r1-turbo":[7e-7,0.0000025,null,null],"novita/Sao10K/L3-8B-Stheno-v3.2":[5e-8,5e-8,null,null],"Sao10K/L3-8B-Stheno-v3.2":[5e-8,5e-8,null,null],"novita/gryphe/mythomax-l2-13b":[9e-8,9e-8,null,null],"novita/baidu/ernie-4.5-vl-28b-a3b-thinking":[3.9e-7,3.9e-7,null,null],"baidu/ernie-4.5-vl-28b-a3b-thinking":[3.9e-7,3.9e-7,null,null],"novita/qwen/qwen3-vl-8b-instruct":[8e-8,5e-7,null,null],"qwen/qwen3-vl-8b-instruct":[8e-8,5e-7,null,null],"novita/zai-org/glm-4.5-air":[1.3e-7,8.5e-7,null,null],"zai-org/glm-4.5-air":[1.3e-7,8.5e-7,null,null],"novita/qwen/qwen3-vl-30b-a3b-instruct":[2e-7,7e-7,null,null],"qwen/qwen3-vl-30b-a3b-instruct":[2e-7,7e-7,null,null],"novita/qwen/qwen3-vl-30b-a3b-thinking":[2e-7,0.000001,null,null],"qwen/qwen3-vl-30b-a3b-thinking":[2e-7,0.000001,null,null],"novita/qwen/qwen3-omni-30b-a3b-thinking":[2.5e-7,9.7e-7,null,null],"qwen/qwen3-omni-30b-a3b-thinking":[2.5e-7,9.7e-7,null,null],"novita/qwen/qwen3-omni-30b-a3b-instruct":[2.5e-7,9.7e-7,null,null],"qwen/qwen3-omni-30b-a3b-instruct":[2.5e-7,9.7e-7,null,null],"novita/qwen/qwen-mt-plus":[2.5e-7,7.5e-7,null,null],"qwen/qwen-mt-plus":[2.5e-7,7.5e-7,null,null],"novita/baidu/ernie-4.5-vl-28b-a3b":[1.4e-7,5.6e-7,null,null],"baidu/ernie-4.5-vl-28b-a3b":[1.4e-7,5.6e-7,null,null],"novita/baidu/ernie-4.5-21B-a3b":[7e-8,2.8e-7,null,null],"baidu/ernie-4.5-21B-a3b":[7e-8,2.8e-7,null,null],"novita/qwen/qwen3-8b-fp8":[3.5e-8,1.38e-7,null,null],"qwen/qwen3-8b-fp8":[3.5e-8,1.38e-7,null,null],"novita/qwen/qwen3-4b-fp8":[3e-8,3e-8,null,null],"qwen/qwen3-4b-fp8":[3e-8,3e-8,null,null],"novita/qwen/qwen2.5-7b-instruct":[7e-8,7e-8,null,null],"qwen/qwen2.5-7b-instruct":[7e-8,7e-8,null,null],"novita/meta-llama/llama-3.2-3b-instruct":[3e-8,5e-8,null,null],"meta-llama/llama-3.2-3b-instruct":[3e-8,5e-8,null,null],"novita/sao10k/l31-70b-euryale-v2.2":[0.00000148,0.00000148,null,null],"sao10k/l31-70b-euryale-v2.2":[0.00000148,0.00000148,null,null],"novita/qwen/qwen3-embedding-0.6b":[7e-8,0,null,null],"qwen/qwen3-embedding-0.6b":[7e-8,0,null,null],"novita/qwen/qwen3-embedding-8b":[7e-8,0,null,null],"qwen/qwen3-embedding-8b":[7e-8,0,null,null],"novita/baai/bge-m3":[1e-8,1e-8,null,null],"baai/bge-m3":[1e-8,1e-8,null,null],"novita/qwen/qwen3-reranker-8b":[5e-8,5e-8,null,null],"qwen/qwen3-reranker-8b":[5e-8,5e-8,null,null],"novita/baai/bge-reranker-v2-m3":[1e-8,1e-8,null,null],"baai/bge-reranker-v2-m3":[1e-8,1e-8,null,null],"llamagate/llama-3.1-8b":[3e-8,5e-8,null,null],"llama-3.1-8b":[3e-8,5e-8,null,null],"llamagate/llama-3.2-3b":[4e-8,8e-8,null,null],"llama-3.2-3b":[4e-8,8e-8,null,null],"llamagate/mistral-7b-v0.3":[1e-7,1.5e-7,null,null],"mistral-7b-v0.3":[1e-7,1.5e-7,null,null],"llamagate/qwen3-8b":[4e-8,1.4e-7,null,null],"qwen3-8b":[4e-8,1.4e-7,null,null],"llamagate/dolphin3-8b":[8e-8,1.5e-7,null,null],"dolphin3-8b":[8e-8,1.5e-7,null,null],"llamagate/deepseek-r1-8b":[1e-7,2e-7,null,null],"deepseek-r1-8b":[1e-7,2e-7,null,null],"llamagate/deepseek-r1-7b-qwen":[8e-8,1.5e-7,null,null],"deepseek-r1-7b-qwen":[8e-8,1.5e-7,null,null],"llamagate/openthinker-7b":[8e-8,1.5e-7,null,null],"openthinker-7b":[8e-8,1.5e-7,null,null],"llamagate/qwen2.5-coder-7b":[6e-8,1.2e-7,null,null],"qwen2.5-coder-7b":[6e-8,1.2e-7,null,null],"llamagate/deepseek-coder-6.7b":[6e-8,1.2e-7,null,null],"deepseek-coder-6.7b":[6e-8,1.2e-7,null,null],"llamagate/codellama-7b":[6e-8,1.2e-7,null,null],"codellama-7b":[6e-8,1.2e-7,null,null],"llamagate/qwen3-vl-8b":[1.5e-7,5.5e-7,null,null],"qwen3-vl-8b":[1.5e-7,5.5e-7,null,null],"llamagate/llava-7b":[1e-7,2e-7,null,null],"llava-7b":[1e-7,2e-7,null,null],"llamagate/gemma3-4b":[3e-8,8e-8,null,null],"gemma3-4b":[3e-8,8e-8,null,null],"llamagate/nomic-embed-text":[2e-8,0,null,null],"nomic-embed-text":[2e-8,0,null,null],"llamagate/qwen3-embedding-8b":[2e-8,0,null,null],"qwen3-embedding-8b":[2e-8,0,null,null],"sarvam/sarvam-m":[0,0,0,0],"sarvam-m":[0,0,0,0],"gemini/gemini-2.0-flash-exp-image-generation":[0,0,null,null],"gemini/gemini-2.0-flash-lite-001":[7.5e-8,3e-7,null,1.875e-8],"gemini/gemini-2.5-flash-native-audio-latest":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-flash-native-audio-preview-09-2025":[3e-7,0.0000025,null,null],"gemini/gemini-2.5-flash-native-audio-preview-12-2025":[3e-7,0.0000025,null,null],"gemini/gemini-3.1-flash-live-preview":[7.5e-7,0.0000045,null,null],"gemini/gemini-pro-latest":[0.00000125,0.00001,null,1.25e-7],"vertex_ai/claude-sonnet-4-6@default":[0.000003,0.000015,0.00000375,3e-7],"claude-sonnet-4-6@default":[0.000003,0.000015,0.00000375,3e-7],"bedrock_mantle/openai.gpt-oss-120b":[1.5e-7,6e-7,null,null],"openai.gpt-oss-120b":[1.5e-7,6e-7,null,null],"bedrock_mantle/openai.gpt-oss-20b":[7.5e-8,3e-7,null,null],"openai.gpt-oss-20b":[7.5e-8,3e-7,null,null],"bedrock_mantle/openai.gpt-oss-safeguard-120b":[1.5e-7,6e-7,null,null],"bedrock_mantle/openai.gpt-oss-safeguard-20b":[7.5e-8,3e-7,null,null],"bedrock/us-east-1/zai.glm-5":[0.000001,0.0000032,null,null],"us-east-1/zai.glm-5":[0.000001,0.0000032,null,null],"bedrock/us-west-2/zai.glm-5":[0.000001,0.0000032,null,null],"us-west-2/zai.glm-5":[0.000001,0.0000032,null,null],"bedrock/us-gov-east-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"us-gov-east-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"bedrock/us-gov-west-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"us-gov-west-1/anthropic.claude-haiku-4-5-20251001-v1:0":[0.0000012,0.000006,0.0000015,1.2e-7],"MiniMax-M2.7-highspeed":[6e-7,0.0000024,3.75e-7,6e-8]}
\ No newline at end of file
diff --git a/src/export.ts b/src/export.ts
index 4e1afc1..70b669c 100644
--- a/src/export.ts
+++ b/src/export.ts
@@ -1,9 +1,10 @@
-import { writeFile, mkdir, readdir, stat, rm } from 'fs/promises'
+import { writeFile, mkdir, readdir, open, stat, rm } from 'fs/promises'
import { dirname, join, resolve } from 'path'
import { CATEGORY_LABELS, type ProjectSummary, type TaskCategory } from './types.js'
-import { getCurrency, convertCost } from './currency.js'
+import { getCurrency, convertCost, roundForActiveCurrency } from './currency.js'
import { dateKey } from './day-aggregator.js'
+import { aggregateModelEfficiency } from './model-efficiency.js'
function escCsv(s: string): string {
const sanitized = /^[\t\r=+\-@]/.test(s) ? `'${s}` : s
@@ -69,7 +70,7 @@ function buildDailyRows(projects: ProjectSummary[], period: string): Row[] {
return Object.entries(daily).sort().map(([date, d]) => ({
Period: period,
Date: date,
- [`Cost (${code})`]: round2(convertCost(d.cost)),
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(d.cost)),
'API Calls': d.calls,
Sessions: d.sessions.size,
'Input Tokens': d.input,
@@ -97,7 +98,7 @@ function buildActivityRows(projects: ProjectSummary[], period: string): Row[] {
.map(([cat, d]) => ({
Period: period,
Activity: CATEGORY_LABELS[cat as TaskCategory] ?? cat,
- [`Cost (${code})`]: round2(convertCost(d.cost)),
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(d.cost)),
'Share (%)': pct(d.cost, totalCost),
Turns: d.turns,
}))
@@ -105,6 +106,7 @@ function buildActivityRows(projects: ProjectSummary[], period: string): Row[] {
function buildModelRows(projects: ProjectSummary[], period: string): Row[] {
const modelTotals: Record = {}
+ const modelEfficiency = aggregateModelEfficiency(projects)
for (const project of projects) {
for (const session of project.sessions) {
for (const [model, d] of Object.entries(session.modelBreakdown)) {
@@ -123,17 +125,26 @@ function buildModelRows(projects: ProjectSummary[], period: string): Row[] {
return Object.entries(modelTotals)
.filter(([name]) => name !== '')
.sort(([, a], [, b]) => b.cost - a.cost)
- .map(([model, d]) => ({
- Period: period,
- Model: model,
- [`Cost (${code})`]: round2(convertCost(d.cost)),
- 'Share (%)': pct(d.cost, totalCost),
- 'API Calls': d.calls,
- 'Input Tokens': d.input,
- 'Output Tokens': d.output,
- 'Cache Read Tokens': d.cacheRead,
- 'Cache Write Tokens': d.cacheWrite,
- }))
+ .map(([model, d]) => {
+ const efficiency = modelEfficiency.get(model)
+ return {
+ Period: period,
+ Model: model,
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(d.cost)),
+ 'Share (%)': pct(d.cost, totalCost),
+ 'API Calls': d.calls,
+ 'Edit Turns': efficiency?.editTurns ?? 0,
+ 'One-shot Rate (%)': efficiency?.oneShotRate ?? '',
+ 'Retries/Edit': efficiency?.retriesPerEdit ?? '',
+ [`Cost/Edit (${code})`]: efficiency?.costPerEditUSD !== null && efficiency?.costPerEditUSD !== undefined
+ ? roundForActiveCurrency(convertCost(efficiency.costPerEditUSD))
+ : '',
+ 'Input Tokens': d.input,
+ 'Output Tokens': d.output,
+ 'Cache Read Tokens': d.cacheRead,
+ 'Cache Write Tokens': d.cacheWrite,
+ }
+ })
}
function buildToolRows(projects: ProjectSummary[]): Row[] {
@@ -182,8 +193,8 @@ function buildProjectRows(projects: ProjectSummary[]): Row[] {
.sort((a, b) => b.totalCostUSD - a.totalCostUSD)
.map(p => ({
Project: p.projectPath,
- [`Cost (${code})`]: round2(convertCost(p.totalCostUSD)),
- [`Avg/Session (${code})`]: p.sessions.length > 0 ? round2(convertCost(p.totalCostUSD / p.sessions.length)) : '',
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(p.totalCostUSD)),
+ [`Avg/Session (${code})`]: p.sessions.length > 0 ? roundForActiveCurrency(convertCost(p.totalCostUSD / p.sessions.length)) : '',
'Share (%)': pct(p.totalCostUSD, total),
'API Calls': p.totalApiCalls,
Sessions: p.sessions.length,
@@ -199,7 +210,7 @@ function buildSessionRows(projects: ProjectSummary[]): Row[] {
Project: p.projectPath,
'Session ID': s.sessionId,
'Started At': s.firstTimestamp ?? '',
- [`Cost (${code})`]: round2(convertCost(s.totalCostUSD)),
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(s.totalCostUSD)),
'API Calls': s.apiCalls,
Turns: s.turns.length,
})
@@ -222,7 +233,7 @@ function buildSummaryRows(periods: PeriodExport[]): Row[] {
const projectCount = p.projects.filter(proj => proj.totalCostUSD > 0).length
return {
Period: p.label,
- [`Cost (${code})`]: round2(convertCost(cost)),
+ [`Cost (${code})`]: roundForActiveCurrency(convertCost(cost)),
'API Calls': calls,
Sessions: sessions,
Projects: projectCount,
@@ -247,10 +258,10 @@ function buildReadme(periods: PeriodExport[]): string {
' daily.csv Day-by-day breakdown, Period column distinguishes the window.',
' activity.csv Time spent per task category (Coding, Debugging, Exploration, etc.).',
' models.csv Spend per model with token totals and cache usage.',
- ' projects.csv Spend per project folder (30-day window).',
- ' sessions.csv One row per session (30-day window) with session IDs and costs.',
- ' tools.csv Tool invocations and share (30-day window).',
- ' shell-commands.csv Shell commands executed via Bash tool (30-day window).',
+ ' projects.csv Spend per project folder for the selected detail period.',
+ ' sessions.csv One row per session for the selected detail period.',
+ ' tools.csv Tool invocations and share for the selected detail period.',
+ ' shell-commands.csv Shell commands executed via Bash tool for the selected detail period.',
'',
'Notes',
'-----',
@@ -346,6 +357,33 @@ export async function exportJson(periods: PeriodExport[], outputPath: string): P
}
const target = resolve(outputPath.toLowerCase().endsWith('.json') ? outputPath : `${outputPath}.json`)
+ // Refuse to overwrite an existing file that wasn't produced by codeburn
+ // export. CSV path has the same guard via the .codeburn-export marker; JSON
+ // was missing it, so a stray `-o ~/important.json` would silently clobber.
+ const existing = await stat(target).catch(() => null)
+ if (existing?.isFile()) {
+ // Read just the first 4KB to look for the schema marker. The schema key
+ // is the first field in the JSON object so a partial read is enough;
+ // loading the whole file (potentially gigabytes) into memory could OOM
+ // on Node's ~512MB string limit.
+ const fh = await open(target, 'r')
+ try {
+ const buf = Buffer.alloc(4096)
+ const { bytesRead } = await fh.read(buf, 0, buf.length, 0)
+ const head = buf.toString('utf-8', 0, bytesRead)
+ if (!head.includes('"schema": "codeburn.export.v')) {
+ throw new Error(
+ `Refusing to overwrite ${target}: file does not look like a codeburn export. ` +
+ `Delete it manually or pick a different -o path.`
+ )
+ }
+ } finally {
+ await fh.close()
+ }
+ }
+ if (existing?.isDirectory()) {
+ throw new Error(`Refusing to overwrite directory at ${target}. Pass a file path instead.`)
+ }
await mkdir(dirname(target), { recursive: true })
await writeFile(target, JSON.stringify(data, null, 2), 'utf-8')
return target
diff --git a/src/format.ts b/src/format.ts
index ee44619..826c04c 100644
--- a/src/format.ts
+++ b/src/format.ts
@@ -8,9 +8,13 @@ import { formatCost } from './currency.js'
export { formatCost }
export function formatTokens(n: number): string {
+ // Guard against Infinity / NaN / negatives that would otherwise leak into
+ // the UI as "Infinity" or "NaN" strings when an upstream calculation glitches.
+ if (!Number.isFinite(n)) return '?'
+ if (n < 0) return '0'
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`
- return n.toString()
+ return Math.round(n).toString()
}
/// Returns YYYY-MM-DD for the given date in the process-local timezone. Cheaper than shelling
diff --git a/src/fs-utils.ts b/src/fs-utils.ts
index 823a630..cc46939 100644
--- a/src/fs-utils.ts
+++ b/src/fs-utils.ts
@@ -1,12 +1,18 @@
import { readFile, stat } from 'fs/promises'
import { readFileSync, statSync, createReadStream } from 'fs'
-import { createInterface } from 'readline'
-// Hard cap well below V8's 512 MB string limit even with split('\n') doubling.
-// Stream threshold chosen as empirical breakeven between readFile+split peak
-// memory and createReadStream+readline overhead for typical session files.
+// Hard cap well below V8's 512 MB string limit. Callers that need line-by-line
+// processing should use readSessionLines(), which avoids materializing the
+// whole file and can return large lines as Buffers.
export const MAX_SESSION_FILE_BYTES = 128 * 1024 * 1024
-export const STREAM_THRESHOLD_BYTES = 8 * 1024 * 1024
+export const LARGE_STREAM_LINE_BYTES = 32 * 1024
+
+// Line-by-line streaming has bounded memory (one line at a time) and is not
+// constrained by V8's string limit, so it can safely handle multi-GB session
+// files. The cap here is purely a sanity check against pathological inputs;
+// real Codex sessions for heavy users have been observed at 250+ MB and will
+// continue to grow as context windows expand.
+export const MAX_STREAM_SESSION_FILE_BYTES = 2 * 1024 * 1024 * 1024
function verbose(): boolean {
return process.env.CODEBURN_VERBOSE === '1'
@@ -16,14 +22,6 @@ function warn(msg: string): void {
if (verbose()) process.stderr.write(`codeburn: ${msg}\n`)
}
-async function readViaStream(filePath: string): Promise {
- const chunks: string[] = []
- const stream = createReadStream(filePath, { encoding: 'utf-8' })
- const rl = createInterface({ input: stream, crlfDelay: Infinity })
- for await (const line of rl) chunks.push(line)
- return chunks.join('\n')
-}
-
export async function readSessionFile(filePath: string): Promise {
let size: number
try {
@@ -39,7 +37,6 @@ export async function readSessionFile(filePath: string): Promise
}
try {
- if (size >= STREAM_THRESHOLD_BYTES) return await readViaStream(filePath)
return await readFile(filePath, 'utf-8')
} catch (err) {
warn(`read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
@@ -69,7 +66,29 @@ export function readSessionFileSync(filePath: string): string | null {
}
}
-export async function* readSessionLines(filePath: string): AsyncGenerator {
+export type SessionLine = string | Buffer
+
+type ReadSessionLinesOptions = {
+ largeLineAsBuffer?: boolean
+ largeLineThresholdBytes?: number
+ startByteOffset?: number
+ byteOffsetTracker?: { lastCompleteLineOffset: number }
+}
+
+export function readSessionLines(
+ filePath: string,
+ shouldSkipHead?: (head: string) => boolean,
+): AsyncGenerator
+export function readSessionLines(
+ filePath: string,
+ shouldSkipHead?: (head: string) => boolean,
+ options?: ReadSessionLinesOptions & { largeLineAsBuffer: true },
+): AsyncGenerator
+export async function* readSessionLines(
+ filePath: string,
+ shouldSkipHead?: (head: string) => boolean,
+ options: ReadSessionLinesOptions = {},
+): AsyncGenerator {
let size: number
try {
size = (await stat(filePath)).size
@@ -78,15 +97,116 @@ export async function* readSessionLines(filePath: string): AsyncGenerator MAX_SESSION_FILE_BYTES) {
- warn(`skipped oversize file ${filePath} (${size} bytes > cap ${MAX_SESSION_FILE_BYTES})`)
+ if (size > MAX_STREAM_SESSION_FILE_BYTES) {
+ warn(
+ `skipped oversize file ${filePath} (${size} bytes > stream cap ${MAX_STREAM_SESSION_FILE_BYTES})`,
+ )
return
}
- const stream = createReadStream(filePath, { encoding: 'utf-8' })
- const rl = createInterface({ input: stream, crlfDelay: Infinity })
+ const stream = createReadStream(
+ filePath,
+ options.startByteOffset !== undefined ? { start: options.startByteOffset } : undefined,
+ )
+ const SKIP_HEAD = 2048
+ const largeLineThreshold = options.largeLineThresholdBytes ?? LARGE_STREAM_LINE_BYTES
+ const formatLine = (buf: Buffer, lineLen: number, head?: string): SessionLine => {
+ if (options.largeLineAsBuffer && lineLen > largeLineThreshold) return buf
+ return head !== undefined && lineLen <= SKIP_HEAD ? head : buf.toString('utf-8')
+ }
+ let parts: Buffer[] = []
+ let len = 0
+ let skipping = false
+ let headChecked = false
+ let chunkBase = options.startByteOffset ?? 0
+ const tracker = options.byteOffsetTracker
+
try {
- for await (const line of rl) yield line
+ for await (const raw of stream) {
+ const chunk = raw as Buffer
+ let pos = 0
+
+ while (pos < chunk.length) {
+ const nl = chunk.indexOf(0x0a, pos)
+
+ if (skipping) {
+ if (nl === -1) {
+ pos = chunk.length
+ } else {
+ if (tracker) tracker.lastCompleteLineOffset = chunkBase + nl + 1
+ skipping = false
+ pos = nl + 1
+ }
+ continue
+ }
+
+ if (nl !== -1) {
+ if (pos < nl) {
+ parts.push(chunk.subarray(pos, nl))
+ len += nl - pos
+ }
+ pos = nl + 1
+ if (tracker) tracker.lastCompleteLineOffset = chunkBase + pos
+
+ if (len === 0) {
+ parts = []
+ headChecked = false
+ continue
+ }
+
+ const buf = parts.length === 1 ? parts[0]! : Buffer.concat(parts, len)
+ const lineLen = len
+ parts = []
+ len = 0
+ headChecked = false
+
+ if (shouldSkipHead) {
+ const head = lineLen > SKIP_HEAD
+ ? buf.subarray(0, SKIP_HEAD).toString('utf-8')
+ : buf.toString('utf-8')
+ if (shouldSkipHead(head)) continue
+ yield formatLine(buf, lineLen, head)
+ } else {
+ yield formatLine(buf, lineLen)
+ }
+ } else {
+ const slice = chunk.subarray(pos)
+ parts.push(slice)
+ len += slice.length
+ pos = chunk.length
+
+ // Mid-line skip: once we have enough bytes to check the head,
+ // enter scanning mode — just look for \n without accumulating.
+ if (shouldSkipHead && !headChecked && len >= SKIP_HEAD) {
+ headChecked = true
+ const headBuf = parts.length === 1
+ ? parts[0]!.subarray(0, SKIP_HEAD)
+ : Buffer.concat(parts, len).subarray(0, SKIP_HEAD)
+ if (shouldSkipHead(headBuf.toString('utf-8'))) {
+ skipping = true
+ parts = []
+ len = 0
+ }
+ }
+ }
+ }
+ chunkBase += chunk.length
+ }
+
+ if (!skipping && len > 0) {
+ const buf = parts.length === 1 ? parts[0]! : Buffer.concat(parts, len)
+ const lineLen = len
+ if (shouldSkipHead) {
+ const head = lineLen > SKIP_HEAD
+ ? buf.subarray(0, SKIP_HEAD).toString('utf-8')
+ : buf.toString('utf-8')
+ if (!shouldSkipHead(head)) {
+ yield formatLine(buf, lineLen, head)
+ }
+ } else {
+ yield formatLine(buf, lineLen)
+ }
+ }
} catch (err) {
warn(`stream read failed for ${filePath}: ${(err as NodeJS.ErrnoException).code ?? 'unknown'}`)
} finally {
diff --git a/src/ink-win.ts b/src/ink-win.ts
new file mode 100644
index 0000000..5fd4bad
--- /dev/null
+++ b/src/ink-win.ts
@@ -0,0 +1,14 @@
+const BSU = '\x1b[?2026h'
+const ESU = '\x1b[?2026l'
+let patched = false
+
+export function patchStdoutForWindows(): void {
+ if (process.platform !== 'win32' || patched) return
+ patched = true
+
+ const origWrite = process.stdout.write.bind(process.stdout)
+ process.stdout.write = function (chunk: unknown, ...args: unknown[]): boolean {
+ if (chunk === BSU || chunk === ESU) return true
+ return (origWrite as Function)(chunk, ...args)
+ } as typeof process.stdout.write
+}
diff --git a/src/main.ts b/src/main.ts
new file mode 100644
index 0000000..19e43e3
--- /dev/null
+++ b/src/main.ts
@@ -0,0 +1,988 @@
+import { Command } from 'commander'
+import { installMenubarApp } from './menubar-installer.js'
+import { exportCsv, exportJson, type PeriodExport } from './export.js'
+import { loadPricing, setModelAliases } from './models.js'
+import { parseAllSessions, filterProjectsByName, filterProjectsByDateRange, clearSessionCache } from './parser.js'
+import { convertCost } from './currency.js'
+import { renderStatusBar } from './format.js'
+import { type PeriodData, type ProviderCost } from './menubar-json.js'
+import { buildMenubarPayload } from './menubar-json.js'
+import { getDaysInRange, ensureCacheHydrated, emptyCache, BACKFILL_DAYS, toDateString } from './daily-cache.js'
+import { aggregateProjectsIntoDays, buildPeriodDataFromDays, dateKey } from './day-aggregator.js'
+import { CATEGORY_LABELS, type DateRange, type ProjectSummary, type TaskCategory } from './types.js'
+import { aggregateModelEfficiency } from './model-efficiency.js'
+import { renderDashboard } from './dashboard.js'
+import { formatDateRangeLabel, parseDateRangeFlags, getDateRange, toPeriod, type Period } from './cli-date.js'
+import { runOptimize, scanAndDetect } from './optimize.js'
+import { renderCompare } from './compare.js'
+import { getAllProviders } from './providers/index.js'
+import { clearPlan, readConfig, readPlan, saveConfig, savePlan, getConfigFilePath, type PlanId } from './config.js'
+import { clampResetDay, getPlanUsageOrNull, type PlanUsage } from './plan-usage.js'
+import { getPresetPlan, isPlanId, isPlanProvider, planDisplayName } from './plans.js'
+import { createRequire } from 'node:module'
+
+const require = createRequire(import.meta.url)
+const { version } = require('../package.json')
+import { loadCurrency, getCurrency, isValidCurrencyCode } from './currency.js'
+
+async function hydrateCache() {
+ try {
+ return await ensureCacheHydrated(
+ (range) => parseAllSessions(range, 'all'),
+ aggregateProjectsIntoDays,
+ )
+ } catch {
+ return emptyCache()
+ }
+}
+
+function collect(val: string, acc: string[]): string[] {
+ acc.push(val)
+ return acc
+}
+
+function parseNumber(value: string): number {
+ return Number(value)
+}
+
+function parseInteger(value: string): number {
+ return parseInt(value, 10)
+}
+
+type JsonPlanSummary = {
+ id: PlanId
+ budget: number
+ spent: number
+ percentUsed: number
+ status: 'under' | 'near' | 'over'
+ projectedMonthEnd: number
+ daysUntilReset: number
+ periodStart: string
+ periodEnd: string
+}
+
+function toJsonPlanSummary(planUsage: PlanUsage): JsonPlanSummary {
+ return {
+ id: planUsage.plan.id,
+ budget: convertCost(planUsage.budgetUsd),
+ spent: convertCost(planUsage.spentApiEquivalentUsd),
+ percentUsed: Math.round(planUsage.percentUsed * 10) / 10,
+ status: planUsage.status,
+ projectedMonthEnd: convertCost(planUsage.projectedMonthUsd),
+ daysUntilReset: planUsage.daysUntilReset,
+ periodStart: planUsage.periodStart.toISOString(),
+ periodEnd: planUsage.periodEnd.toISOString(),
+ }
+}
+
+function assertFormat(value: string, allowed: readonly string[], command: string): void {
+ if (!allowed.includes(value)) {
+ process.stderr.write(
+ `codeburn ${command}: unknown format "${value}". Valid values: ${allowed.join(', ')}.\n`
+ )
+ process.exit(1)
+ }
+}
+
+async function runJsonReport(period: Period, provider: string, project: string[], exclude: string[]): Promise {
+ await loadPricing()
+ const { range, label } = getDateRange(period)
+ const projects = filterProjectsByName(await parseAllSessions(range, provider), project, exclude)
+ const report: ReturnType & { plan?: JsonPlanSummary } = buildJsonReport(projects, label, period)
+ const planUsage = await getPlanUsageOrNull()
+ if (planUsage) {
+ report.plan = toJsonPlanSummary(planUsage)
+ }
+ console.log(JSON.stringify(report, null, 2))
+}
+
+const program = new Command()
+ .name('codeburn')
+ .description('See where your AI coding tokens go - by task, tool, model, and project')
+ .version(version)
+ .option('--verbose', 'print warnings to stderr on read failures and skipped files')
+ .option('--timezone ', 'IANA timezone for date grouping (e.g. Asia/Tokyo, America/New_York)')
+
+program.hook('preAction', async (thisCommand) => {
+ const tz = thisCommand.opts<{ timezone?: string }>().timezone ?? process.env['CODEBURN_TZ']
+ if (tz) {
+ try {
+ Intl.DateTimeFormat(undefined, { timeZone: tz })
+ } catch {
+ console.error(`\n Invalid timezone: "${tz}". Use an IANA timezone like "America/New_York" or "Asia/Tokyo".\n`)
+ process.exit(1)
+ }
+ process.env.TZ = tz
+ }
+ const config = await readConfig()
+ setModelAliases(config.modelAliases ?? {})
+ if (thisCommand.opts<{ verbose?: boolean }>().verbose) {
+ process.env['CODEBURN_VERBOSE'] = '1'
+ }
+ await loadCurrency()
+})
+
+function buildJsonReport(projects: ProjectSummary[], period: string, periodKey: string) {
+ const sessions = projects.flatMap(p => p.sessions)
+ const { code } = getCurrency()
+
+ const totalCostUSD = projects.reduce((s, p) => s + p.totalCostUSD, 0)
+ const totalCalls = projects.reduce((s, p) => s + p.totalApiCalls, 0)
+ const totalSessions = projects.reduce((s, p) => s + p.sessions.length, 0)
+ const totalInput = sessions.reduce((s, sess) => s + sess.totalInputTokens, 0)
+ const totalOutput = sessions.reduce((s, sess) => s + sess.totalOutputTokens, 0)
+ const totalCacheRead = sessions.reduce((s, sess) => s + sess.totalCacheReadTokens, 0)
+ const totalCacheWrite = sessions.reduce((s, sess) => s + sess.totalCacheWriteTokens, 0)
+ // Match src/menubar-json.ts:cacheHitPercent: reads over reads+fresh-input. cache_write
+ // counts tokens being stored, not served, so it doesn't belong in the denominator.
+ const cacheHitDenom = totalInput + totalCacheRead
+ const cacheHitPercent = cacheHitDenom > 0 ? Math.round((totalCacheRead / cacheHitDenom) * 1000) / 10 : 0
+
+ // Per-day rollup. Mirrors parser.ts categoryBreakdown semantics so a
+ // consumer summing daily[].editTurns over a period gets the same total as
+ // sum(activities[].editTurns) for that period: every turn counts once for
+ // `turns`, edit turns count for `editTurns`, edit turns with zero retries
+ // count for `oneShotTurns`. Issue #279 — daily-resolution efficiency
+ // dashboards need this without re-deriving from activity-level rollups.
+ const dailyMap: Record = {}
+ for (const sess of sessions) {
+ for (const turn of sess.turns) {
+ // Prefer the user-message timestamp on the turn; fall back to the first
+ // assistant-call timestamp when the user line is missing (continuation
+ // sessions where the JSONL begins mid-conversation). Previously these
+ // turns dropped from daily but stayed in activities, breaking the
+ // sum(daily[].editTurns) === sum(activities[].editTurns) invariant.
+ const ts = turn.timestamp || turn.assistantCalls[0]?.timestamp
+ if (!ts) { continue }
+ const day = dateKey(ts)
+ if (!dailyMap[day]) { dailyMap[day] = { cost: 0, calls: 0, turns: 0, editTurns: 0, oneShotTurns: 0 } }
+ dailyMap[day].turns += 1
+ if (turn.hasEdits) {
+ dailyMap[day].editTurns += 1
+ if (turn.retries === 0) dailyMap[day].oneShotTurns += 1
+ }
+ for (const call of turn.assistantCalls) {
+ dailyMap[day].cost += call.costUSD
+ dailyMap[day].calls += 1
+ }
+ }
+ }
+ const daily = Object.entries(dailyMap).sort().map(([date, d]) => ({
+ date,
+ cost: convertCost(d.cost),
+ calls: d.calls,
+ turns: d.turns,
+ editTurns: d.editTurns,
+ oneShotTurns: d.oneShotTurns,
+ // Pre-computed convenience for dashboards that don't want to do the math.
+ // null when there are no edit turns (the rate is undefined, not zero —
+ // a day where the user only had Q&A turns shouldn't read as 0% one-shot).
+ oneShotRate: d.editTurns > 0
+ ? Math.round((d.oneShotTurns / d.editTurns) * 1000) / 10
+ : null,
+ }))
+
+ const projectList = projects.map(p => ({
+ name: p.project,
+ path: p.projectPath,
+ cost: convertCost(p.totalCostUSD),
+ avgCostPerSession: p.sessions.length > 0
+ ? convertCost(p.totalCostUSD / p.sessions.length)
+ : null,
+ calls: p.totalApiCalls,
+ sessions: p.sessions.length,
+ }))
+
+ const modelMap: Record = {}
+ const modelEfficiency = aggregateModelEfficiency(projects)
+ for (const sess of sessions) {
+ for (const [model, d] of Object.entries(sess.modelBreakdown)) {
+ if (!modelMap[model]) { modelMap[model] = { calls: 0, cost: 0, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 0 } }
+ modelMap[model].calls += d.calls
+ modelMap[model].cost += d.costUSD
+ modelMap[model].inputTokens += d.tokens.inputTokens
+ modelMap[model].outputTokens += d.tokens.outputTokens
+ modelMap[model].cacheReadTokens += d.tokens.cacheReadInputTokens
+ modelMap[model].cacheWriteTokens += d.tokens.cacheCreationInputTokens
+ }
+ }
+ const models = Object.entries(modelMap)
+ .sort(([, a], [, b]) => b.cost - a.cost)
+ .map(([name, { cost, ...rest }]) => {
+ const efficiency = modelEfficiency.get(name)
+ return {
+ name,
+ ...rest,
+ cost: convertCost(cost),
+ editTurns: efficiency?.editTurns ?? 0,
+ oneShotTurns: efficiency?.oneShotTurns ?? 0,
+ oneShotRate: efficiency?.oneShotRate ?? null,
+ retriesPerEdit: efficiency?.retriesPerEdit ?? null,
+ costPerEdit: efficiency?.costPerEditUSD !== null && efficiency?.costPerEditUSD !== undefined
+ ? convertCost(efficiency.costPerEditUSD)
+ : null,
+ }
+ })
+
+ const catMap: Record = {}
+ for (const sess of sessions) {
+ for (const [cat, d] of Object.entries(sess.categoryBreakdown)) {
+ if (!catMap[cat]) { catMap[cat] = { turns: 0, cost: 0, editTurns: 0, oneShotTurns: 0 } }
+ catMap[cat].turns += d.turns
+ catMap[cat].cost += d.costUSD
+ catMap[cat].editTurns += d.editTurns
+ catMap[cat].oneShotTurns += d.oneShotTurns
+ }
+ }
+ const activities = Object.entries(catMap)
+ .sort(([, a], [, b]) => b.cost - a.cost)
+ .map(([cat, d]) => ({
+ category: CATEGORY_LABELS[cat as TaskCategory] ?? cat,
+ cost: convertCost(d.cost),
+ turns: d.turns,
+ editTurns: d.editTurns,
+ oneShotTurns: d.oneShotTurns,
+ oneShotRate: d.editTurns > 0 ? Math.round((d.oneShotTurns / d.editTurns) * 1000) / 10 : null,
+ }))
+
+ const toolMap: Record = {}
+ const mcpMap: Record = {}
+ const bashMap: Record = {}
+ for (const sess of sessions) {
+ for (const [tool, d] of Object.entries(sess.toolBreakdown)) {
+ toolMap[tool] = (toolMap[tool] ?? 0) + d.calls
+ }
+ for (const [server, d] of Object.entries(sess.mcpBreakdown)) {
+ mcpMap[server] = (mcpMap[server] ?? 0) + d.calls
+ }
+ for (const [cmd, d] of Object.entries(sess.bashBreakdown)) {
+ bashMap[cmd] = (bashMap[cmd] ?? 0) + d.calls
+ }
+ }
+
+ const sortedMap = (m: Record) =>
+ Object.entries(m).sort(([, a], [, b]) => b - a).map(([name, calls]) => ({ name, calls }))
+
+ const topSessions = projects
+ .flatMap(p => p.sessions.map(s => ({ project: p.project, sessionId: s.sessionId, date: s.firstTimestamp ? dateKey(s.firstTimestamp) : null, cost: convertCost(s.totalCostUSD), calls: s.apiCalls })))
+ .sort((a, b) => b.cost - a.cost)
+ .slice(0, 5)
+
+ return {
+ generated: new Date().toISOString(),
+ currency: code,
+ period,
+ periodKey,
+ overview: {
+ cost: convertCost(totalCostUSD),
+ calls: totalCalls,
+ sessions: totalSessions,
+ cacheHitPercent,
+ tokens: {
+ input: totalInput,
+ output: totalOutput,
+ cacheRead: totalCacheRead,
+ cacheWrite: totalCacheWrite,
+ },
+ },
+ daily,
+ projects: projectList,
+ models,
+ activities,
+ tools: sortedMap(toolMap),
+ mcpServers: sortedMap(mcpMap),
+ shellCommands: sortedMap(bashMap),
+ topSessions,
+ }
+}
+
+program
+ .command('report', { isDefault: true })
+ .description('Interactive usage dashboard')
+ .option('-p, --period ', 'Starting period: today, week, 30days, month, all', 'week')
+ .option('--from ', 'Start date (YYYY-MM-DD). Overrides --period when set')
+ .option('--to ', 'End date (YYYY-MM-DD). Overrides --period when set')
+ .option('--provider ', 'Filter by provider (e.g. claude, gemini, cursor, copilot)', 'all')
+ .option('--format ', 'Output format: tui, json', 'tui')
+ .option('--project ', 'Show only projects matching name (repeatable)', collect, [])
+ .option('--exclude ', 'Exclude projects matching name (repeatable)', collect, [])
+ .option('--refresh