diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..9af748a0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,18 @@ +## Summary + + + +## Testing + +- [ ] I have tested this locally against real data (not just unit tests) +- [ ] `npm test` passes +- [ ] `npm run build` succeeds + +### For new providers only: + +- [ ] I installed the tool and generated real sessions by using it +- [ ] `npm run dev -- today` shows correct costs and session counts for this provider +- [ ] `npm run dev -- models --provider ` shows correct model names and pricing +- [ ] Screenshot or terminal output attached below proving it works with real data + + diff --git a/.github/workflows/release-menubar.yml b/.github/workflows/release-menubar.yml index 990d4731..db413344 100644 --- a/.github/workflows/release-menubar.yml +++ b/.github/workflows/release-menubar.yml @@ -2,8 +2,8 @@ name: Release macOS Menubar # Triggers on a `mac-v*` tag push (e.g. `git tag mac-v0.8.0 && git push origin mac-v0.8.0`), # or manually via the Actions tab. Builds a universal arm64+x86_64 bundle, ad-hoc signs it, -# zips via `ditto`, and uploads the zip to the GitHub Release. `npx codeburn menubar` clears -# the download quarantine flag on install so Gatekeeper stays quiet. +# zips via `ditto`, and uploads the zip to the GitHub Release. The installer verifies +# the checksum and bundle identity before replacing the local app. on: push: tags: @@ -45,7 +45,9 @@ jobs: uses: actions/upload-artifact@v4 with: name: CodeBurnMenubar-${{ steps.version.outputs.value }} - path: mac/.build/dist/CodeBurnMenubar-*.zip + path: | + mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip + mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256 if-no-files-found: error - name: Create / update GitHub Release @@ -58,14 +60,16 @@ jobs: Install with: ``` - npx codeburn menubar + npm install -g codeburn + codeburn menubar ``` - That command drops the app into `~/Applications`, clears the download - quarantine, and launches it. If you download the zip from this page directly - and macOS shows "cannot verify developer", right-click the app in Finder and - pick Open to whitelist it once. + That command drops the app into `~/Applications`, records the persistent + `codeburn` CLI path used by the menubar, verifies the downloaded checksum, + clears quarantine after bundle verification, and launches it. If you download + the zip from this page directly and macOS shows "cannot verify developer", + right-click the app in Finder and pick Open to whitelist it once. files: | - mac/.build/dist/CodeBurnMenubar-*.zip - mac/.build/dist/CodeBurnMenubar-*.zip.sha256 + mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip + mac/.build/dist/CodeBurnMenubar-${{ steps.version.outputs.value }}.zip.sha256 fail_on_unmatched_files: true diff --git a/.gitignore b/.gitignore index abdd828a..431bdc25 100644 --- a/.gitignore +++ b/.gitignore @@ -40,3 +40,6 @@ assets/discord-*.png # Desktop app experiments desktop/ + +# WIP / not ready +src/summit.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index e7dd43d7..8b745428 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,100 @@ # Changelog +## Unreleased + +### Added (CLI) +- **Multiple subscription plans can be tracked at the same time.** + `codeburn plan set` now stores plans in a provider-keyed `plans` map, so + setting a Codex custom plan no longer overwrites an existing Claude plan. + `codeburn plan reset --provider ` removes only that provider's plan, + while `codeburn plan reset` remains a full reset. The dashboard and JSON + outputs include one overage summary per active provider plan. Aggregate + `all` plans remain mutually exclusive with provider-specific plans to avoid + double-counted overage rows. Existing single-plan `plan` config files + continue to load as a backward-compatible fallback, and subsequent writes + save the new `plans` map format. Preset plans now reject mismatched + `--provider` scopes instead of silently ignoring them. Closes #299. +- **Mistral Vibe provider.** CodeBurn now reads Mistral Vibe session folders + from `$VIBE_HOME/logs/session/` or `~/.vibe/logs/session/`, using + `meta.json` for cumulative prompt/completion tokens, model pricing, and + timestamps, and `messages.jsonl` for user prompts and tool calls. Subagent + sessions under a parent session's `agents/` folder are tracked separately. + Closes #283. +- **Kimi Code CLI provider.** CodeBurn now reads Kimi session usage from + `$KIMI_SHARE_DIR/sessions/` or `~/.kimi/sessions/`, including subagent + `wire.jsonl` files. The parser consumes Kimi's official `StatusUpdate` + token usage fields (`input_other`, `input_cache_read`, + `input_cache_creation`, `output`), normalizes Kimi tool names such as + `Shell`, `ReadFile`, and `WriteFile`, and maps hidden managed Kimi Code + model aliases to priced Kimi K2 entries. + +## 0.9.9 - 2026-05-15 + +### Added (CLI) +- **IBM Bob provider.** Discovers IBM Bob IDE task history, reuses the + Cline-family parser for token/cost records, extracts model tags and + workspace-based project names from session data. Closes #248. + +### Fixed (CLI) +- **Reduced Claude parser OOM risk.** Large Claude JSONL sessions retained + full entry objects (text, thinking blocks, tool results) in memory during + parsing, causing V8 heap exhaustion on heavy usage months. Entries are now + compacted immediately after JSON.parse, keeping only the fields needed for + cost/token aggregation. This is a mitigation - very heavy users may still + need the streaming parser refactor planned next. +- **Eager daily-cache hydration caused OOM on most CLI commands.** Eight + commands (report, today, month, export, optimize, compare, models, yield) + called `hydrateCache()` which parses a 365-day backfill, even though only + `status --format menubar-json` consumes the daily cache. Removed from all + paths that parse their own date ranges via `parseAllSessions`. +- **Session cache retained between status parses.** The `status --format json` + path parsed today and month ranges without clearing the in-process session + cache between them, keeping both result sets pinned. Cache is now cleared + after each period is consumed. +- **Claude 1-hour cache write pricing.** 1-hour cache writes are now priced + at 2x base input (previously used the 5-minute 1.25x rate for all writes). + Daily cache bumped to v6 so stale totals are recomputed. Closes #276. +- **OpenCode MCP usage now counted.** OpenCode stores MCP tool calls as + `_` names, which the shared MCP pipeline did not recognize. + The provider now normalizes these to the canonical `mcp____` + form so MCP breakdowns and `optimize` work correctly. Closes #308. +- **Antigravity Windows language-server discovery.** Antigravity detection now + supports Windows process discovery, `--extension_server_port`, + `--extension_server_csrf_token`, `--flag=value` syntax, and both wrapped and + unwrapped Connect-RPC response shapes. Closes #249. +- **Mangled project names in dashboard.** The By Project and Top Sessions + panels decoded slugs by splitting on `-`, which broke directory names + containing dashes or dots (e.g. `my-project` rendered as `my/project`). + Now uses the real project path instead. Closes #320. +- **Cursor undated bubble rows misattributed to Today.** Bubble rows without + a `createdAt` timestamp were defaulting to the current date, inflating + Today's spend. Now skipped at both the SQL and application level. +- **Node version guard.** Running on Node < 22.13.0 now prints a clear + upgrade message instead of crashing with a cryptic `node:sqlite` parse + error. Closes #319. + +### Fixed (macOS menubar) +- **All-provider refresh OOM.** Refreshing with provider set to "All" could + exhaust the V8 heap on accounts with heavy session history. +- **Tab refresh recovery.** Switching tabs during a refresh no longer leaves + the panel in a stale loading state. +- **Stale cache recovery.** The menubar now detects and discards a corrupt or + outdated on-disk cache instead of rendering zeroes until the next restart. +- **Refresh timer hardening.** The 30-second auto-refresh timer is now + cancelled on sleep/wake and restarted cleanly, preventing overlapping + refreshes after lid-open. +- **Version display.** The settings panel now shows the version without the + `v` prefix for consistency with `codeburn --version`. + ## 0.9.8 - 2026-05-10 ### Added (CLI) +- **Cline provider support.** CodeBurn now reads Cline task usage from both + VS Code globalStorage (`saoudrizwan.claude-dev`) and Cline's + `~/.cline/data` task root. It reuses the existing Cline-family parser for + `ui_messages.json` usage entries, deduplicates migrated tasks by the newest + `ui_messages.json`, and exposes Cline in CLI provider filters, docs, and the + macOS menubar provider tabs. Closes #130. - **Multiple Claude config directories.** Set `CLAUDE_CONFIG_DIRS` to an OS-delimited list of paths (`:`-separated on POSIX, `;`-separated on Windows) to scan more than one Claude data directory in a single run. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 84b21f40..aebe0f23 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -84,6 +84,23 @@ The `.github/workflows/block-claude-coauthor.yml` workflow rejects any PR whose If a flagged PR rejects on this check, the workflow prints the exact rebase command to fix it. +## Before You Start + +**Comment on the issue first.** Before writing code for a feature or new provider, leave a comment on the relevant issue saying what you plan to do. Wait for a maintainer to confirm the approach. Unsolicited PRs that duplicate work already in progress or take an incompatible approach will be closed. + +**One PR at a time.** We will not review a second PR from you until the first is merged or closed. This keeps the review queue manageable and ensures each contribution gets proper attention. + +## Adding a New Provider + +New providers have the highest bar because broken parsing silently produces wrong data for users. Before opening a PR: + +1. **Install the tool and use it.** Generate real sessions by actually coding with the provider. We do this ourselves for every provider we ship. +2. **Test against real data.** Run `npm run dev -- today` and `npm run dev -- models` with your real sessions and confirm the output looks correct — costs are non-zero, model names resolve, session counts match what you see in the tool. +3. **Include proof in the PR.** Attach a screenshot or terminal output showing codeburn correctly parsing your real sessions. PRs for new providers without evidence of local testing will not be reviewed. +4. **Do not rely on AI-generated guesses about storage paths or schemas.** Tools change their data formats between versions. The only way to know the current schema is to install the tool and inspect the actual files on disk. + +PRs that add a provider based solely on online documentation or AI-generated code, without evidence of testing against real data, will be closed. + ## Pull Requests 1. Fork or branch from `main`. diff --git a/README.md b/README.md index b3700224..fc847c06 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ Sponsor

-CodeBurn tracks token usage, cost, and performance across **18 AI coding tools**. It breaks down spending by task type, model, tool, project, and provider so you can see exactly where your budget goes. +CodeBurn tracks token usage, cost, and performance across **19 AI coding tools**. It breaks down spending by task type, model, tool, project, and provider so you can see exactly where your budget goes. Everything runs locally. No wrapper, no proxy, no API keys. CodeBurn reads session data directly from disk and prices every call using [LiteLLM](https://github.com/BerriAI/litellm). @@ -99,11 +99,14 @@ Arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--fr |---|----------|-----------|-----| | | Claude Code | Yes | [claude.md](docs/providers/claude.md) | | | Claude Desktop | Yes | [claude.md](docs/providers/claude.md) | +| | Cline | Yes | [cline.md](docs/providers/cline.md) | | | Codex (OpenAI) | Yes | [codex.md](docs/providers/codex.md) | | | Cursor | Yes | [cursor.md](docs/providers/cursor.md) | | | cursor-agent | Yes | [cursor-agent.md](docs/providers/cursor-agent.md) | | | Gemini CLI | Yes | [gemini.md](docs/providers/gemini.md) | +| | Mistral Vibe | Yes | [mistral-vibe.md](docs/providers/mistral-vibe.md) | | | GitHub Copilot | Yes | [copilot.md](docs/providers/copilot.md) | +| | IBM Bob | Yes | [ibm-bob.md](docs/providers/ibm-bob.md) | | | Kiro | Yes | [kiro.md](docs/providers/kiro.md) | | | OpenCode | Yes | [opencode.md](docs/providers/opencode.md) | | | OpenClaw | Yes | [openclaw.md](docs/providers/openclaw.md) | @@ -113,13 +116,14 @@ Arrow keys switch between Today, 7 Days, 30 Days, Month, and 6 Months (use `--fr | | Roo Code | Yes | [roo-code.md](docs/providers/roo-code.md) | | | KiloCode | Yes | [kilo-code.md](docs/providers/kilo-code.md) | | | Qwen | Yes | [qwen.md](docs/providers/qwen.md) | +| | Kimi Code CLI | Yes | [kimi.md](docs/providers/kimi.md) | | | Goose | Yes | [goose.md](docs/providers/goose.md) | | | Antigravity | Yes | [antigravity.md](docs/providers/antigravity.md) | | | Crush | Yes | [crush.md](docs/providers/crush.md) | Each provider doc lists the exact data location, storage format, and known quirks. Linux and Windows paths are detected automatically. If a path has changed or is wrong, please [open an issue](https://github.com/getagentseal/codeburn/issues). -Provider logos are trademarks of their respective owners. The icon set was sourced from [tokscale](https://github.com/junhoyeo/tokscale) (MIT) plus official vendor assets, used under nominative fair use for the purpose of identifying supported tools. +Provider logos are trademarks of their respective owners. The icon set was sourced from [tokscale](https://github.com/junhoyeo/tokscale) (MIT), official vendor assets, and simple provider identifiers, used under nominative fair use for the purpose of identifying supported tools. CodeBurn auto-detects which AI coding tools you use. If multiple providers have session data on disk, press `p` in the dashboard to toggle between them. @@ -131,6 +135,8 @@ The `--provider` flag filters any command to a single provider: `codeburn report **Gemini CLI** stores sessions as single JSON files. Each session embeds real token counts (input, output, cached, thoughts) per message, so no estimation is needed. Gemini reports input tokens inclusive of cached; CodeBurn subtracts cached from input before pricing to avoid double charging. +**Mistral Vibe** stores sessions as folders under `~/.vibe/logs/session/` (or `$VIBE_HOME/logs/session/`). CodeBurn reads cumulative prompt/completion totals and model pricing from `meta.json`, then reads `messages.jsonl` for the first user prompt and assistant tool calls. Subagent sessions under `agents/` are counted as separate Vibe sessions. + **Kiro** stores conversations as `.chat` JSON files. Token counts are estimated from content length. The underlying model is not exposed, so sessions are labeled `kiro-auto` and costed at Sonnet rates. **GitHub Copilot** reads from both `~/.copilot/session-state/` (legacy CLI) and VS Code's `workspaceStorage/*/GitHub.copilot-chat/transcripts/`. The VS Code format has no explicit token counts; tokens are estimated from content length and the model is inferred from tool call ID prefixes. @@ -255,13 +261,14 @@ Requires a git repository. Run from your project directory. codeburn plan set claude-max # $200/month codeburn plan set claude-pro # $20/month codeburn plan set cursor-pro # $20/month -codeburn plan set custom --monthly-usd 150 --provider claude # custom +codeburn plan set custom --monthly-usd 200 --provider codex # ChatGPT Pro-style custom plan +codeburn plan reset --provider codex # remove one provider plan codeburn plan set none # disable plan view -codeburn plan # show current +codeburn plan # show configured plans codeburn plan reset # remove plan config ``` -Subscription tracking for Claude Pro, Claude Max, and Cursor Pro. The dashboard shows a progress bar of API-equivalent cost against your plan price. Supports custom plans. Presets use publicly stated plan prices (as of April 2026); they do not model exact token allowances, because vendors do not publish precise consumer-plan limits. +Subscription tracking for Claude Pro, Claude Max, Cursor Pro, and custom provider plans. Plans are stored per provider, so you can track Claude and Codex/Cursor subscriptions at the same time; the dashboard shows one overage line per active provider plan. A legacy/custom `all` plan remains a single aggregate plan and is replaced when you add a provider-specific plan, avoiding double-counted overage rows. Existing single-plan config is still read as a fallback. Presets use publicly stated plan prices (as of April 2026); they do not model exact token allowances, because vendors do not publish precise consumer-plan limits. ### Currency @@ -374,13 +381,21 @@ These are starting points, not verdicts. A 60% cache hit on a single experimenta **Pi / OMP** stores sessions as JSONL at `~/.pi/agent/sessions//*.jsonl` (Pi) and `~/.omp/agent/sessions//*.jsonl` (OMP). Each assistant message carries token usage (input, output, cacheRead, cacheWrite) plus inline `toolCall` content blocks. CodeBurn extracts token counts, normalizes tool names to the standard set (`bash` to `Bash`, `dispatch_agent` to `Agent`), and pulls bash commands from `toolCall.arguments.command` for the shell breakdown. +**Codebuff** (formerly Manicode) stores per-chat history as JSON at `~/.config/manicode/projects//chats//chat-messages.json`. Codebuff bills in credits rather than tokens, so CodeBurn records each completed assistant message (via `msg.credits`) and approximates cost at the public pay-as-you-go rate ($0.01 / credit). When Codebuff routes a call through an upstream provider and the stashed RunState records token-level usage (`message.metadata.runState.sessionState.mainAgentState.messageHistory[*].providerOptions`), the real tokens and LiteLLM-calculated cost take precedence. Codebuff-native tool names (`read_files`, `str_replace`, `run_terminal_command`, `spawn_agents`, etc.) normalize to the canonical set (`Read`, `Edit`, `Bash`, `Agent`). The `manicode-dev` and `manicode-staging` channels are walked automatically when present. Honors `CODEBUFF_DATA_DIR` for a custom root. + **Gemini CLI** stores sessions as single JSON files at `~/.gemini/tmp//chats/session-*.json`. Each session embeds real token counts (input, output, cached, thoughts) per message. Gemini reports input tokens inclusive of cached; CodeBurn subtracts cached from input before pricing to avoid double charging. +**Mistral Vibe** stores session folders at `~/.vibe/logs/session/`. Each folder contains `meta.json` with cumulative prompt/completion token totals, model pricing, timestamps, and working directory, plus `messages.jsonl` with user prompts and assistant tool calls. CodeBurn emits one record per Vibe session because the source data is cumulative, not per assistant turn. + **OpenClaw** stores agent sessions as JSONL at `~/.openclaw/agents/*.jsonl`. Also checks legacy paths `.clawdbot`, `.moltbot`, `.moldbot`. Token usage comes from assistant message `usage` blocks; model from `modelId` or `message.model` fields. -**Roo Code / KiloCode** are Cline-family VS Code extensions. CodeBurn reads `ui_messages.json` from each task directory in VS Code's `globalStorage`, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts. +**Cline / Roo Code / KiloCode** are Cline-family coding agents. CodeBurn reads `ui_messages.json` from each task directory, filtering `type: "say"` entries with `say: "api_req_started"` to extract token counts. Cline scans both VS Code's `globalStorage/saoudrizwan.claude-dev` and `~/.cline/data`. + +**IBM Bob** stores IDE task history in `User/globalStorage/ibm.bob-code/tasks//` under the IBM Bob application data directory. CodeBurn reads `ui_messages.json` for API request token/cost records and `api_conversation_history.json` for the selected model, with support for both GA (`IBM Bob`) and preview (`Bob-IDE`) app data folders. + +**Kimi Code CLI** stores session logs under `$KIMI_SHARE_DIR/sessions///` or `~/.kimi/sessions///`. CodeBurn reads `wire.jsonl` `StatusUpdate.token_usage` records, maps `input_other`, `input_cache_read`, `input_cache_creation`, and `output` into the standard token columns, and includes subagent sessions under each session's `subagents/` folder. -CodeBurn deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session ID for Gemini, by session+message ID for OpenCode, by responseId for Pi/OMP), filters by date range per entry, and classifies each turn. +CodeBurn deduplicates messages (by API message ID for Claude, by cumulative token cross-check for Codex, by conversation/timestamp for Cursor, by session ID for Gemini, by session+message ID for OpenCode, by responseId for Pi/OMP, by chat folder + message ID for Codebuff, by session+message ID for Kimi), filters by date range per entry, and classifies each turn. ## Environment Variables @@ -389,8 +404,12 @@ CodeBurn deduplicates messages (by API message ID for Claude, by cumulative toke | `CLAUDE_CONFIG_DIR` | Override Claude Code data directory (default: `~/.claude`) | | `CLAUDE_CONFIG_DIRS` | OS-delimited list of Claude data directories to scan together (e.g. `~/.claude-work:~/.claude-personal`). Sessions merge into one row per project. Overrides `CLAUDE_CONFIG_DIR` when set. | | `CODEX_HOME` | Override Codex data directory (default: `~/.codex`) | +| `CODEBUFF_DATA_DIR` | Override Codebuff data directory (default: `~/.config/manicode`) | | `FACTORY_DIR` | Override Droid data directory (default: `~/.factory`) | +| `KIMI_SHARE_DIR` | Override Kimi Code CLI share directory (default: `~/.kimi`) | +| `KIMI_MODEL_NAME` | Override Kimi model name when Kimi sessions do not record the model | | `QWEN_DATA_DIR` | Override Qwen data directory (default: `~/.qwen/projects`) | +| `VIBE_HOME` | Override Mistral Vibe home directory (default: `~/.vibe`) | ## Sponsoring CodeBurn diff --git a/RELEASING.md b/RELEASING.md index 56e41242..b42d7d82 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -120,25 +120,25 @@ git push origin mac-v0.9.8 The `.github/workflows/release-menubar.yml` workflow automatically detects the `mac-v*` tag and: 1. Checks out the repo -2. Runs `mac/Scripts/package-app.sh 0.9.8` +2. Runs `mac/Scripts/package-app.sh v0.9.8` 3. Signs the app bundle (ad-hoc signing) -4. Creates a zip file: `CodeBurnMenubar-0.9.8.zip` -5. Computes a SHA-256 checksum: `CodeBurnMenubar-0.9.8.zip.sha256` +4. Creates a zip file: `CodeBurnMenubar-v0.9.8.zip` +5. Computes a SHA-256 checksum: `CodeBurnMenubar-v0.9.8.zip.sha256` 6. Uploads both to a GitHub Release named "Menubar v0.9.8" The script output on the build machine shows: ``` -✓ Built /path/mac/.build/dist/CodeBurnMenubar-0.9.8.zip -✓ Checksum /path/mac/.build/dist/CodeBurnMenubar-0.9.8.zip.sha256 - CodeBurnMenubar-0.9.8.zip +✓ Built /path/mac/.build/dist/CodeBurnMenubar-v0.9.8.zip +✓ Checksum /path/mac/.build/dist/CodeBurnMenubar-v0.9.8.zip.sha256 + CodeBurnMenubar-v0.9.8.zip ``` No manual action is needed; the workflow handles everything. ### 4. Verify the Release -After the workflow completes, the GitHub Release page shows the zip and sha256 files. The menubar installer command in the CLI calls `npx codeburn menubar`, which fetches the latest release from GitHub and installs it into `~/Applications`. +After the workflow completes, the GitHub Release page shows the zip and sha256 files. The installed CLI command `codeburn menubar --force` fetches the newest `mac-v*` menubar release that includes both assets, verifies the checksum and bundle identity, and installs it into `~/Applications`. ## Homebrew Tap Update @@ -227,12 +227,12 @@ If a release is published with broken assets (e.g., a menubar zip with a build e Use `gh release upload` with the `--clobber` flag to overwrite existing files: ```bash -# After re-running mac/Scripts/package-app.sh 0.9.8 to regenerate the zip and sha256 -gh release upload mac-v0.9.8 mac/.build/dist/CodeBurnMenubar-0.9.8.zip --clobber -gh release upload mac-v0.9.8 mac/.build/dist/CodeBurnMenubar-0.9.8.zip.sha256 --clobber +# 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 fetches from the Release by tag, so users who run `npx codeburn menubar` after the replacement get the fixed version automatically. +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 @@ -245,7 +245,7 @@ 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 `npx codeburn menubar --force`). +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 diff --git a/assets/providers/cline.svg b/assets/providers/cline.svg new file mode 100644 index 00000000..d00094b4 --- /dev/null +++ b/assets/providers/cline.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/providers/ibm-bob.svg b/assets/providers/ibm-bob.svg new file mode 100644 index 00000000..ab76047e --- /dev/null +++ b/assets/providers/ibm-bob.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/assets/providers/kimi.svg b/assets/providers/kimi.svg new file mode 100644 index 00000000..c09b36fe --- /dev/null +++ b/assets/providers/kimi.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/providers/mistral-vibe.svg b/assets/providers/mistral-vibe.svg new file mode 100644 index 00000000..f70841a7 --- /dev/null +++ b/assets/providers/mistral-vibe.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/docs/architecture.md b/docs/architecture.md index 9b1ea14f..5f31c7c3 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -128,14 +128,14 @@ type Provider = { } ``` -`src/providers/index.ts` registers eighteen providers across two tiers: +`src/providers/index.ts` registers twenty-one providers across two tiers: -- **Eager**: `claude`, `codex`, `copilot`, `droid`, `gemini`, `kilo-code`, `kiro`, `openclaw`, `pi`, `omp`, `qwen`, `roo-code`. Imported at module load. +- **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 `kilo-code` and `roo-code`. It is not registered as a provider on its own. +`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/`. @@ -181,7 +181,7 @@ The `prepublishOnly` hook in `package.json` runs `npm run build` so `npm publish - `tests/` root (27 files) covers CLI, parser, optimize, cache, format, models, plans. - `tests/security/` (1 file) covers prototype-pollution guards. -- `tests/providers/` (14 files) covers per-provider parsing. +- `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. diff --git a/docs/providers/README.md b/docs/providers/README.md index 05f43dbf..414289ec 100644 --- a/docs/providers/README.md +++ b/docs/providers/README.md @@ -11,12 +11,16 @@ For the architectural picture, see `../architecture.md`. | 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` | @@ -38,7 +42,7 @@ For the architectural picture, see `../architecture.md`. | Helper | Used by | Source | |---|---|---| -| [vscode-cline-parser](vscode-cline-parser.md) | `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` | +| [vscode-cline-parser](vscode-cline-parser.md) | `cline`, `ibm-bob`, `kilo-code`, `roo-code` | `src/providers/vscode-cline-parser.ts` | ## File Format diff --git a/docs/providers/antigravity.md b/docs/providers/antigravity.md index 723cef59..ca6d23f7 100644 --- a/docs/providers/antigravity.md +++ b/docs/providers/antigravity.md @@ -3,41 +3,50 @@ 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 (`src/providers/index.ts:14-27`). Lazy because the protobuf dependency is heavy. -- **Test:** none. Mocking the RPC endpoint cleanly is the open issue. +- **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`. +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 5-15 MB depending on cascade size). +4. Validates the response (capped at 16 MB). -If the language server is not running, the parser falls back to the cached results file (`antigravity.ts:262-272`). +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; see `antigravity.ts:299-323`. +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 version constant is at `antigravity.ts:12`; the cache machinery (`loadCache`, `flushCache`) lives in `antigravity.ts:75-125`. 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. +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 `:` (`antigravity.ts:308`). +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 5-15 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` (`antigravity.ts:313-323`). Thinking is billed at output rate. +- 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 requires Antigravity running locally. There is no fixture for the RPC, which is a real testing gap. +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 at `antigravity.ts:299-323` is the place to look. +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 index b0b7b8cf..b5954c1f 100644 --- a/docs/providers/claude.md +++ b/docs/providers/claude.md @@ -25,6 +25,17 @@ JSONL, one event per line, per session file. Sessions live under `// + 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/ibm-bob.md b/docs/providers/ibm-bob.md new file mode 100644 index 00000000..c9d43737 --- /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 index 188465f3..51527ef5 100644 --- a/docs/providers/kilo-code.md +++ b/docs/providers/kilo-code.md @@ -25,10 +25,10 @@ Delegated. Per `::` (handled in `vscode-cline-parse ## Quirks - This file is a thin wrapper. Almost every bug for KiloCode actually lives in `vscode-cline-parser.ts`. -- The two providers using the cline parser (KiloCode and Roo Code) differ **only** by extension ID. +- 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 "KiloCode and Roo Code both broken in the same way", fix it in `vscode-cline-parser.ts`. +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 00000000..19d6876e --- /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/mistral-vibe.md b/docs/providers/mistral-vibe.md new file mode 100644 index 00000000..c7005f70 --- /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/opencode.md b/docs/providers/opencode.md index 0251fcd4..0148cc91 100644 --- a/docs/providers/opencode.md +++ b/docs/providers/opencode.md @@ -4,7 +4,7 @@ OpenCode (sst/opencode). - **Source:** `src/providers/opencode.ts` - **Loading:** lazy (`src/providers/index.ts:59-75`) -- **Test:** `tests/providers/opencode.test.ts` (558 lines, the largest provider test) +- **Test:** `tests/providers/opencode.test.ts` (676 lines, the largest provider test) ## Where it reads from @@ -20,14 +20,18 @@ None. ## Deduplication -Per `:` (`opencode.ts:242`). +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 (`opencode.ts:104-131`). This is the right behavior; do not silently swallow these. -- Source paths are encoded as `:` (`opencode.ts:147-150`). -- Each message's `parts` are indexed (`opencode.ts:177-191`); preserving the order matters for reasoning-token correctness. +- **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 diff --git a/docs/providers/roo-code.md b/docs/providers/roo-code.md index 6f9d16ac..e8290649 100644 --- a/docs/providers/roo-code.md +++ b/docs/providers/roo-code.md @@ -25,10 +25,10 @@ Delegated. Per `::` (in `vscode-cline-parser.ts:109 ## Quirks - Thin wrapper. Almost every Roo Code bug actually lives in `vscode-cline-parser.ts`. -- The two providers using the cline parser (KiloCode and Roo Code) differ **only** by extension ID. +- 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 KiloCode, fix it in `vscode-cline-parser.ts`. +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 index 5b6bdfa9..3535e63c 100644 --- a/docs/providers/vscode-cline-parser.md +++ b/docs/providers/vscode-cline-parser.md @@ -1,49 +1,50 @@ # vscode-cline-parser (Shared Helper) -Shared discovery and parsing for VS Code extensions descended from Cline. +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 `kilo-code.ts` and `roo-code.ts`. -- **Test:** none directly. Coverage comes from `tests/providers/kilo-code.test.ts` and `tests/providers/roo-code.test.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 VS Code's `globalStorage//tasks/` directories and returns one source per task that has a `ui_messages.json` file (`vscode-cline-parser.ts:25-50`). -2. `createClineParser` reads each task's `ui_messages.json` and `api_conversation_history.json`, extracts model, tools, and token counts, and yields `ParsedProviderCall` objects. +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// +/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 (`vscode-cline-parser.ts:54-72`). Falls back to `cline-auto` if no tag is found. +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` (`vscode-cline-parser.ts:119-134`). +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 (`vscode-cline-parser.ts:139`). +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` (`vscode-cline-parser.ts:109`). +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` (`vscode-cline-parser.ts:157`). Subsequent user turns are accounted but not surfaced. +- 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 **both** KiloCode and Roo Code. Run both test files (`tests/providers/kilo-code.test.ts` and `tests/providers/roo-code.test.ts`) before opening a PR. -2. If you find that one of the two 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 a third Cline-derivative extension, register it as a thin wrapper file in the same shape as `kilo-code.ts` and `roo-code.ts`. +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/gnome/indicator.js b/gnome/indicator.js index c2f8266e..533f6441 100644 --- a/gnome/indicator.js +++ b/gnome/indicator.js @@ -41,6 +41,7 @@ const PROVIDERS = [ { id: 'gemini', label: 'Gemini' }, { id: 'kilo-code', label: 'Kilo Code' }, { id: 'kiro', label: 'Kiro' }, + { id: 'kimi', label: 'Kimi' }, { id: 'roo-code', label: 'Roo Code' }, ]; @@ -69,6 +70,7 @@ const PROVIDER_PATHS = { codex: '.codex/sessions', cursor: '.config/Cursor/User/globalStorage/state.vscdb', copilot: '.copilot/session-state', + kimi: '.kimi/sessions', pi: '.pi/agent/sessions', }; diff --git a/gnome/prefs.js b/gnome/prefs.js index 2b9d477b..08d4b824 100644 --- a/gnome/prefs.js +++ b/gnome/prefs.js @@ -13,6 +13,7 @@ const PROVIDERS = [ { 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' }, diff --git a/mac/README.md b/mac/README.md index 3a7f1d77..b12b836d 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 5de94edd..c9982a75 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,18 +87,17 @@ 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}" diff --git a/mac/Sources/CodeBurnMenubar/AppStore.swift b/mac/Sources/CodeBurnMenubar/AppStore.swift index 00b27e8b..c901362f 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 @@ -51,6 +52,7 @@ final class AppStore { 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 @@ -63,6 +65,10 @@ final class AppStore { return Date().timeIntervalSince(last) } + private var todayAllKey: PayloadCacheKey { + PayloadCacheKey(period: .today, provider: .all) + } + private var currentKey: PayloadCacheKey { PayloadCacheKey(period: selectedPeriod, provider: selectedProvider) } @@ -74,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 @@ -87,6 +102,47 @@ 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 @@ -95,6 +151,12 @@ final class AppStore { 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 } @@ -103,16 +165,7 @@ final class AppStore { /// all-provider data in parallel so tab strip costs stay in sync with the hero. func switchTo(period: Period) { selectedPeriod = period - switchTask?.cancel() - switchTask = Task { - if selectedProvider == .all { - await refresh(includeOptimize: false, force: true) - } else { - async let main: Void = refresh(includeOptimize: false, force: true) - async let all: Void = refreshQuietly(period: period) - _ = await (main, all) - } - } + startInteractiveSelectionRefresh() } /// Switch to a provider filter. Cancels any in-flight switch so rapid tab tapping only @@ -120,13 +173,21 @@ final class AppStore { /// in parallel so the tab strip costs stay in sync with the hero. func switchTo(provider: ProviderFilter) { selectedProvider = provider + 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) + await refresh(includeOptimize: false, force: true, showLoading: true) } else { - async let main: Void = refresh(includeOptimize: false, force: true) - async let all: Void = refreshQuietly(period: selectedPeriod) + async let main: Void = refresh(includeOptimize: false, force: true, showLoading: true) + async let all: Void = refreshQuietly(period: period) _ = await (main, all) } } @@ -135,11 +196,23 @@ final class AppStore { private var inFlightKeys: Set = [] 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 @@ -150,6 +223,7 @@ final class AppStore { } 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) @@ -180,13 +254,24 @@ final class AppStore { } } - private func invalidateStaleDayCache() { + private func currentCacheDate() -> String { let formatter = DateFormatter() formatter.dateFormat = "yyyy-MM-dd" - let today = formatter.string(from: Date()) + 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) } } @@ -198,8 +283,9 @@ final class AppStore { invalidateStaleDayCache() let key = currentKey let cacheDateAtStart = cacheDate + let generationAtStart = payloadRefreshGeneration if !force, cache[key]?.isFresh == true { return } - if !force, inFlightKeys.contains(key) { return } + if inFlightKeys.contains(key) { return } inFlightKeys.insert(key) attemptedKeys.insert(key) lastErrorByKey[key] = nil @@ -226,6 +312,10 @@ final class AppStore { } 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 @@ -238,7 +328,8 @@ final class AppStore { // 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 { + if cacheDate != cacheDateAtStart || cacheDate != currentCacheDate() { + invalidateStaleDayCache() NSLog("CodeBurn: dropping fetch result for \(key.period.rawValue)/\(key.provider.rawValue) — calendar rolled mid-fetch") return } @@ -252,7 +343,11 @@ final class AppStore { do { let fallback = try await DataClient.fetch(period: key.period, provider: key.provider, includeOptimize: false) guard !Task.isCancelled else { return } - if cacheDate != cacheDateAtStart { 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 @@ -274,15 +369,33 @@ 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: 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 { return } - let key = PayloadCacheKey(period: period, provider: .all) + if cacheDate != cacheDateAtStart || cacheDate != currentCacheDate() { + invalidateStaleDayCache() + return + } cache[key] = CachedPayload(payload: fresh, fetchedAt: Date()) lastSuccessByKey[key] = Date() lastErrorByKey[key] = nil @@ -505,7 +618,7 @@ final class AppStore { var aggregateQuotaStatus: AggregateQuotaStatus { var providers: [(name: String, percent: Double)] = [] - if case .loaded = subscriptionLoadState, let usage = subscription { + if let usage = subscription, shouldIncludeCachedQuota(loadState: subscriptionLoadState) { let worst = [ usage.fiveHourPercent, usage.sevenDayPercent, @@ -514,7 +627,7 @@ final class AppStore { ].compactMap { $0 }.max() ?? 0 if worst > 0 { providers.append(("Claude", worst)) } } - if case .loaded = codexLoadState, let usage = codexUsage { + 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)) } } @@ -525,6 +638,15 @@ final class AppStore { 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) @@ -720,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" @@ -734,15 +860,22 @@ enum ProviderFilter: String, CaseIterable, Identifiable { 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()] } } @@ -751,13 +884,17 @@ 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" @@ -765,6 +902,8 @@ enum ProviderFilter: String, CaseIterable, Identifiable { case .omp: "omp" case .rooCode: "roo-code" case .crush: "crush" + case .antigravity: "antigravity" + case .goose: "goose" } } } diff --git a/mac/Sources/CodeBurnMenubar/AppVersion.swift b/mac/Sources/CodeBurnMenubar/AppVersion.swift new file mode 100644 index 00000000..c5ee14a8 --- /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 5868258e..61915759 100644 --- a/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift +++ b/mac/Sources/CodeBurnMenubar/CodeBurnApp.swift @@ -3,9 +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 @@ -35,10 +37,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { /// 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 refreshLoopTask: Task? + 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 @@ -91,28 +102,21 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { queue: .main ) { [weak self] _ in Task { @MainActor in - self?.forceRefreshTask?.cancel() - self?.forceRefreshTask = nil - self?.forceRefreshStartedAt = nil - self?.forceRefreshGeneration &+= 1 - self?.refreshLoopTask?.cancel() - self?.refreshLoopTask = nil + self?.prepareRefreshPipelineForSleep() } } // didWakeNotification + screensDidWakeNotification can both fire on - // the same wake. forceRefresh has a 5-second rate-limit gate so the - // duplicate is squashed there. Restart the refresh loop too, since - // we cancelled it on willSleep. + // 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?.store.resetLoadingState() - self?.forceRefresh() - if self?.refreshLoopTask == nil { self?.startRefreshLoop() } + self?.recoverRefreshPipelineAfterInterruption(resetLoading: true, reason: "wake") } } @@ -121,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") + } } } @@ -131,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) } } @@ -192,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" @@ -211,14 +283,30 @@ 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 @discardableResult private func clearStaleForceRefreshIfNeeded(now: Date = Date()) -> Bool { - if let started = forceRefreshStartedAt, forceRefreshTask != nil { + 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)) + NSLog("CodeBurn: force refresh stuck for %ds - cancelling and restarting", Int(elapsed)) forceRefreshTask?.cancel() forceRefreshTask = nil forceRefreshStartedAt = nil @@ -229,10 +317,61 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { return false } - private func forceRefresh() { + @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) - guard now.timeIntervalSince(lastRefreshTime) > 5 else { return } + 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 @@ -240,8 +379,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { forceRefreshTask = Task { async let main: Void = store.refresh(includeOptimize: false, force: true, showLoading: true) - async let today: Void = store.refreshQuietly(period: .today) - _ = await (main, today) + 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 } @@ -249,6 +391,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { self.forceRefreshStartedAt = nil self.lastRefreshTime = Date() } + _ = await quotas } } @@ -275,75 +418,184 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { } fileprivate var lastSubscriptionRefreshAt: Date? + fileprivate var lastCodexRefreshAt: Date? - private func startRefreshLoop() { - refreshLoopTask?.cancel() - refreshLoopTask = Task { [weak self] in - // Provider refreshes only run when the user has explicitly connected. - // Each refresh is a no-op until its corresponding bootstrap flag is set. - if let self { - async let claude = self.store.refreshSubscriptionReportingSuccess() - async let codex = self.store.refreshCodexReportingSuccess() - if await claude { self.lastSubscriptionRefreshAt = Date() } - if await codex { self.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() } - while !Task.isCancelled { - guard let self else { return } - let clearedStaleForceRefresh = self.clearStaleForceRefreshIfNeeded() - let clearedStaleLoading = self.store.clearStaleLoadingIfNeeded() - // Skip the loop's tick if a wake / manual / distributed- - // notification refresh just ran. Without this gate, every - // wake produced two refreshes (forceRefresh from the wake - // observer plus the loop's natural tick). - let sinceLast = Date().timeIntervalSince(self.lastRefreshTime) - if self.forceRefreshTask == nil && (clearedStaleForceRefresh || clearedStaleLoading || sinceLast >= 5) { - if self.store.selectedPeriod != .today || self.store.selectedProvider != .all { - async let quiet: Void = self.store.refreshQuietly(period: .today) - async let main: Void = self.store.refresh(includeOptimize: false, force: true) - _ = await (quiet, main) - } else { - await self.store.refresh(includeOptimize: false, force: true) - } - self.lastRefreshTime = Date() - self.refreshStatusButton() - } - // Cadence-driven live-quota refresh, anchored on LAST SUCCESS - // (not last attempt) so an intermittent failure doesn't reset - // the timer. Each provider has its own anchor so a Codex 429 - // doesn't delay a due Claude refresh. - let cadence = SubscriptionRefreshCadence.current - if cadence != .manual { - let claudeElapsed = Date().timeIntervalSince(self.lastSubscriptionRefreshAt ?? .distantPast) - if claudeElapsed >= TimeInterval(cadence.rawValue) { - let succeeded = await self.store.refreshSubscriptionReportingSuccess() - if succeeded { self.lastSubscriptionRefreshAt = Date() } - } - let codexElapsed = Date().timeIntervalSince(self.lastCodexRefreshAt ?? .distantPast) - if codexElapsed >= TimeInterval(cadence.rawValue) { - let succeeded = await self.store.refreshCodexReportingSuccess() - if succeeded { self.lastCodexRefreshAt = Date() } - } - } - try? await Task.sleep(nanoseconds: refreshIntervalNanos) + case (false, true): + if await refreshCodexQuotaSingleFlight() { + lastCodexRefreshAt = Date() } + case (false, false): + break } + return true } - fileprivate var lastCodexRefreshAt: Date? + 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() { - Task { [weak self] in + 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 + // 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 claude: Bool = self.store.refreshSubscriptionReportingSuccess() - async let codex: Bool = self.store.refreshCodexReportingSuccess() + async let quotas: Bool = self.refreshLiveQuotaProgressIfDue(force: true) + if needsTodayTotal { + await self.store.refreshQuietly(period: .today, force: true) + } _ = await payload - if await claude { self.lastSubscriptionRefreshAt = Date() } - if await codex { self.lastCodexRefreshAt = Date() } + 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() + } } } @@ -541,6 +793,8 @@ final class AppDelegate: NSObject, NSApplicationDelegate, NSPopoverDelegate { window.collectionBehavior.insert(.canJoinAllSpaces) window.makeKeyAndOrderFront(nil) } + refreshPayloadForPopoverOpen() + refreshLiveQuotaProgressForPopoverOpen() } } @@ -626,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() } diff --git a/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift b/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift index 9d887bfa..e47db7b2 100644 --- a/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift +++ b/mac/Sources/CodeBurnMenubar/Data/ClaudeCredentialStore.swift @@ -36,15 +36,11 @@ enum ClaudeCredentialStore { private static let credentialsRelativePath = ".claude/.credentials.json" private static let maxCredentialBytes = 64 * 1024 - /// Local cache file. Stored under Application Support with 0600 permissions - /// so only the current user can read it. We deliberately do NOT use the - /// macOS Keychain for our own cache: keychain ACLs are bound to the binary - /// code signature, so reading our own item triggers a prompt every time the - /// binary changes (debug rebuilds, app updates with re-signing). Putting the - /// cache in a plain file means the only Keychain prompt our user ever sees - /// is the initial Connect read of Claude Code's own keychain entry. - /// Threat model: same as ~/.claude/.credentials.json (also plaintext). + /// 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? @@ -283,6 +279,10 @@ enum ClaudeCredentialStore { } 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 @@ -291,21 +291,66 @@ enum ClaudeCredentialStore { // 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) - return try? JSONDecoder().decode(CredentialRecord.self, from: data) + 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) - // SafeFile.write opens the temp file with O_CREAT | O_EXCL | O_NOFOLLOW - // and the explicit 0600 mode in a single syscall — no race window - // where the file briefly exists at default umask, and no chance of - // following a malicious symlink at the destination path. Also creates - // the parent dir at 0700. - try SafeFile.write(data, to: url.path, mode: 0o600) + 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()) } diff --git a/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift b/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift index d821151d..cffae7bf 100644 --- a/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift +++ b/mac/Sources/CodeBurnMenubar/Data/CodexCredentialStore.swift @@ -1,4 +1,5 @@ import Foundation +import Security /// Owns the Codex (ChatGPT-mode) OAuth credential lifecycle. Mirrors /// ClaudeCredentialStore but reads from ~/.codex/auth.json — Codex CLI @@ -17,6 +18,8 @@ enum CodexCredentialStore { 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? @@ -198,28 +201,74 @@ enum CodexCredentialStore { } 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) - return try? JSONDecoder().decode(CredentialRecord.self, from: data) + 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) - do { - // SafeFile.write opens the temp file with O_CREAT | O_EXCL | O_NOFOLLOW - // and the explicit 0600 mode in a single syscall — no race window - // where the file briefly exists at default umask, and no chance of - // following a malicious symlink at the destination path. - try SafeFile.write(data, to: url.path, mode: 0o600) - } catch { - throw StoreError.fileWriteFailed(String(describing: error)) + 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()) } diff --git a/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift b/mac/Sources/CodeBurnMenubar/Data/UpdateChecker.swift index 6ad0a900..5441794e 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,22 +80,50 @@ 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 @@ -93,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) + } +} + +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." + } + } } -private struct GitHubRelease: Decodable { +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 4f4a5f82..befa3b1b 100644 --- a/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift +++ b/mac/Sources/CodeBurnMenubar/Security/CodeburnCLI.swift @@ -14,19 +14,69 @@ enum CodeburnCLI { /// Homebrew and npm global installs. private static let additionalPathEntries = ["/opt/homebrew/bin", "/usr/local/bin"] + private static let userNodePaths: [String] = { + let home = FileManager.default.homeDirectoryForCurrentUser.path + var paths: [String] = [] + for dir in ["\(home)/.volta/bin", "\(home)/.npm-global/bin", "\(home)/.asdf/shims"] { + paths.append(dir) + } + let nvmDir = ProcessInfo.processInfo.environment["NVM_DIR"] ?? "\(home)/.nvm" + let versionsDir = "\(nvmDir)/versions/node" + if let entries = try? FileManager.default.contentsOfDirectory(atPath: versionsDir) { + for entry in entries.sorted().reversed() { + let bin = "\(versionsDir)/\(entry)/bin" + if FileManager.default.isExecutableFile(atPath: "\(bin)/codeburn") { + paths.append(bin) + break + } + } + } + return paths + }() + 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] + } + for candidate in (additionalPathEntries + userNodePaths).map({ "\($0)/codeburn" }) { + if isSafe(candidate), FileManager.default.isExecutableFile(atPath: candidate) { + return [candidate] + } } - return parts + 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` @@ -56,7 +106,7 @@ enum CodeburnCLI { private static func augmentedPath(_ existing: String) -> String { var parts = existing.split(separator: ":", omittingEmptySubsequences: true).map(String.init) - for extra in additionalPathEntries where !parts.contains(extra) { + for extra in additionalPathEntries + userNodePaths where !parts.contains(extra) { parts.append(extra) } return parts.joined(separator: ":") diff --git a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift index 6561cc97..82f2cebc 100644 --- a/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift +++ b/mac/Sources/CodeBurnMenubar/Views/AgentTabStrip.swift @@ -1,26 +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 - AgentTab( - filter: filter, - cost: cost(for: filter), - isActive: store.selectedProvider == filter, - quota: store.quotaSummary(for: filter) - ) { - store.switchTo(provider: 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 + } + } + ) + } + .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 { @@ -55,6 +140,60 @@ 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 { @@ -340,13 +479,17 @@ 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) @@ -354,6 +497,8 @@ extension ProviderFilter { 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/MenuBarContent.swift b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift index fbf3dd90..7bad14b3 100644 --- a/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift +++ b/mac/Sources/CodeBurnMenubar/Views/MenuBarContent.swift @@ -279,7 +279,7 @@ private struct Header: View { .foregroundStyle(.secondary) } Spacer() - if updateChecker.updateAvailable { + if updateChecker.updateAvailable || updateChecker.updateError != nil { UpdateBadge() } AccentPicker() @@ -409,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) @@ -430,6 +437,7 @@ private struct UpdateBadge: View { .tint(Theme.brandAccent) .controlSize(.mini) .disabled(updateChecker.isUpdating) + .help(updateChecker.updateError ?? "Install the latest menubar build") } } @@ -537,12 +545,7 @@ struct FooterBar: View { .fixedSize() Button { - // showLoading: true is safe now that the overlay condition uses - // `!hasCachedData` instead of `isLoading`. The button icon swaps - // to the spinner glyph (driven by store.isLoading), giving the - // user visible feedback the click was registered, but the - // popover body keeps the existing data instead of blanking out. - Task { await store.refresh(includeOptimize: false, force: true, showLoading: true) } + refreshNow() } label: { Image(systemName: store.isLoading ? "arrow.triangle.2.circlepath" : "arrow.clockwise") .font(.system(size: 11, weight: .medium)) @@ -567,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) @@ -588,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" } diff --git a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift index a4c35853..a3173803 100644 --- a/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift +++ b/mac/Sources/CodeBurnMenubar/Views/SettingsView.swift @@ -337,10 +337,8 @@ private struct CodexConnectionRow: View { // MARK: - About private struct AboutSettingsTab: View { - private let appVersion: String = - (Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String) ?? "—" - private let buildVersion: String = - (Bundle.main.infoDictionary?["CFBundleVersion"] as? String) ?? "—" + private let appVersion: String = AppVersion.normalizedBundleShortVersion + private let buildVersion: String = AppVersion.normalizedBundleBuildVersion var body: some View { VStack(spacing: 14) { diff --git a/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift b/mac/Tests/CodeBurnMenubarTests/AppStoreRefreshRecoveryTests.swift new file mode 100644 index 00000000..fd75fec3 --- /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 00000000..898f5e06 --- /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 00000000..44f52b50 --- /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 2c2d21b5..ad40db2d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codeburn", - "version": "0.9.7", + "version": "0.9.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codeburn", - "version": "0.9.7", + "version": "0.9.9", "license": "MIT", "dependencies": { "chalk": "^5.4.1", @@ -27,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 a58098db..243c7684 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "codeburn", - "version": "0.9.8", + "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,8 +21,11 @@ "claude-code", "cursor", "codex", + "kimi", + "ibm-bob", "opencode", "pi", + "codebuff", "ai-coding", "token-usage", "cost-tracking", @@ -30,7 +33,7 @@ "developer-tools" ], "engines": { - "node": ">=22" + "node": ">=22.13.0" }, "author": "AgentSeal ", "license": "MIT", diff --git a/src/cli.ts b/src/cli.ts index 4ebfe337..dec3d494 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -1,978 +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, 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() +#!/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) +} + +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 - - // 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 ', 'Auto-refresh interval in seconds (0 to disable)', parseInteger, 30) - .action(async (opts) => { - assertFormat(opts.format, ['tui', 'json'], 'report') - 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 = formatDateRangeLabel(opts.from, opts.to) - 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() - const customRangeLabel = customRange ? formatDateRangeLabel(opts.from, opts.to) : undefined - await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude, customRange, customRangeLabel) - }) - -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 + 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) => { - assertFormat(opts.format, ['terminal', 'menubar-json', 'json'], 'status') - 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(now.getFullYear(), now.getMonth(), now.getDate() - 1)) - 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(now.getFullYear(), now.getMonth(), now.getDate() - BACKFILL_DAYS)) - 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)', parseInteger, 30) - .action(async (opts) => { - assertFormat(opts.format, ['tui', 'json'], 'today') - 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)', parseInteger, 30) - .action(async (opts) => { - assertFormat(opts.format, ['tui', 'json'], 'month') - 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') - .option('-f, --format ', 'Export format: csv, json', 'csv') - .option('-o, --output ', 'Output file path') - .option('--from ', 'Start date (YYYY-MM-DD). Exports a single custom period when set') - .option('--to ', 'End date (YYYY-MM-DD). Exports a single custom period when set') - .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) => { - assertFormat(opts.format, ['csv', 'json'], 'export') - await loadPricing() - await hydrateCache() - const pf = opts.provider - const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude) - 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 periods: PeriodExport[] = customRange - ? [{ label: formatDateRangeLabel(opts.from, opts.to), projects: fp(await parseAllSessions(customRange, pf)) }] - : [ - { 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) - } - - const exportedLabel = customRange ? formatDateRangeLabel(opts.from, opts.to) : 'Today + 7 Days + 30 Days' - console.log(`\n Exported (${exportedLabel}) 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 }) => { - assertFormat(opts?.format ?? 'text', ['text', 'json'], 'plan') - 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('models') - .description('Per-model token + cost table, optionally exploded by task type') - .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days') - .option('--from ', 'Custom range start (YYYY-MM-DD)') - .option('--to ', 'Custom range end (YYYY-MM-DD)') - .option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all') - .option('--task ', 'Filter to one task type (e.g. feature, debugging, refactoring)') - .option('--by-task', 'One row per (provider, model, task) instead of one row per (provider, model)') - .option('--top ', 'Show only the top N rows', (v: string) => parseInt(v, 10)) - .option('--min-cost ', 'Hide rows below this cost threshold', (v: string) => parseFloat(v)) - .option('--no-totals', 'Suppress the footer totals row') - .option('--format ', 'Output format: table, markdown, json, csv', 'table') - .action(async (opts) => { - const { aggregateModels, renderTable, renderMarkdown, renderJson, renderCsv } = await import('./models-report.js') - await loadPricing() - await hydrateCache() - - let range - if (opts.from || opts.to) { - const customRange = parseDateRangeFlags(opts.from, opts.to) - if (!customRange) { - process.stderr.write('codeburn: --from and --to must be valid YYYY-MM-DD dates\n') - process.exit(1) - } - range = customRange - } else { - range = getDateRange(opts.period).range - } - - const projects = await parseAllSessions(range, opts.provider) - const rows = await aggregateModels(projects, { - byTask: !!opts.byTask, - taskFilter: opts.task, - topN: typeof opts.top === 'number' && Number.isFinite(opts.top) ? opts.top : undefined, - minCost: typeof opts.minCost === 'number' && Number.isFinite(opts.minCost) ? opts.minCost : 0.01, - }) - - const fmt = (opts.format ?? 'table').toLowerCase() - if (rows.length === 0 && (fmt === 'table' || fmt === 'markdown')) { - process.stdout.write('No model usage found for the selected period.\n') - return - } - if (fmt === 'json') { - process.stdout.write(renderJson(rows) + '\n') - } else if (fmt === 'csv') { - process.stdout.write(renderCsv(rows, { byTask: !!opts.byTask }) + '\n') - } else if (fmt === 'markdown' || fmt === 'md') { - process.stdout.write(renderMarkdown(rows, { byTask: !!opts.byTask, showTotals: opts.totals !== false }) + '\n') - } else if (fmt === 'table') { - process.stdout.write(renderTable(rows, { byTask: !!opts.byTask, showTotals: opts.totals !== false }) + '\n') - } else { - process.stderr.write(`codeburn: unknown --format "${opts.format}". Choose table, markdown, json, or csv.\n`) - process.exit(1) - } - }) - -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/config.ts b/src/config.ts index 12fec8f1..53e96248 100644 --- a/src/config.ts +++ b/src/config.ts @@ -2,6 +2,7 @@ import { readFile, writeFile, mkdir, rename } from 'fs/promises' import { join } from 'path' import { homedir } from 'os' import { randomBytes } from 'crypto' +import { PLAN_PROVIDERS } from './plans.js' export type PlanId = 'claude-pro' | 'claude-max' | 'claude-max-5x' | 'cursor-pro' | 'custom' | 'none' export type PlanProvider = 'claude' | 'codex' | 'cursor' | 'all' @@ -14,12 +15,17 @@ export type Plan = { setAt: string } +export type PlanConfig = Omit & Partial> +export type PlanConfigMap = Partial> +export type PlanMap = Partial> + export type CodeburnConfig = { currency?: { code: string symbol?: string } plan?: Plan + plans?: PlanConfigMap modelAliases?: Record } @@ -53,19 +59,79 @@ export async function saveConfig(config: CodeburnConfig): Promise { } export async function readPlan(): Promise { - const config = await readConfig() - return config.plan + const plans = await readPlans() + for (const provider of PLAN_PROVIDERS) { + const plan = plans[provider] + if (plan) return plan + } + return undefined +} + +function planFromConfig(provider: PlanProvider, plan: PlanConfig | undefined): Plan | undefined { + if (!plan) return undefined + return { + ...plan, + provider, + setAt: plan.setAt ?? '', + } +} + +function normalizePlans(config: CodeburnConfig): PlanMap { + const plans: PlanMap = {} + + if (config.plans && Object.keys(config.plans).length > 0) { + for (const provider of PLAN_PROVIDERS) { + const plan = planFromConfig(provider, config.plans[provider]) + if (plan) plans[provider] = plan + } + if (plans.all && PLAN_PROVIDERS.some(provider => provider !== 'all' && plans[provider])) { + delete plans.all + } + return plans + } + + if (config.plan) { + plans[config.plan.provider] = config.plan + } + + return plans +} + +export async function readPlans(): Promise { + return normalizePlans(await readConfig()) } export async function savePlan(plan: Plan): Promise { const config = await readConfig() - config.plan = plan + const plans = normalizePlans(config) + if (plan.provider === 'all') { + config.plans = { all: plan } + } else { + delete plans.all + plans[plan.provider] = plan + config.plans = plans + } + delete config.plan await saveConfig(config) } -export async function clearPlan(): Promise { +export async function clearPlan(provider?: PlanProvider): Promise { const config = await readConfig() + if (provider) { + const plans = normalizePlans(config) + delete plans[provider] + if (Object.keys(plans).length > 0) { + config.plans = plans + } else { + delete config.plans + } + delete config.plan + await saveConfig(config) + return + } + delete config.plan + delete config.plans await saveConfig(config) } diff --git a/src/daily-cache.ts b/src/daily-cache.ts index 6c930657..ab43017b 100644 --- a/src/daily-cache.ts +++ b/src/daily-cache.ts @@ -5,24 +5,19 @@ import { homedir } from 'os' import { join } from 'path' import type { DateRange, ProjectSummary } from './types.js' -// Bumped to 5 alongside the Cursor per-project breakdown: prior daily -// entries recorded every Cursor session under a single 'cursor' project -// label. After the upgrade, the breakdown produces per-workspace project -// labels for new days; without invalidation the dashboard would show -// 'cursor' for historical days and `-Users-you-myproject` for new ones -// in the same window, producing a confusing mixed projection. -export const DAILY_CACHE_VERSION = 5 -// MIN_SUPPORTED_VERSION bumped to 5 too. The migration path +// 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 v2/v3/v4 cache would carry forward stale provider totals -// (single 'cursor' bucket instead of per-workspace) for the full cache -// retention window. Setting the floor to 5 forces those older caches to -// be discarded and recomputed cleanly. Confirmed by live test: -// menubar-json --period all reported cursor=$3.78 against a migrated -// v4 cache but $4.08 (correct) after the cache was discarded. -const MIN_SUPPORTED_VERSION = 5 +// 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 = { diff --git a/src/dashboard.tsx b/src/dashboard.tsx index b46dbcce..f1b49edb 100644 --- a/src/dashboard.tsx +++ b/src/dashboard.tsx @@ -9,13 +9,12 @@ 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 { getPlanUsages, type PlanUsage } from './plan-usage.js' import { planDisplayName } from './plans.js' import { getDateRange, PERIODS, PERIOD_LABELS, type Period, formatDateRangeLabel } from './cli-date.js' -import { join } from 'path' import { patchStdoutForWindows } from './ink-win.js' type View = 'dashboard' | 'optimize' | 'compare' @@ -25,6 +24,7 @@ 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', @@ -52,8 +52,10 @@ const PROVIDER_COLORS: Record = { claude: '#FF8C42', codex: '#5BF5A0', cursor: '#00B4D8', + 'ibm-bob': '#0F62FE', opencode: '#A78BFA', pi: '#F472B6', + kimi: '#B6E34A', all: '#FF8C42', } @@ -100,6 +102,14 @@ 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 } function getLayout(columns?: number): Layout { @@ -153,7 +163,30 @@ function renderPlanBar(percentUsed: number, width: number): string { return `${'▓'.repeat(width)}${'▶'.repeat(chevrons)}` } -function Overview({ projects, label, width, planUsage }: { projects: ProjectSummary[]; label: string; width: number; planUsage?: PlanUsage }) { +function planLabel(planUsage: PlanUsage): string { + const name = planDisplayName(planUsage.plan.id) + return planUsage.plan.id === 'custom' ? `${name} (${planUsage.plan.provider})` : name +} + +function planColor(planUsage: PlanUsage): string { + return planUsage.status === 'over' + ? '#F55B5B' + : planUsage.status === 'near' + ? ORANGE + : '#5BF58C' +} + +function planStatusText(planUsage: PlanUsage): string { + if (planUsage.status === 'under') { + return `Well within plan. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).` + } + if (planUsage.status === 'near') { + return `Approaching plan limit. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).` + } + return `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x your subscription value. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).` +} + +function Overview({ projects, label, width, planUsages }: { projects: ProjectSummary[]; label: string; width: number; planUsages?: PlanUsage[] }) { const totalCost = 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) @@ -165,15 +198,7 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm const allInputTokens = totalInput + totalCacheRead + totalCacheWrite const cacheHit = allInputTokens > 0 ? (totalCacheRead / allInputTokens) * 100 : 0 - const planLabel = planUsage ? `${planDisplayName(planUsage.plan.id)}: ${formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs ${formatCost(planUsage.budgetUsd)} plan` : '' - const planPct = planUsage ? `${planUsage.percentUsed.toFixed(1)}%` : '' - const planColor = planUsage - ? planUsage.status === 'over' - ? '#F55B5B' - : planUsage.status === 'near' - ? ORANGE - : '#5BF58C' - : DIM + const activePlanUsages = planUsages ?? [] return ( @@ -194,22 +219,23 @@ function Overview({ projects, label, width, planUsage }: { projects: ProjectSumm {formatTokens(totalInput)} in {formatTokens(totalOutput)} out {formatTokens(totalCacheRead)} cached {formatTokens(totalCacheWrite)} written - {planUsage && ( + {activePlanUsages.length > 0 && ( <> - - {planLabel} - - {renderPlanBar(planUsage.percentUsed, PLAN_BAR_WIDTH)} - - {planPct} - - - {planUsage.status === 'under' - ? `Well within plan. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).` - : planUsage.status === 'near' - ? `Approaching plan limit. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).` - : `${(planUsage.spentApiEquivalentUsd / Math.max(planUsage.budgetUsd, 1)).toFixed(1)}x your subscription value. Projected month: ${formatCost(planUsage.projectedMonthUsd)} (reset in ${planUsage.daysUntilReset} days).`} - + {activePlanUsages.map(planUsage => { + const color = planColor(planUsage) + return ( + + + {planLabel(planUsage)}: {formatCost(planUsage.spentApiEquivalentUsd)} API-equivalent vs {formatCost(planUsage.budgetUsd)} plan + + {renderPlanBar(planUsage.percentUsed, PLAN_BAR_WIDTH)} + + {planUsage.percentUsed.toFixed(1)}% + + {planStatusText(planUsage)} + + ) + })} )} @@ -247,16 +273,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('/') } @@ -282,7 +311,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)} @@ -442,7 +471,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) @@ -460,7 +489,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 ( @@ -513,8 +542,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 } @@ -654,8 +685,8 @@ function StatusBar({ width, showProvider, view, findingCount, optimizeAvailable, 5 6 months )} - {!isOptimize && optimizeAvailable && findingCount != null && findingCount > 0 && ( - <> o optimize ({findingCount}) + {!isOptimize && optimizeAvailable && ( + <> o optimize{findingCount != null && findingCount > 0 ? ({findingCount}) : null} )} {!isOptimize && compareAvailable && ( <> c compare @@ -671,7 +702,7 @@ function Row({ wide, width, children }: { wide: boolean; width: number; children return <>{children} } -function DashboardContent({ projects, period, columns, activeProvider, budgets, planUsage }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string; budgets?: Map; planUsage?: PlanUsage }) { +function DashboardContent({ projects, period, columns, activeProvider, budgets, planUsages }: { projects: ProjectSummary[]; period: Period; columns?: number; activeProvider?: string; budgets?: Map; planUsages?: PlanUsage[] }) { const { dashWidth, wide, halfWidth, barWidth } = getLayout(columns) const isCursor = activeProvider === 'cursor' if (projects.length === 0) return No usage data found for {PERIOD_LABELS[period]}. @@ -679,7 +710,7 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets, const days = period === 'all' ? undefined : (period === 'month' || period === '30days' ? 31 : 14) return ( - + @@ -692,11 +723,11 @@ function DashboardContent({ projects, period, columns, activeProvider, budgets, ) } -function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsage, refreshSeconds, projectFilter, excludeFilter, customRange, customRangeLabel }: { +function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, initialPlanUsages, refreshSeconds, projectFilter, excludeFilter, customRange, customRangeLabel }: { initialProjects: ProjectSummary[] initialPeriod: Period initialProvider: string - initialPlanUsage?: PlanUsage + initialPlanUsages?: PlanUsage[] refreshSeconds?: number projectFilter?: string[] excludeFilter?: string[] @@ -711,8 +742,9 @@ 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) + const [planUsages, setPlanUsages] = useState(initialPlanUsages ?? []) // 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. @@ -721,13 +753,16 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, 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(() => { @@ -744,13 +779,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) } @@ -758,23 +791,30 @@ 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, getPeriodRange(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 { + 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 @@ -783,20 +823,46 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, if (reloadGenerationRef.current !== generation) return setProjects(filteredProjects) - const usage = await getPlanUsageOrNull() + const usage = await getPlanUsages() if (reloadGenerationRef.current !== generation) return - setPlanUsage(usage ?? undefined) + setPlanUsages(usage) } catch (error) { console.error(error) } finally { 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]) @@ -826,7 +892,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, useInput((input, key) => { if (input === 'q') { exit(); return } - if (input === 'o' && findingCount > 0 && view === 'dashboard' && optimizeAvailable) { setView('optimize'); 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 @@ -864,7 +930,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, const headerLabel = customRangeLabel ?? PERIOD_LABELS[period] - if (loading) { + if (loading || optimizeLoading) { return ( {!isCustomRange && } @@ -877,7 +943,9 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, Loading {headerLabel} model data... - : Loading {headerLabel}...} + : view === 'optimize' + ? Scanning {headerLabel}... + : Loading {headerLabel}...} {view !== 'compare' && } ) @@ -891,7 +959,7 @@ function InteractiveDashboard({ initialProjects, initialPeriod, initialProvider, ? setView('dashboard')} /> : view === 'optimize' && optimizeResult ? - : } + : } {view !== 'compare' && } ) @@ -906,13 +974,13 @@ function CustomRangeBanner({ label, width }: { label: string; width: number }) { ) } -function StaticDashboard({ projects, period, activeProvider, planUsage }: { projects: ProjectSummary[]; period: Period; activeProvider?: string; planUsage?: PlanUsage }) { +function StaticDashboard({ projects, period, activeProvider, planUsages }: { projects: ProjectSummary[]; period: Period; activeProvider?: string; planUsages?: PlanUsage[] }) { const { columns } = useWindowSize() const { dashWidth } = getLayout(columns) return ( - + ) } @@ -921,16 +989,16 @@ export async function renderDashboard(period: Period = 'week', provider: string await loadPricing() const range = customRange ?? getPeriodRange(period) const filteredProjects = filterProjectsByName(await parseAllSessions(range, provider), projectFilter, excludeFilter) - const planUsage = await getPlanUsageOrNull() + const planUsages = await getPlanUsages() const isTTY = process.stdin.isTTY && process.stdout.isTTY patchStdoutForWindows() if (isTTY) { const { waitUntilExit } = render( - + ) await waitUntilExit() } else { - const { unmount } = render(, { patchConsole: false }) + const { unmount } = render(, { patchConsole: false }) unmount() } } diff --git a/src/data/litellm-snapshot.json b/src/data/litellm-snapshot.json index 2bb00ced..7a7ec4a9 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-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-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":[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],"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.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 +{"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/fs-utils.ts b/src/fs-utils.ts index d851ce4c..cc469395 100644 --- a/src/fs-utils.ts +++ b/src/fs-utils.ts @@ -1,12 +1,11 @@ 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 @@ -23,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 { @@ -46,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'}`) @@ -76,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 @@ -92,10 +104,109 @@ export async function* readSessionLines(filePath: string): AsyncGenerator { + 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/main.ts b/src/main.ts new file mode 100644 index 00000000..9216b0bd --- /dev/null +++ b/src/main.ts @@ -0,0 +1,1049 @@ +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, readPlans, saveConfig, savePlan, getConfigFilePath, type Plan, type PlanId, type PlanProvider } from './config.js' +import { clampResetDay, getPlanUsages, type PlanUsage } from './plan-usage.js' +import { getPresetPlan, isPlanId, isPlanProvider, PLAN_IDS, PLAN_PROVIDERS, 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 + provider: PlanProvider + 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, + provider: planUsage.plan.provider, + 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(), + } +} + +type JsonPlanSummaryMap = Partial> + +function toJsonPlanSummaryMap(planUsages: PlanUsage[]): JsonPlanSummaryMap { + const summaries: JsonPlanSummaryMap = {} + for (const usage of planUsages) { + summaries[usage.plan.provider] = toJsonPlanSummary(usage) + } + return summaries +} + +async function attachPlanSummaries(payload: T): Promise { + const planUsages = await getPlanUsages() + if (planUsages.length > 0) { + return { + ...payload, + plan: toJsonPlanSummary(planUsages[0]!), + plans: toJsonPlanSummaryMap(planUsages), + } + } + return payload +} + +function planLabel(plan: Plan): string { + const name = planDisplayName(plan.id) + return plan.id === 'custom' ? `${name} (${plan.provider})` : name +} + +function toPlanDisplay(plan: Plan) { + return { + id: plan.id, + monthlyUsd: plan.monthlyUsd, + provider: plan.provider, + resetDay: clampResetDay(plan.resetDay), + setAt: plan.setAt || null, + } +} + +function sortedPlans(plans: Partial>): Plan[] { + return PLAN_PROVIDERS + .map(provider => plans[provider]) + .filter((plan): plan is Plan => plan !== undefined) +} + +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; plans?: JsonPlanSummaryMap } = await attachPlanSummaries(buildJsonReport(projects, label, period)) + 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 ', 'Auto-refresh interval in seconds (0 to disable)', parseInteger, 30) + .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'report') + 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() + if (customRange) { + const label = formatDateRangeLabel(opts.from, opts.to) + const projects = filterProjectsByName( + await parseAllSessions(customRange, opts.provider), + opts.project, + opts.exclude, + ) + console.log(JSON.stringify(await attachPlanSummaries(buildJsonReport(projects, label, 'custom')), null, 2)) + } else { + await runJsonReport(period, opts.provider, opts.project, opts.exclude) + } + return + } + const customRangeLabel = customRange ? formatDateRangeLabel(opts.from, opts.to) : undefined + await renderDashboard(period, opts.provider, opts.refresh, opts.project, opts.exclude, customRange, customRangeLabel) + }) + +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 + 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) => { + assertFormat(opts.format, ['terminal', 'menubar-json', 'json'], 'status') + 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 todayRange: DateRange = { start: todayStart, end: now } + const todayStr = toDateString(todayStart) + const yesterdayStr = toDateString(new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1)) + const rangeStartStr = toDateString(periodInfo.range.start) + const rangeEndStr = toDateString(periodInfo.range.end) + const isAllProviders = pf === 'all' + + const cache = await hydrateCache() + let todayAllProjects: ProjectSummary[] | null = null + let todayAllDays: ReturnType | null = null + + const getTodayAllProjects = async (): Promise => { + if (!todayAllProjects) { + todayAllProjects = fp(await parseAllSessions(todayRange, 'all')) + } + return todayAllProjects + } + + const getTodayAllDays = async (): Promise> => { + if (!todayAllDays) { + todayAllDays = aggregateProjectsIntoDays(await getTodayAllProjects()) + } + return todayAllDays + } + + // 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 today's all-provider sessions once; historical data comes from cache to avoid + // double-counting. Reusing the same parsed object is important for the menubar path: + // large active sessions can OOM if this command retains multiple near-identical scans. + const todayProjects = await getTodayAllProjects() + const todayDays = await getTodayAllDays() + 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) { + const allDaysForProviders = [ + ...getDaysInRange(cache, rangeStartStr, yesterdayStr), + ...(await getTodayAllDays()).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(now.getFullYear(), now.getMonth(), now.getDate() - BACKFILL_DAYS)) + const allCacheDays = getDaysInRange(cache, historyStartStr, yesterdayStr) + const fullHistory = [...allCacheDays, ...(await getTodayAllDays()).filter(d => d.date === todayStr)] + 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') { + const todayProjects = fp(await parseAllSessions(getDateRange('today').range, pf)) + const todayData = buildPeriodData('today', todayProjects) + clearSessionCache() + const monthProjects = fp(await parseAllSessions(getDateRange('month').range, pf)) + const monthData = buildPeriodData('month', monthProjects) + clearSessionCache() + const { code, rate } = getCurrency() + const payload: { + currency: string + today: { cost: number; calls: number } + month: { cost: number; calls: number } + plan?: JsonPlanSummary + plans?: JsonPlanSummaryMap + } = { + 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 }, + } + console.log(JSON.stringify(await attachPlanSummaries(payload))) + return + } + + const monthProjects2 = fp(await parseAllSessions(getDateRange('month').range, pf)) + clearSessionCache() + console.log(renderStatusBar(monthProjects2)) + }) + +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)', parseInteger, 30) + .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'today') + if (opts.format === 'json') { + await runJsonReport('today', opts.provider, opts.project, opts.exclude) + return + } + 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)', parseInteger, 30) + .action(async (opts) => { + assertFormat(opts.format, ['tui', 'json'], 'month') + if (opts.format === 'json') { + await runJsonReport('month', opts.provider, opts.project, opts.exclude) + return + } + await renderDashboard('month', opts.provider, opts.refresh, opts.project, opts.exclude) + }) + +program + .command('export') + .description('Export usage data to CSV or JSON') + .option('-f, --format ', 'Export format: csv, json', 'csv') + .option('-o, --output ', 'Output file path') + .option('--from ', 'Start date (YYYY-MM-DD). Exports a single custom period when set') + .option('--to ', 'End date (YYYY-MM-DD). Exports a single custom period when set') + .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) => { + assertFormat(opts.format, ['csv', 'json'], 'export') + await loadPricing() + const pf = opts.provider + const fp = (p: ProjectSummary[]) => filterProjectsByName(p, opts.project, opts.exclude) + 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) + } + + let periods: PeriodExport[] + if (customRange) { + periods = [{ label: formatDateRangeLabel(opts.from, opts.to), projects: fp(await parseAllSessions(customRange, pf)) }] + clearSessionCache() + } else { + const thirtyDayProjects = fp(await parseAllSessions(getDateRange('30days').range, pf)) + clearSessionCache() + periods = [ + { label: 'Today', projects: filterProjectsByDateRange(thirtyDayProjects, getDateRange('today').range) }, + { label: '7 Days', projects: filterProjectsByDateRange(thirtyDayProjects, getDateRange('week').range) }, + { label: '30 Days', projects: thirtyDayProjects }, + ] + } + + 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) + } + + const exportedLabel = customRange ? formatDateRangeLabel(opts.from, opts.to) : 'Today + 7 Days + 30 Days' + console.log(`\n Exported (${exportedLabel}) 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') + .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 }) => { + assertFormat(opts?.format ?? 'text', ['text', 'json'], 'plan') + const mode = action ?? 'show' + const providerOption = opts?.provider + if (providerOption !== undefined && !isPlanProvider(providerOption)) { + console.error(`\n --provider must be one of: all, claude, codex, cursor; got "${providerOption}".\n`) + process.exitCode = 1 + return + } + + if (mode === 'show') { + const plans = sortedPlans(await readPlans()) + .filter(plan => plan.id !== 'none') + .filter(plan => !providerOption || providerOption === 'all' || plan.provider === providerOption) + if (opts?.format === 'json') { + if (plans.length === 0) { + console.log(JSON.stringify({ id: 'none', monthlyUsd: 0, provider: 'all', resetDay: 1, setAt: null })) + return + } + console.log(JSON.stringify({ + ...toPlanDisplay(plans[0]!), + plans: Object.fromEntries(plans.map(plan => [plan.provider, toPlanDisplay(plan)])), + })) + return + } + if (plans.length === 0) { + console.log('\n Plan: none') + console.log(' API-pricing view is active.') + console.log(` Config: ${getConfigFilePath()}\n`) + return + } + console.log(`\n Plans: ${plans.length}`) + for (const plan of plans) { + console.log(` ${plan.provider}: ${planLabel(plan)} (${plan.id})`) + console.log(` Budget: $${plan.monthlyUsd}/month`) + console.log(` Reset day: ${clampResetDay(plan.resetDay)}`) + if (plan.setAt) console.log(` Set at: ${plan.setAt}`) + } + console.log(` Config: ${getConfigFilePath()}\n`) + return + } + + if (mode === 'reset') { + await clearPlan(providerOption) + if (providerOption) { + console.log(`\n Plan reset for ${providerOption}.\n`) + } else { + 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: ${PLAN_IDS.join(', ')}; 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(providerOption) + if (providerOption) { + console.log(`\n Plan reset for ${providerOption}.\n`) + } else { + 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 = providerOption ?? 'all' + 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 + } + + if (providerOption === 'all') { + console.error(`\n ${id} is a ${preset.provider} plan; omit --provider or use --provider ${preset.provider}.\n`) + process.exitCode = 1 + return + } + + if (providerOption && providerOption !== preset.provider) { + console.error(`\n ${id} is a ${preset.provider} plan; use --provider ${preset.provider} or omit --provider.\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() + 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() + const { range } = getDateRange(opts.period) + await renderCompare(range, opts.provider) + }) + +program + .command('models') + .description('Per-model token + cost table, optionally exploded by task type') + .option('-p, --period ', 'Analysis period: today, week, 30days, month, all', '30days') + .option('--from ', 'Custom range start (YYYY-MM-DD)') + .option('--to ', 'Custom range end (YYYY-MM-DD)') + .option('--provider ', 'Filter by provider (e.g. claude, codex, cursor)', 'all') + .option('--task ', 'Filter to one task type (e.g. feature, debugging, refactoring)') + .option('--by-task', 'One row per (provider, model, task) instead of one row per (provider, model)') + .option('--top ', 'Show only the top N rows', (v: string) => parseInt(v, 10)) + .option('--min-cost ', 'Hide rows below this cost threshold', (v: string) => parseFloat(v)) + .option('--no-totals', 'Suppress the footer totals row') + .option('--format ', 'Output format: table, markdown, json, csv', 'table') + .action(async (opts) => { + const { aggregateModels, renderTable, renderMarkdown, renderJson, renderCsv } = await import('./models-report.js') + await loadPricing() + + let range + if (opts.from || opts.to) { + const customRange = parseDateRangeFlags(opts.from, opts.to) + if (!customRange) { + process.stderr.write('codeburn: --from and --to must be valid YYYY-MM-DD dates\n') + process.exit(1) + } + range = customRange + } else { + range = getDateRange(opts.period).range + } + + const projects = await parseAllSessions(range, opts.provider) + const rows = await aggregateModels(projects, { + byTask: !!opts.byTask, + taskFilter: opts.task, + topN: typeof opts.top === 'number' && Number.isFinite(opts.top) ? opts.top : undefined, + minCost: typeof opts.minCost === 'number' && Number.isFinite(opts.minCost) ? opts.minCost : 0.01, + }) + + const fmt = (opts.format ?? 'table').toLowerCase() + if (rows.length === 0 && (fmt === 'table' || fmt === 'markdown')) { + process.stdout.write('No model usage found for the selected period.\n') + return + } + if (fmt === 'json') { + process.stdout.write(renderJson(rows) + '\n') + } else if (fmt === 'csv') { + process.stdout.write(renderCsv(rows, { byTask: !!opts.byTask }) + '\n') + } else if (fmt === 'markdown' || fmt === 'md') { + process.stdout.write(renderMarkdown(rows, { byTask: !!opts.byTask, showTotals: opts.totals !== false }) + '\n') + } else if (fmt === 'table') { + process.stdout.write(renderTable(rows, { byTask: !!opts.byTask, showTotals: opts.totals !== false }) + '\n') + } else { + process.stderr.write(`codeburn: unknown --format "${opts.format}". Choose table, markdown, json, or csv.\n`) + process.exit(1) + } + }) + +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() + 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/menubar-installer.ts b/src/menubar-installer.ts index 397a81c7..915aefaf 100644 --- a/src/menubar-installer.ts +++ b/src/menubar-installer.ts @@ -1,27 +1,56 @@ import { spawn } from 'node:child_process' import { createHash } from 'node:crypto' import { createWriteStream } from 'node:fs' -import { mkdir, mkdtemp, readFile, rename, rm, stat } from 'node:fs/promises' +import { chmod, mkdir, mkdtemp, readFile, rename, rm, stat, writeFile } from 'node:fs/promises' import { homedir, platform, tmpdir } from 'node:os' import { join } from 'node:path' import { pipeline } from 'node:stream/promises' import { Readable } from 'node:stream' -/// Public GitHub repo that hosts signed macOS release builds. `/releases/latest` returns the -/// newest tagged release; we filter its assets list for our zipped .app bundle. -const RELEASE_API = 'https://api.github.com/repos/getagentseal/codeburn/releases/latest' +/// Public GitHub repo that hosts macOS release builds. CLI and menubar releases share +/// the repository, so we scan recent releases and choose the newest `mac-v*` release +/// that actually contains the menubar zip. +const RELEASE_API = 'https://api.github.com/repos/getagentseal/codeburn/releases?per_page=20' const APP_BUNDLE_NAME = 'CodeBurnMenubar.app' -const ASSET_PATTERN = /^CodeBurnMenubar-.*\.zip$/ -const CHECKSUM_PATTERN = /^CodeBurnMenubar-.*\.zip\.sha256$/ +const EXPECTED_BUNDLE_ID = 'org.agentseal.codeburn-menubar' +const VERSIONED_ASSET_PATTERN = /^CodeBurnMenubar-v.+\.zip$/ const APP_PROCESS_NAME = 'CodeBurnMenubar' const SUPPORTED_OS = 'darwin' const MIN_MACOS_MAJOR = 14 +const PERSISTED_CLI_PATH = join(homedir(), 'Library', 'Application Support', 'CodeBurn', 'codeburn-cli-path.v1') export type InstallResult = { installedPath: string; launched: boolean } -type ReleaseAsset = { name: string; browser_download_url: string } -type ReleaseResponse = { tag_name: string; assets: ReleaseAsset[] } -type ResolvedAssets = { zip: ReleaseAsset; checksum: ReleaseAsset | null } +export type ReleaseAsset = { name: string; browser_download_url: string } +export type ReleaseResponse = { tag_name: string; assets: ReleaseAsset[] } +export type ResolvedAssets = { release: ReleaseResponse; zip: ReleaseAsset; checksum: ReleaseAsset } + +export function resolveMenubarReleaseAssets(release: ReleaseResponse): ResolvedAssets { + const zip = release.assets.find(a => VERSIONED_ASSET_PATTERN.test(a.name)) + if (!zip) { + throw new Error( + `No ${APP_BUNDLE_NAME} versioned zip found in release ${release.tag_name}. ` + + `Check https://github.com/getagentseal/codeburn/releases.` + ) + } + const checksum = release.assets.find(a => a.name === `${zip.name}.sha256`) + if (!checksum) { + throw new Error(`Missing checksum asset ${zip.name}.sha256 in release ${release.tag_name}.`) + } + return { release, zip, checksum } +} + +export function resolveLatestMenubarReleaseAssets(releases: ReleaseResponse[]): ResolvedAssets { + for (const release of releases) { + if (!release.tag_name.startsWith('mac-v')) continue + try { + return resolveMenubarReleaseAssets(release) + } catch { + continue + } + } + throw new Error('No mac-v* release with a CodeBurnMenubar-v*.zip and checksum was found.') +} function userApplicationsDir(): string { return join(homedir(), 'Applications') @@ -70,16 +99,8 @@ async function fetchLatestReleaseAssets(): Promise { if (!response.ok) { throw new Error(`GitHub release lookup failed: HTTP ${response.status}`) } - const body = await response.json() as ReleaseResponse - const zip = body.assets.find(a => ASSET_PATTERN.test(a.name)) - if (!zip) { - throw new Error( - `No ${APP_BUNDLE_NAME} zip found in release ${body.tag_name}. ` + - `Check https://github.com/getagentseal/codeburn/releases.` - ) - } - const checksum = body.assets.find(a => CHECKSUM_PATTERN.test(a.name)) ?? null - return { zip, checksum } + const body = await response.json() as ReleaseResponse[] + return resolveLatestMenubarReleaseAssets(body) } async function verifyChecksum(archivePath: string, checksumUrl: string): Promise { @@ -128,6 +149,57 @@ async function runCommand(command: string, args: string[]): Promise { }) } +async function captureCommand(command: string, args: string[]): Promise { + return new Promise((resolve, reject) => { + const proc = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] }) + let out = '' + let err = '' + proc.stdout.on('data', (chunk: Buffer) => { out += chunk.toString() }) + proc.stderr.on('data', (chunk: Buffer) => { err += chunk.toString() }) + proc.on('error', reject) + proc.on('close', (code) => { + if (code === 0) resolve(out.trim()) + else reject(new Error(`${command} exited with status ${code}${err ? `: ${err.trim()}` : ''}`)) + }) + }) +} + +async function verifyBundleIdentity(appPath: string): Promise { + const bundleID = await captureCommand('/usr/libexec/PlistBuddy', [ + '-c', + 'Print :CFBundleIdentifier', + join(appPath, 'Contents', 'Info.plist'), + ]) + if (bundleID !== EXPECTED_BUNDLE_ID) { + throw new Error(`Unexpected menubar bundle id ${bundleID}; expected ${EXPECTED_BUNDLE_ID}.`) + } + await runCommand('/usr/bin/codesign', ['--verify', '--deep', '--strict', appPath]) +} + +async function resolvePersistentCodeburnPath(): Promise { + const path = await captureCommand('/usr/bin/env', [ + 'PATH=/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin', + 'which', + 'codeburn', + ]) + if (!path.startsWith('/')) { + throw new Error('Resolved codeburn path is not absolute.') + } + if (path.includes('/_npx/') || path.includes('/.npm/_npx/')) { + throw new Error( + 'The menubar app needs a persistent codeburn command. Install CodeBurn globally first: npm install -g codeburn' + ) + } + return path +} + +async function persistCodeburnPath(): Promise { + const cliPath = await resolvePersistentCodeburnPath() + await mkdir(join(homedir(), 'Library', 'Application Support', 'CodeBurn'), { recursive: true, mode: 0o700 }) + await writeFile(PERSISTED_CLI_PATH, `${cliPath}\n`, { mode: 0o600 }) + await chmod(PERSISTED_CLI_PATH, 0o600) +} + async function isAppRunning(): Promise { return new Promise((resolve) => { const proc = spawn('/usr/bin/pgrep', ['-f', APP_PROCESS_NAME]) @@ -150,6 +222,7 @@ async function killRunningApp(): Promise { export async function installMenubarApp(options: { force?: boolean } = {}): Promise { await ensureSupportedPlatform() + await persistCodeburnPath() const appsDir = userApplicationsDir() const targetPath = join(appsDir, APP_BUNDLE_NAME) @@ -171,21 +244,20 @@ export async function installMenubarApp(options: { force?: boolean } = {}): Prom console.log(`Downloading ${zip.name}...`) await downloadToFile(zip.browser_download_url, archivePath) - if (checksum) { - console.log('Verifying checksum...') - await verifyChecksum(archivePath, checksum.browser_download_url) - } else { - console.log('Warning: no checksum file found in release, skipping verification.') - } + console.log('Verifying checksum...') + await verifyChecksum(archivePath, checksum.browser_download_url) console.log('Unpacking...') - await runCommand('/usr/bin/unzip', ['-q', archivePath, '-d', stagingDir]) + await runCommand('/usr/bin/ditto', ['-x', '-k', archivePath, stagingDir]) const unpackedApp = join(stagingDir, APP_BUNDLE_NAME) if (!(await exists(unpackedApp))) { throw new Error(`Archive did not contain ${APP_BUNDLE_NAME}.`) } + console.log('Verifying app bundle...') + await verifyBundleIdentity(unpackedApp) + // Clear Gatekeeper's quarantine xattr. Without this, the first launch shows the // "cannot verify developer" prompt even for a signed + notarized app when the bundle // was delivered via curl/fetch instead of the Mac App Store. diff --git a/src/models-report.ts b/src/models-report.ts index ab70646b..70e91702 100644 --- a/src/models-report.ts +++ b/src/models-report.ts @@ -43,7 +43,7 @@ type Bucket = { } type ModelKey = string -type CategoryKey = string +type CategoryKey = TaskCategory function bucketKey(provider: string, model: string, category: TaskCategory | null): string { return `${provider} ${model} ${category ?? ''}` diff --git a/src/models.ts b/src/models.ts index e4441e0a..5d48822c 100644 --- a/src/models.ts +++ b/src/models.ts @@ -25,6 +25,7 @@ type SnapshotEntry = [number, number, number | null, number | null] const LITELLM_URL = 'https://raw.githubusercontent.com/BerriAI/litellm/main/model_prices_and_context_window.json' const CACHE_TTL_MS = 24 * 60 * 60 * 1000 const WEB_SEARCH_COST = 0.01 +const ONE_HOUR_CACHE_WRITE_MULTIPLIER_FROM_FIVE_MINUTE_RATE = 1.6 const FAST_MULTIPLIERS: Record = { 'claude-opus-4-7': 6, @@ -166,10 +167,14 @@ const BUILTIN_ALIASES: Record = { 'copilot-auto': 'claude-sonnet-4-5', 'copilot-openai-auto': 'gpt-5.3-codex', 'copilot-anthropic-auto': 'claude-sonnet-4-5', + 'ibm-bob-auto': 'claude-sonnet-4-5', 'kiro-auto': 'claude-sonnet-4-5', 'cline-auto': 'claude-sonnet-4-5', 'openclaw-auto': 'claude-sonnet-4-5', 'qwen-auto': 'claude-sonnet-4-5', + 'kimi-auto': 'kimi-k2-thinking', + 'kimi-code': 'kimi-k2-thinking', + 'kimi-for-coding': 'kimi-k2-thinking', // Cursor emits dot-version tier-last names plus tier/reasoning suffixes // that LiteLLM does not index (`-high`, `-low`, `-medium`, `-thinking`, // `-high-thinking`, `-fast-mode`). Missing aliases here surface as $0 in @@ -310,6 +315,7 @@ export function calculateCost( cacheReadTokens: number, webSearchRequests: number, speed: 'standard' | 'fast' = 'standard', + oneHourCacheCreationTokens = 0, ): number { const costs = getModelCosts(model) if (!costs) { @@ -335,11 +341,15 @@ export function calculateCost( // from real spend in aggregate totals. NaN is also handled here; the // arithmetic below short-circuits to 0 when any operand is non-finite. const safe = (n: number) => (Number.isFinite(n) && n > 0 ? n : 0) + const safeOneHourCacheCreation = safe(oneHourCacheCreationTokens) + const safeCacheCreation = Math.max(safe(cacheCreationTokens), safeOneHourCacheCreation) + const safeFiveMinuteCacheCreation = Math.max(0, safeCacheCreation - safeOneHourCacheCreation) return multiplier * ( safe(inputTokens) * costs.inputCostPerToken + safe(outputTokens) * costs.outputCostPerToken + - safe(cacheCreationTokens) * costs.cacheWriteCostPerToken + + safeFiveMinuteCacheCreation * costs.cacheWriteCostPerToken + + safeOneHourCacheCreation * costs.cacheWriteCostPerToken * ONE_HOUR_CACHE_WRITE_MULTIPLIER_FROM_FIVE_MINUTE_RATE + safe(cacheReadTokens) * costs.cacheReadCostPerToken + safe(webSearchRequests) * costs.webSearchCostPerRequest ) @@ -351,10 +361,12 @@ const autoModelNames: Record = { 'copilot-auto': 'Copilot (auto)', 'copilot-openai-auto': 'Copilot (OpenAI)', 'copilot-anthropic-auto': 'Copilot (Anthropic)', + 'ibm-bob-auto': 'IBM Bob (auto)', 'kiro-auto': 'Kiro (auto)', 'cline-auto': 'Cline (auto)', 'openclaw-auto': 'OpenClaw (auto)', 'qwen-auto': 'Qwen (auto)', + 'kimi-auto': 'Kimi (auto)', } const SHORT_NAMES: Record = { @@ -398,6 +410,17 @@ const SHORT_NAMES: Record = { 'gemini-3-flash-preview': 'Gemini 3 Flash', 'gemini-2.5-pro': 'Gemini 2.5 Pro', 'gemini-2.5-flash': 'Gemini 2.5 Flash', + 'kimi-k2-thinking-turbo': 'Kimi K2 Thinking Turbo', + 'kimi-k2-thinking': 'Kimi K2 Thinking', + 'kimi-thinking-preview': 'Kimi Thinking', + 'kimi-k2.6': 'Kimi K2.6', + 'kimi-k2.5': 'Kimi K2.5', + 'kimi-k2p5': 'Kimi K2.5', + 'kimi-k2-instruct': 'Kimi K2 Instruct', + 'kimi-k2-0905': 'Kimi K2', + 'kimi-k2': 'Kimi K2', + 'kimi-latest': 'Kimi Latest', + 'moonshot-v1': 'Moonshot v1', 'deepseek-coder-max': 'DeepSeek Coder Max', 'deepseek-coder': 'DeepSeek Coder', 'deepseek-r1': 'DeepSeek R1', diff --git a/src/optimize.ts b/src/optimize.ts index bd2aa0ce..c672bac0 100644 --- a/src/optimize.ts +++ b/src/optimize.ts @@ -6,6 +6,7 @@ import { homedir } from 'os' import { readSessionLines, readSessionFileSync } from './fs-utils.js' import { discoverAllSessions } from './providers/index.js' +import { parseJsonlLine, shouldSkipLine } from './parser.js' import type { DateRange, ProjectSummary } from './types.js' import { formatCost } from './currency.js' import { formatTokens } from './format.js' @@ -141,6 +142,8 @@ const SHELL_PROFILES = ['.zshrc', '.bashrc', '.bash_profile', '.profile'] const TOP_ITEMS_PREVIEW = 3 const GHOST_NAMES_PREVIEW = 5 const GHOST_CLEANUP_COMMANDS_LIMIT = 10 +const OPTIMIZE_TEXT_CAP = 2000 +const OPTIMIZE_FIELD_CAP = 500 // ============================================================================ // Types @@ -209,7 +212,33 @@ type ScanData = { // JSONL scanner // ============================================================================ -const FILE_READ_CONCURRENCY = 16 +function cappedString(value: unknown, cap = OPTIMIZE_FIELD_CAP): string | undefined { + return typeof value === 'string' ? value.slice(0, cap) : undefined +} + +function compactOptimizeInput(name: string, input: unknown): Record { + if (!input || typeof input !== 'object') return {} + const raw = input as Record + if (isReadTool(name)) { + const filePath = cappedString(raw['file_path'], OPTIMIZE_TEXT_CAP) + return filePath ? { file_path: filePath } : {} + } + if (name === 'Agent' || name === 'Task') { + const subagentType = cappedString(raw['subagent_type']) + return subagentType ? { subagent_type: subagentType } : {} + } + if (name === 'Skill') { + const skill = cappedString(raw['skill']) + const skillName = cappedString(raw['name']) + return { + ...(skill ? { skill } : {}), + ...(skillName ? { name: skillName } : {}), + } + } + return {} +} + +const FILE_READ_CONCURRENCY = 4 const RESULT_CACHE_TTL_MS = 60_000 const RECENT_WINDOW_HOURS = 48 const RECENT_WINDOW_MS = RECENT_WINDOW_HOURS * 60 * 60 * 1000 @@ -286,10 +315,19 @@ export async function scanJsonlFile( const sessionId = basename(filePath, '.jsonl') let lastVersion = '' - for await (const line of readSessionLines(filePath)) { - if (!line.trim()) continue - let entry: Record - try { entry = JSON.parse(line) } catch { continue } + const skipThreshold = dateRange + ? new Date(dateRange.start.getTime() - 86_400_000).toISOString() + : null + const skipFn = dateRange + ? (head: string) => shouldSkipLine(head, skipThreshold!) + : undefined + const lines = readSessionLines(filePath, skipFn, { largeLineAsBuffer: true }) + for await (const line of lines) { + if (typeof line === 'string' && !line.trim()) continue + if (Buffer.isBuffer(line) && line.length === 0) continue + const parsed = parseJsonlLine(line) + if (!parsed) continue + const entry = parsed as Record if (entry.version && typeof entry.version === 'string') lastVersion = entry.version @@ -304,11 +342,15 @@ export async function scanJsonlFile( const msg = entry.message as Record | undefined const msgContent = msg?.content if (typeof msgContent === 'string') { - userMessages.push(msgContent) + userMessages.push(msgContent.slice(0, OPTIMIZE_TEXT_CAP)) } else if (Array.isArray(msgContent)) { + let remaining = OPTIMIZE_TEXT_CAP for (const block of msgContent) { + if (remaining <= 0) break if (block && typeof block === 'object' && block.type === 'text' && typeof block.text === 'string') { - userMessages.push(block.text) + const text = block.text.slice(0, remaining) + userMessages.push(text) + remaining -= text.length } } } @@ -330,9 +372,10 @@ export async function scanJsonlFile( for (const block of blocks) { if (block.type !== 'tool_use') continue + const name = typeof block.name === 'string' ? block.name : '' calls.push({ - name: block.name as string, - input: (block.input as Record) ?? {}, + name, + input: compactOptimizeInput(name, block.input), sessionId, project, recent, diff --git a/src/parser.ts b/src/parser.ts index 50fa648d..51c70b0e 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -5,6 +5,20 @@ import { calculateCost, getShortModelName } from './models.js' import { discoverAllSessions, getProvider } from './providers/index.js' import { flushCodexCache } from './codex-cache.js' import { flushAntigravityCache } from './providers/antigravity.js' +import { isSqliteBusyError } from './sqlite.js' +import { + type CachedCall, + type CachedFile, + type CachedTurn, + type ProviderSection, + type SessionCache, + cleanupOrphanedTempFiles, + computeEnvFingerprint, + fingerprintFile, + loadCache, + reconcileFile, + saveCache, +} from './session-cache.js' import type { ParsedProviderCall } from './providers/types.js' import type { AssistantMessageContent, @@ -31,7 +45,18 @@ function normalizeProjectPathKey(projectPath: string): string { return (normalized.replace(/\/+$/, '') || normalized).toLowerCase() } -function parseJsonlLine(line: string): JournalEntry | null { +const LARGE_JSONL_LINE_BYTES = 32 * 1024 + +export function parseJsonlLine(line: string | Buffer): JournalEntry | null { + if (Buffer.isBuffer(line)) { + if (line.length > LARGE_JSONL_LINE_BYTES) return parseLargeJsonlBuffer(line) + try { + return JSON.parse(line.toString('utf-8')) as JournalEntry + } catch { + return null + } + } + if (line.length > LARGE_JSONL_LINE_BYTES) return parseLargeJsonlLine(line) try { return JSON.parse(line) as JournalEntry } catch { @@ -39,6 +64,829 @@ function parseJsonlLine(line: string): JournalEntry | null { } } +const RAW_HEAD_BYTES = 2048 + +type JsonValueBounds = { + start: number + end: number + kind: 'string' | 'object' | 'array' | 'scalar' +} + +function findJsonStringEnd(source: string, start: number, limit = source.length): number { + for (let i = start + 1; i < limit; i++) { + const ch = source.charCodeAt(i) + if (ch === 0x5c) { + i++ + continue + } + if (ch === 0x22) return i + } + return -1 +} + +function findJsonContainerEnd(source: string, start: number, open: number, close: number, limit = source.length): number { + let depth = 0 + let inString = false + for (let i = start; i < limit; i++) { + const ch = source.charCodeAt(i) + if (inString) { + if (ch === 0x5c) { + i++ + } else if (ch === 0x22) { + inString = false + } + continue + } + if (ch === 0x22) { + inString = true + } else if (ch === open) { + depth++ + } else if (ch === close) { + depth-- + if (depth === 0) return i + } + } + return -1 +} + +function findJsonValueBounds(source: string, start: number, limit = source.length): JsonValueBounds | null { + let i = start + while (i < limit && /\s/.test(source[i]!)) i++ + if (i >= limit) return null + const ch = source.charCodeAt(i) + if (ch === 0x22) { + const end = findJsonStringEnd(source, i, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'string' } + } + if (ch === 0x7b) { + const end = findJsonContainerEnd(source, i, 0x7b, 0x7d, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'object' } + } + if (ch === 0x5b) { + const end = findJsonContainerEnd(source, i, 0x5b, 0x5d, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'array' } + } + let end = i + while (end < limit) { + const c = source.charCodeAt(end) + if (c === 0x2c || c === 0x7d || c === 0x5d || /\s/.test(source[end]!)) break + end++ + } + return { start: i, end, kind: 'scalar' } +} + +function findObjectFieldValue(source: string, objectStart: number, objectEnd: number, field: string): JsonValueBounds | null { + if (source.charCodeAt(objectStart) !== 0x7b) return null + let i = objectStart + 1 + while (i < objectEnd - 1) { + while (i < objectEnd && /\s/.test(source[i]!)) i++ + if (source.charCodeAt(i) === 0x2c) { + i++ + continue + } + if (source.charCodeAt(i) !== 0x22) { + i++ + continue + } + const keyEnd = findJsonStringEnd(source, i, objectEnd) + if (keyEnd === -1) return null + const key = source.slice(i + 1, keyEnd) + i = keyEnd + 1 + while (i < objectEnd && /\s/.test(source[i]!)) i++ + if (source.charCodeAt(i) !== 0x3a) continue + const value = findJsonValueBounds(source, i + 1, objectEnd) + if (!value) return null + if (key === field) return value + i = value.end + } + return null +} + +function readJsonString(source: string, bounds: JsonValueBounds | null, cap = Number.POSITIVE_INFINITY): string | undefined { + if (!bounds || bounds.kind !== 'string') return undefined + let out = '' + for (let i = bounds.start + 1; i < bounds.end - 1 && out.length < cap; i++) { + const ch = source[i]! + if (ch !== '\\') { + out += ch + continue + } + const next = source[++i] + if (!next) break + if (next === 'n') out += '\n' + else if (next === 'r') out += '\r' + else if (next === 't') out += '\t' + else if (next === 'b') out += '\b' + else if (next === 'f') out += '\f' + else if (next === 'u' && i + 4 < bounds.end) { + const hex = source.slice(i + 1, i + 5) + const code = Number.parseInt(hex, 16) + if (Number.isFinite(code)) out += String.fromCharCode(code) + i += 4 + } else { + out += next + } + } + return out +} + +function readJsonNumberField(source: string, objectBounds: JsonValueBounds | null, field: string): number | undefined { + if (!objectBounds || objectBounds.kind !== 'object') return undefined + const bounds = findObjectFieldValue(source, objectBounds.start, objectBounds.end, field) + if (!bounds) return undefined + const value = Number(source.slice(bounds.start, bounds.end)) + return Number.isFinite(value) ? value : undefined +} + +function parseLargeUsage(source: string, usageBounds: JsonValueBounds | null) { + const usage: AssistantMessageContent['usage'] = { + input_tokens: readJsonNumberField(source, usageBounds, 'input_tokens') ?? 0, + output_tokens: readJsonNumberField(source, usageBounds, 'output_tokens') ?? 0, + cache_creation_input_tokens: readJsonNumberField(source, usageBounds, 'cache_creation_input_tokens'), + cache_read_input_tokens: readJsonNumberField(source, usageBounds, 'cache_read_input_tokens'), + } + + if (usageBounds?.kind === 'object') { + const cacheCreation = findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'cache_creation') + const ephemeral5m = readJsonNumberField(source, cacheCreation, 'ephemeral_5m_input_tokens') + const ephemeral1h = readJsonNumberField(source, cacheCreation, 'ephemeral_1h_input_tokens') + if (ephemeral5m !== undefined || ephemeral1h !== undefined) { + ;(usage as AssistantMessageContent['usage']).cache_creation = { + ...(ephemeral5m !== undefined ? { ephemeral_5m_input_tokens: ephemeral5m } : {}), + ...(ephemeral1h !== undefined ? { ephemeral_1h_input_tokens: ephemeral1h } : {}), + } + } + + const serverToolUse = findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'server_tool_use') + const webSearch = readJsonNumberField(source, serverToolUse, 'web_search_requests') + const webFetch = readJsonNumberField(source, serverToolUse, 'web_fetch_requests') + if (webSearch !== undefined || webFetch !== undefined) { + ;(usage as AssistantMessageContent['usage']).server_tool_use = { + ...(webSearch !== undefined ? { web_search_requests: webSearch } : {}), + ...(webFetch !== undefined ? { web_fetch_requests: webFetch } : {}), + } + } + + const speed = readJsonString(source, findObjectFieldValue(source, usageBounds.start, usageBounds.end, 'speed')) + if (speed === 'standard' || speed === 'fast') usage.speed = speed + } + + return usage +} + +function extractLargeToolBlocks(source: string, contentBounds: JsonValueBounds | null): ToolUseBlock[] { + if (!contentBounds || contentBounds.kind !== 'array') return [] + const tools: ToolUseBlock[] = [] + let i = contentBounds.start + 1 + while (i < contentBounds.end - 1 && tools.length < MAX_TOOL_BLOCKS) { + while (i < contentBounds.end && /\s/.test(source[i]!)) i++ + if (source.charCodeAt(i) === 0x2c) { + i++ + continue + } + if (source.charCodeAt(i) !== 0x7b) { + i++ + continue + } + const objectEnd = findJsonContainerEnd(source, i, 0x7b, 0x7d, contentBounds.end) + if (objectEnd === -1) break + const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const } + const blockType = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'type')) + if (blockType === 'tool_use') { + const name = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'name')) ?? '' + const id = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'id')) ?? '' + const inputBounds = findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'input') + const input: Record = {} + if (inputBounds?.kind === 'object') { + if (name === 'Skill') { + const skill = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'skill'), 200) + const skillName = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'name'), 200) + if (skill !== undefined) input['skill'] = skill + if (skillName !== undefined) input['name'] = skillName + } else if (name === 'Read' || name === 'FileReadTool') { + const filePath = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'file_path'), BASH_COMMAND_CAP) + if (filePath !== undefined) input['file_path'] = filePath + } else if (name === 'Agent' || name === 'Task') { + const subagentType = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'subagent_type'), 200) + if (subagentType !== undefined) input['subagent_type'] = subagentType + } else if (BASH_TOOLS.has(name)) { + const command = readJsonString(source, findObjectFieldValue(source, inputBounds.start, inputBounds.end, 'command'), BASH_COMMAND_CAP) + if (command !== undefined) input['command'] = command + } + } + tools.push({ type: 'tool_use', id, name, input }) + } + i = objectEnd + 1 + } + return tools +} + +function extractLargeUserText(source: string, contentBounds: JsonValueBounds | null): string | undefined { + if (!contentBounds) return undefined + if (contentBounds.kind === 'string') return readJsonString(source, contentBounds, USER_TEXT_CAP) + if (contentBounds.kind !== 'array') return undefined + + let text = '' + let i = contentBounds.start + 1 + while (i < contentBounds.end - 1 && text.length < USER_TEXT_CAP) { + while (i < contentBounds.end && /\s/.test(source[i]!)) i++ + if (source.charCodeAt(i) === 0x2c) { + i++ + continue + } + if (source.charCodeAt(i) !== 0x7b) { + i++ + continue + } + const objectEnd = findJsonContainerEnd(source, i, 0x7b, 0x7d, contentBounds.end) + if (objectEnd === -1) break + const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const } + const type = readJsonString(source, findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'type')) + if (type === 'text' || type === 'input_text') { + const part = readJsonString( + source, + findObjectFieldValue(source, objectBounds.start, objectBounds.end, 'text'), + USER_TEXT_CAP - text.length, + ) + if (part) text += (text ? ' ' : '') + part + } + i = objectEnd + 1 + } + return text || undefined +} + +function extractLargeAddedNames(source: string, attachmentBounds: JsonValueBounds | null): string[] { + if (!attachmentBounds || attachmentBounds.kind !== 'object') return [] + const attachmentType = readJsonString(source, findObjectFieldValue(source, attachmentBounds.start, attachmentBounds.end, 'type')) + if (attachmentType !== 'deferred_tools_delta') return [] + const addedNames = findObjectFieldValue(source, attachmentBounds.start, attachmentBounds.end, 'addedNames') + if (!addedNames || addedNames.kind !== 'array') return [] + const names: string[] = [] + let i = addedNames.start + 1 + while (i < addedNames.end - 1 && names.length < MAX_ADDED_NAMES) { + while (i < addedNames.end && /\s/.test(source[i]!)) i++ + if (source.charCodeAt(i) === 0x2c) { + i++ + continue + } + if (source.charCodeAt(i) !== 0x22) { + i++ + continue + } + const end = findJsonStringEnd(source, i, addedNames.end) + if (end === -1) break + const name = readJsonString(source, { start: i, end: end + 1, kind: 'string' }, 500) + if (name) names.push(name) + i = end + 1 + } + return names +} + +function parseLargeJsonlLine(line: string): JournalEntry | null { + const rootEnd = findJsonContainerEnd(line, 0, 0x7b, 0x7d) + if (rootEnd === -1) return null + const rootStart = 0 + const rootLimit = rootEnd + 1 + const type = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'type')) + if (!type) return null + + const entry: JournalEntry = { type } + const timestamp = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'timestamp')) + const sessionId = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'sessionId')) + const cwd = readJsonString(line, findObjectFieldValue(line, rootStart, rootLimit, 'cwd')) + if (timestamp !== undefined) entry.timestamp = timestamp + if (sessionId !== undefined) entry.sessionId = sessionId + if (cwd !== undefined) entry.cwd = cwd + const addedNames = extractLargeAddedNames(line, findObjectFieldValue(line, rootStart, rootLimit, 'attachment')) + if (addedNames.length > 0) { + ;(entry as Record)['attachment'] = { type: 'deferred_tools_delta', addedNames } + } + + if (type === 'user') { + const message = findObjectFieldValue(line, rootStart, rootLimit, 'message') + if (message?.kind === 'object') { + const content = findObjectFieldValue(line, message.start, message.end, 'content') + const text = extractLargeUserText(line, content) + if (text !== undefined) entry.message = { role: 'user', content: text } + } + return entry + } + + if (type !== 'assistant') return entry + const message = findObjectFieldValue(line, rootStart, rootLimit, 'message') + if (message?.kind !== 'object') return entry + const model = readJsonString(line, findObjectFieldValue(line, message.start, message.end, 'model')) + const usageBounds = findObjectFieldValue(line, message.start, message.end, 'usage') + if (!model || usageBounds?.kind !== 'object') return entry + const id = readJsonString(line, findObjectFieldValue(line, message.start, message.end, 'id')) + const contentBounds = findObjectFieldValue(line, message.start, message.end, 'content') + + entry.message = { + type: 'message', + role: 'assistant', + model, + ...(id !== undefined ? { id } : {}), + content: extractLargeToolBlocks(line, contentBounds), + usage: parseLargeUsage(line, usageBounds), + } + + return entry +} + +type BufferJsonValueBounds = { + start: number + end: number + kind: 'string' | 'object' | 'array' | 'scalar' +} + +function isJsonWhitespaceByte(ch: number | undefined): boolean { + return ch === 0x20 || ch === 0x0a || ch === 0x0d || ch === 0x09 +} + +function findJsonStringEndBuffer(source: Buffer, start: number, limit = source.length): number { + for (let i = start + 1; i < limit; i++) { + const ch = source[i] + if (ch === 0x5c) { + i++ + continue + } + if (ch === 0x22) return i + } + return -1 +} + +function findJsonContainerEndBuffer(source: Buffer, start: number, open: number, close: number, limit = source.length): number { + let depth = 0 + let inString = false + for (let i = start; i < limit; i++) { + const ch = source[i] + if (inString) { + if (ch === 0x5c) { + i++ + } else if (ch === 0x22) { + inString = false + } + continue + } + if (ch === 0x22) { + inString = true + } else if (ch === open) { + depth++ + } else if (ch === close) { + depth-- + if (depth === 0) return i + } + } + return -1 +} + +function findJsonValueBoundsBuffer(source: Buffer, start: number, limit = source.length): BufferJsonValueBounds | null { + let i = start + while (i < limit && isJsonWhitespaceByte(source[i])) i++ + if (i >= limit) return null + const ch = source[i] + if (ch === 0x22) { + const end = findJsonStringEndBuffer(source, i, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'string' } + } + if (ch === 0x7b) { + const end = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'object' } + } + if (ch === 0x5b) { + const end = findJsonContainerEndBuffer(source, i, 0x5b, 0x5d, limit) + return end === -1 ? null : { start: i, end: end + 1, kind: 'array' } + } + let end = i + while (end < limit) { + const c = source[end] + if (c === 0x2c || c === 0x7d || c === 0x5d || isJsonWhitespaceByte(c)) break + end++ + } + return { start: i, end, kind: 'scalar' } +} + +function bufferKeyEquals(source: Buffer, keyStart: number, keyEnd: number, field: string): boolean { + if (keyEnd - keyStart !== field.length) return false + return source.subarray(keyStart, keyEnd).equals(Buffer.from(field)) +} + +function findObjectFieldValueBuffer(source: Buffer, objectStart: number, objectEnd: number, field: string): BufferJsonValueBounds | null { + if (source[objectStart] !== 0x7b) return null + let i = objectStart + 1 + while (i < objectEnd - 1) { + while (i < objectEnd && isJsonWhitespaceByte(source[i])) i++ + if (source[i] === 0x2c) { + i++ + continue + } + if (source[i] !== 0x22) { + i++ + continue + } + const keyEnd = findJsonStringEndBuffer(source, i, objectEnd) + if (keyEnd === -1) return null + const keyStart = i + 1 + i = keyEnd + 1 + while (i < objectEnd && isJsonWhitespaceByte(source[i])) i++ + if (source[i] !== 0x3a) continue + const value = findJsonValueBoundsBuffer(source, i + 1, objectEnd) + if (!value) return null + if (bufferKeyEquals(source, keyStart, keyEnd, field)) return value + i = value.end + } + return null +} + +function appendBufferJsonSegment(source: Buffer, start: number, end: number, current: string, cap: number): string { + if (start >= end || current.length >= cap) return current + const remaining = cap - current.length + const cappedEnd = Number.isFinite(cap) ? Math.min(end, start + remaining * 4) : end + return current + source.subarray(start, cappedEnd).toString('utf-8').slice(0, remaining) +} + +function readJsonStringBuffer(source: Buffer, bounds: BufferJsonValueBounds | null, cap = Number.POSITIVE_INFINITY): string | undefined { + if (!bounds || bounds.kind !== 'string') return undefined + let out = '' + let segmentStart = bounds.start + 1 + for (let i = bounds.start + 1; i < bounds.end - 1 && out.length < cap; i++) { + const ch = source[i] + if (ch !== 0x5c) continue + + out = appendBufferJsonSegment(source, segmentStart, i, out, cap) + if (out.length >= cap) break + const next = source[++i] + if (next === undefined) break + if (next === 0x6e) out += '\n' + else if (next === 0x72) out += '\r' + else if (next === 0x74) out += '\t' + else if (next === 0x62) out += '\b' + else if (next === 0x66) out += '\f' + else if (next === 0x75 && i + 4 < bounds.end) { + const hex = source.subarray(i + 1, i + 5).toString('ascii') + const code = Number.parseInt(hex, 16) + if (Number.isFinite(code)) out += String.fromCharCode(code) + i += 4 + } else { + out += String.fromCharCode(next) + } + segmentStart = i + 1 + } + return appendBufferJsonSegment(source, segmentStart, bounds.end - 1, out, cap) +} + +function readJsonNumberFieldBuffer(source: Buffer, objectBounds: BufferJsonValueBounds | null, field: string): number | undefined { + if (!objectBounds || objectBounds.kind !== 'object') return undefined + const bounds = findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, field) + if (!bounds) return undefined + const value = Number(source.subarray(bounds.start, bounds.end).toString('ascii')) + return Number.isFinite(value) ? value : undefined +} + +function parseLargeUsageBuffer(source: Buffer, usageBounds: BufferJsonValueBounds | null) { + const usage: AssistantMessageContent['usage'] = { + input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'input_tokens') ?? 0, + output_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'output_tokens') ?? 0, + cache_creation_input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'cache_creation_input_tokens'), + cache_read_input_tokens: readJsonNumberFieldBuffer(source, usageBounds, 'cache_read_input_tokens'), + } + + if (usageBounds?.kind === 'object') { + const cacheCreation = findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'cache_creation') + const ephemeral5m = readJsonNumberFieldBuffer(source, cacheCreation, 'ephemeral_5m_input_tokens') + const ephemeral1h = readJsonNumberFieldBuffer(source, cacheCreation, 'ephemeral_1h_input_tokens') + if (ephemeral5m !== undefined || ephemeral1h !== undefined) { + ;(usage as AssistantMessageContent['usage']).cache_creation = { + ...(ephemeral5m !== undefined ? { ephemeral_5m_input_tokens: ephemeral5m } : {}), + ...(ephemeral1h !== undefined ? { ephemeral_1h_input_tokens: ephemeral1h } : {}), + } + } + + const serverToolUse = findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'server_tool_use') + const webSearch = readJsonNumberFieldBuffer(source, serverToolUse, 'web_search_requests') + const webFetch = readJsonNumberFieldBuffer(source, serverToolUse, 'web_fetch_requests') + if (webSearch !== undefined || webFetch !== undefined) { + ;(usage as AssistantMessageContent['usage']).server_tool_use = { + ...(webSearch !== undefined ? { web_search_requests: webSearch } : {}), + ...(webFetch !== undefined ? { web_fetch_requests: webFetch } : {}), + } + } + + const speed = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, usageBounds.start, usageBounds.end, 'speed')) + if (speed === 'standard' || speed === 'fast') usage.speed = speed + } + + return usage +} + +function extractLargeToolBlocksBuffer(source: Buffer, contentBounds: BufferJsonValueBounds | null): ToolUseBlock[] { + if (!contentBounds || contentBounds.kind !== 'array') return [] + const tools: ToolUseBlock[] = [] + let i = contentBounds.start + 1 + while (i < contentBounds.end - 1 && tools.length < MAX_TOOL_BLOCKS) { + while (i < contentBounds.end && isJsonWhitespaceByte(source[i])) i++ + if (source[i] === 0x2c) { + i++ + continue + } + if (source[i] !== 0x7b) { + i++ + continue + } + const objectEnd = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, contentBounds.end) + if (objectEnd === -1) break + const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const } + const blockType = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'type')) + if (blockType === 'tool_use') { + const name = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'name')) ?? '' + const id = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'id')) ?? '' + const inputBounds = findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'input') + const input: Record = {} + if (inputBounds?.kind === 'object') { + if (name === 'Skill') { + const skill = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'skill'), 200) + const skillName = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'name'), 200) + if (skill !== undefined) input['skill'] = skill + if (skillName !== undefined) input['name'] = skillName + } else if (name === 'Read' || name === 'FileReadTool') { + const filePath = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'file_path'), BASH_COMMAND_CAP) + if (filePath !== undefined) input['file_path'] = filePath + } else if (name === 'Agent' || name === 'Task') { + const subagentType = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'subagent_type'), 200) + if (subagentType !== undefined) input['subagent_type'] = subagentType + } else if (BASH_TOOLS.has(name)) { + const command = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, inputBounds.start, inputBounds.end, 'command'), BASH_COMMAND_CAP) + if (command !== undefined) input['command'] = command + } + } + tools.push({ type: 'tool_use', id, name, input }) + } + i = objectEnd + 1 + } + return tools +} + +function extractLargeUserTextBuffer(source: Buffer, contentBounds: BufferJsonValueBounds | null): string | undefined { + if (!contentBounds) return undefined + if (contentBounds.kind === 'string') return readJsonStringBuffer(source, contentBounds, USER_TEXT_CAP) + if (contentBounds.kind !== 'array') return undefined + + let text = '' + let i = contentBounds.start + 1 + while (i < contentBounds.end - 1 && text.length < USER_TEXT_CAP) { + while (i < contentBounds.end && isJsonWhitespaceByte(source[i])) i++ + if (source[i] === 0x2c) { + i++ + continue + } + if (source[i] !== 0x7b) { + i++ + continue + } + const objectEnd = findJsonContainerEndBuffer(source, i, 0x7b, 0x7d, contentBounds.end) + if (objectEnd === -1) break + const objectBounds = { start: i, end: objectEnd + 1, kind: 'object' as const } + const type = readJsonStringBuffer(source, findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'type')) + if (type === 'text' || type === 'input_text') { + const part = readJsonStringBuffer( + source, + findObjectFieldValueBuffer(source, objectBounds.start, objectBounds.end, 'text'), + USER_TEXT_CAP - text.length, + ) + if (part) text += (text ? ' ' : '') + part + } + i = objectEnd + 1 + } + return text || undefined +} + +function extractLargeAddedNamesBuffer(source: Buffer, attachmentBounds: BufferJsonValueBounds | null): string[] { + if (!attachmentBounds || attachmentBounds.kind !== 'object') return [] + const attachmentType = readJsonStringBuffer( + source, + findObjectFieldValueBuffer(source, attachmentBounds.start, attachmentBounds.end, 'type'), + ) + if (attachmentType !== 'deferred_tools_delta') return [] + const addedNames = findObjectFieldValueBuffer(source, attachmentBounds.start, attachmentBounds.end, 'addedNames') + if (!addedNames || addedNames.kind !== 'array') return [] + const names: string[] = [] + let i = addedNames.start + 1 + while (i < addedNames.end - 1 && names.length < MAX_ADDED_NAMES) { + while (i < addedNames.end && isJsonWhitespaceByte(source[i])) i++ + if (source[i] === 0x2c) { + i++ + continue + } + if (source[i] !== 0x22) { + i++ + continue + } + const end = findJsonStringEndBuffer(source, i, addedNames.end) + if (end === -1) break + const name = readJsonStringBuffer(source, { start: i, end: end + 1, kind: 'string' }, 500) + if (name) names.push(name) + i = end + 1 + } + return names +} + +function parseLargeJsonlBuffer(line: Buffer): JournalEntry | null { + let rootStart = 0 + while (rootStart < line.length && isJsonWhitespaceByte(line[rootStart])) rootStart++ + if (line[rootStart] !== 0x7b) return null + const rootEnd = findJsonContainerEndBuffer(line, rootStart, 0x7b, 0x7d) + if (rootEnd === -1) return null + const rootLimit = rootEnd + 1 + const type = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'type')) + if (!type) return null + + const entry: JournalEntry = { type } + const timestamp = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'timestamp')) + const sessionId = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'sessionId')) + const cwd = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'cwd')) + if (timestamp !== undefined) entry.timestamp = timestamp + if (sessionId !== undefined) entry.sessionId = sessionId + if (cwd !== undefined) entry.cwd = cwd + const addedNames = extractLargeAddedNamesBuffer(line, findObjectFieldValueBuffer(line, rootStart, rootLimit, 'attachment')) + if (addedNames.length > 0) { + ;(entry as Record)['attachment'] = { type: 'deferred_tools_delta', addedNames } + } + + if (type === 'user') { + const message = findObjectFieldValueBuffer(line, rootStart, rootLimit, 'message') + if (message?.kind === 'object') { + const content = findObjectFieldValueBuffer(line, message.start, message.end, 'content') + const text = extractLargeUserTextBuffer(line, content) + if (text !== undefined) entry.message = { role: 'user', content: text } + } + return entry + } + + if (type !== 'assistant') return entry + const message = findObjectFieldValueBuffer(line, rootStart, rootLimit, 'message') + if (message?.kind !== 'object') return entry + const model = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, message.start, message.end, 'model')) + const usageBounds = findObjectFieldValueBuffer(line, message.start, message.end, 'usage') + if (!model || usageBounds?.kind !== 'object') return entry + const id = readJsonStringBuffer(line, findObjectFieldValueBuffer(line, message.start, message.end, 'id')) + const contentBounds = findObjectFieldValueBuffer(line, message.start, message.end, 'content') + + entry.message = { + type: 'message', + role: 'assistant', + model, + ...(id !== undefined ? { id } : {}), + content: extractLargeToolBlocksBuffer(line, contentBounds), + usage: parseLargeUsageBuffer(line, usageBounds), + } + + return entry +} + +function getTopLevelRawJsonStringField(head: string, field: string): string | null { + let i = 0 + while (i < head.length && /\s/.test(head[i]!)) i++ + if (head.charCodeAt(i) !== 0x7b) return null + i++ + while (i < head.length) { + while (i < head.length && /\s/.test(head[i]!)) i++ + if (head.charCodeAt(i) === 0x2c) { + i++ + continue + } + if (head.charCodeAt(i) === 0x7d) return null + if (head.charCodeAt(i) !== 0x22) return null + const keyEnd = findJsonStringEnd(head, i) + if (keyEnd === -1) return null + const key = head.slice(i + 1, keyEnd) + i = keyEnd + 1 + while (i < head.length && /\s/.test(head[i]!)) i++ + if (head.charCodeAt(i) !== 0x3a) return null + const value = findJsonValueBounds(head, i + 1) + if (!value) return null + if (key === field) return readJsonString(head, value) ?? null + i = value.end + } + return null +} + +export function shouldSkipLine(line: string, threshold: string): boolean { + const head = line.length > RAW_HEAD_BYTES ? line.slice(0, RAW_HEAD_BYTES) : line + const type = getTopLevelRawJsonStringField(head, 'type') + if (type !== 'user' && type !== 'assistant') return false + const ts = getTopLevelRawJsonStringField(head, 'timestamp') + if (!ts || ts.length < 10) return false + return ts < threshold +} + +const USER_TEXT_CAP = 2000 +const BASH_COMMAND_CAP = 2000 +const MAX_TOOL_BLOCKS = 500 +const MAX_ADDED_NAMES = 1000 + +export function compactEntry(raw: JournalEntry): JournalEntry { + const entry: JournalEntry = { type: raw.type } + + if (raw.timestamp !== undefined) entry.timestamp = raw.timestamp + if (raw.sessionId !== undefined) entry.sessionId = raw.sessionId + if (raw.cwd !== undefined) entry.cwd = raw.cwd + + const att = (raw as Record)['attachment'] + if (att && typeof att === 'object') { + const a = att as Record + if (a['type'] === 'deferred_tools_delta' && Array.isArray(a['addedNames'])) { + const names: string[] = [] + for (let i = 0; i < Math.min(a['addedNames'].length, MAX_ADDED_NAMES); i++) { + const n = a['addedNames'][i] + if (typeof n === 'string') names.push(n) + } + ;(entry as Record)['attachment'] = { type: 'deferred_tools_delta', addedNames: names } + } + } + + if (!raw.message) return entry + + if (raw.message.role === 'user') { + const content = raw.message.content + if (typeof content === 'string') { + entry.message = { role: 'user', content: content.slice(0, USER_TEXT_CAP) } + } else if (Array.isArray(content)) { + let remaining = USER_TEXT_CAP + const blocks: { type: 'text'; text: string }[] = [] + for (const b of content) { + if (remaining <= 0) break + if (!b || typeof b !== 'object' || b.type !== 'text') continue + const text = (b as { text?: unknown }).text + if (typeof text !== 'string') continue + const sliced = text.slice(0, remaining) + blocks.push({ type: 'text', text: sliced }) + remaining -= sliced.length + } + entry.message = { role: 'user', content: blocks } + } + return entry + } + + const msg = raw.message as AssistantMessageContent + if (!msg.usage || !msg.model) return entry + + const rawContent = msg.content + const contentArr = Array.isArray(rawContent) ? rawContent : [] + const toolBlocks = contentArr.filter((b): b is ToolUseBlock => b != null && typeof b === 'object' && b.type === 'tool_use') + const compactContent: ContentBlock[] = toolBlocks.slice(0, MAX_TOOL_BLOCKS).map(tb => { + let input: Record = {} + if (tb.name === 'Skill') { + const ri = (tb.input ?? {}) as Record + if (typeof ri['skill'] === 'string') input['skill'] = (ri['skill'] as string).slice(0, 200) + if (typeof ri['name'] === 'string') input['name'] = (ri['name'] as string).slice(0, 200) + } else if (tb.name === 'Read' || tb.name === 'FileReadTool') { + const ri = (tb.input ?? {}) as Record + if (typeof ri['file_path'] === 'string') input['file_path'] = (ri['file_path'] as string).slice(0, BASH_COMMAND_CAP) + } else if (tb.name === 'Agent' || tb.name === 'Task') { + const ri = (tb.input ?? {}) as Record + if (typeof ri['subagent_type'] === 'string') input['subagent_type'] = (ri['subagent_type'] as string).slice(0, 200) + } else if (BASH_TOOLS.has(tb.name)) { + const ri = (tb.input ?? {}) as Record + if (typeof ri['command'] === 'string') { + input['command'] = (ri['command'] as string).slice(0, BASH_COMMAND_CAP) + } + } + return { type: 'tool_use' as const, id: tb.id ?? '', name: tb.name, input } + }) + + const u = msg.usage + const compactUsage: AssistantMessageContent['usage'] = { + input_tokens: u.input_tokens, + output_tokens: u.output_tokens, + } + if (u.cache_creation_input_tokens) compactUsage.cache_creation_input_tokens = u.cache_creation_input_tokens + if (u.cache_creation) { + compactUsage.cache_creation = { + ...(u.cache_creation.ephemeral_5m_input_tokens ? { ephemeral_5m_input_tokens: u.cache_creation.ephemeral_5m_input_tokens } : {}), + ...(u.cache_creation.ephemeral_1h_input_tokens ? { ephemeral_1h_input_tokens: u.cache_creation.ephemeral_1h_input_tokens } : {}), + } + } + if (u.cache_read_input_tokens) compactUsage.cache_read_input_tokens = u.cache_read_input_tokens + if (u.server_tool_use) { + compactUsage.server_tool_use = { + ...(u.server_tool_use.web_search_requests ? { web_search_requests: u.server_tool_use.web_search_requests } : {}), + ...(u.server_tool_use.web_fetch_requests ? { web_fetch_requests: u.server_tool_use.web_fetch_requests } : {}), + } + } + if (u.speed) compactUsage.speed = u.speed + + entry.message = { + type: 'message', + role: 'assistant', + model: msg.model, + usage: compactUsage, + content: compactContent, + ...(msg.id ? { id: msg.id } : {}), + } + + return entry +} + function extractToolNames(content: ContentBlock[]): string[] { return content .filter((b): b is ToolUseBlock => b.type === 'tool_use') @@ -92,16 +940,39 @@ function getMessageId(entry: JournalEntry): string | null { return msg?.id ?? null } +function positiveNumber(n: number | undefined): number { + return n !== undefined && Number.isFinite(n) && n > 0 ? n : 0 +} + +function extractClaudeCacheCreation(usage: AssistantMessageContent['usage']): { totalTokens: number; oneHourTokens: number } { + const legacyTotal = positiveNumber(usage.cache_creation_input_tokens) + const cacheCreation = usage.cache_creation + const fiveMinuteTokens = positiveNumber(cacheCreation?.ephemeral_5m_input_tokens) + const oneHourTokens = positiveNumber(cacheCreation?.ephemeral_1h_input_tokens) + const splitTotal = fiveMinuteTokens + oneHourTokens + + if (splitTotal === 0) return { totalTokens: legacyTotal, oneHourTokens: 0 } + + // Valid Claude usage reports the legacy total and split total as equal. + // Keep the larger value so malformed partial splits do not drop tokens. + const totalTokens = Math.max(legacyTotal, splitTotal) + return { + totalTokens, + oneHourTokens: Math.min(oneHourTokens, totalTokens), + } +} + function parseApiCall(entry: JournalEntry): ParsedApiCall | null { if (entry.type !== 'assistant') return null const msg = entry.message as AssistantMessageContent | undefined if (!msg?.usage || !msg?.model) return null const usage = msg.usage + const cacheCreation = extractClaudeCacheCreation(usage) const tokens: TokenUsage = { inputTokens: usage.input_tokens ?? 0, outputTokens: usage.output_tokens ?? 0, - cacheCreationInputTokens: usage.cache_creation_input_tokens ?? 0, + cacheCreationInputTokens: cacheCreation.totalTokens, cacheReadInputTokens: usage.cache_read_input_tokens ?? 0, cachedInputTokens: 0, reasoningTokens: 0, @@ -118,6 +989,7 @@ function parseApiCall(entry: JournalEntry): ParsedApiCall | null { tokens.cacheReadInputTokens, tokens.webSearchRequests, usage.speed ?? 'standard', + cacheCreation.oneHourTokens, ) const bashCmds = extractBashCommandsFromContent(msg.content ?? []) @@ -136,6 +1008,7 @@ function parseApiCall(entry: JournalEntry): ParsedApiCall | null { timestamp: entry.timestamp ?? '', bashCommands: bashCmds, deduplicationKey: msg.id ?? `claude:${entry.timestamp}`, + cacheCreationOneHourTokens: cacheCreation.oneHourTokens || undefined, } } @@ -391,10 +1264,21 @@ async function parseSessionFile( const entries: JournalEntry[] = [] let hasLines = false - for await (const line of readSessionLines(filePath)) { + // When a dateRange is given, skip user/assistant lines whose timestamp + // is older than range.start - 24h without calling JSON.parse. Huge lines + // that cannot be skipped are yielded as Buffers and compact-parsed without + // converting the whole line into a V8 string. + const earlySkipThreshold = dateRange + ? new Date(dateRange.start.getTime() - 86_400_000).toISOString() + : null + const skipFn = earlySkipThreshold + ? (head: string) => shouldSkipLine(head, earlySkipThreshold) + : undefined + + for await (const line of readSessionLines(filePath, skipFn, { largeLineAsBuffer: true })) { hasLines = true const entry = parseJsonlLine(line) - if (entry) entries.push(entry) + if (entry) entries.push(compactEntry(entry)) } if (!hasLines) return null @@ -451,31 +1335,115 @@ async function collectJsonlFiles(dirPath: string): Promise { return jsonlFiles } -async function scanProjectDirs(dirs: Array<{ path: string; name: string }>, seenMsgIds: Set, dateRange?: DateRange): Promise { - const projectMap = new Map() +async function scanProjectDirs( + dirs: Array<{ path: string; name: string }>, + seenMsgIds: Set, + diskCache: SessionCache, + dateRange?: DateRange, +): Promise { + const section = getOrCreateProviderSection(diskCache, 'claude') + const allDiscoveredFiles = new Set() + + type FileInfo = { dirName: string; fp: NonNullable>> } + const unchangedFiles: Array<{ filePath: string; dirName: string; cached: CachedFile }> = [] + const changedFiles: Array<{ filePath: string; info: FileInfo }> = [] for (const { path: dirPath, name: dirName } of dirs) { const jsonlFiles = await collectJsonlFiles(dirPath) - for (const filePath of jsonlFiles) { - const parsed = await parseSessionFile(filePath, dirName, seenMsgIds, dateRange) - if (parsed && parsed.session.apiCalls > 0) { - const projectPath = parsed.canonicalCwd ?? unsanitizePath(dirName) - const projectKey = parsed.canonicalCwd ? normalizeProjectPathKey(parsed.canonicalCwd) : `slug:${dirName}` - const existing = projectMap.get(projectKey) - if (existing) { - existing.sessions.push(parsed.session) - } else { - projectMap.set(projectKey, { project: dirName, projectPath, sessions: [parsed.session] }) - } + allDiscoveredFiles.add(filePath) + const fp = await fingerprintFile(filePath) + if (!fp) continue + + const action = reconcileFile(fp, section.files[filePath]) + if (action.action === 'unchanged') { + unchangedFiles.push({ filePath, dirName, cached: section.files[filePath]! }) + } else { + changedFiles.push({ filePath, info: { dirName, fp } }) + } + } + } + + // Pre-seed dedup set from cached (unchanged) files + for (const { cached } of unchangedFiles) { + for (const turn of cached.turns) { + for (const call of turn.calls) { + seenMsgIds.add(call.deduplicationKey) + } + } + } + + // Parse changed files, update cache + for (const { filePath, info } of changedFiles) { + // Clear stale entry before parse — if parse fails, file is excluded + delete section.files[filePath] + + const tracker = { lastCompleteLineOffset: 0 } + const entries = await parseClaudeEntries(filePath, tracker) + if (!entries) continue + + const turns = groupIntoTurns(dedupeStreamingMessageIds(entries), seenMsgIds) + section.files[filePath] = { + fingerprint: info.fp, + lastCompleteLineOffset: tracker.lastCompleteLineOffset, + canonicalCwd: extractCanonicalCwd(entries), + mcpInventory: extractMcpInventory(entries), + turns: turns.map(parsedTurnToCachedTurn), + } + } + + // Remove deleted files from cache + for (const cachedPath of Object.keys(section.files)) { + if (!allDiscoveredFiles.has(cachedPath)) { + delete section.files[cachedPath] + } + } + + // Query-time: derive ProjectSummary[] from all cached turns + const projectMap = new Map() + + const allFiles = [ + ...unchangedFiles.map(f => ({ filePath: f.filePath, dirName: f.dirName })), + ...changedFiles.map(f => ({ filePath: f.filePath, dirName: f.info.dirName })), + ] + + for (const { filePath, dirName } of allFiles) { + const cachedFile = section.files[filePath] + if (!cachedFile || cachedFile.turns.length === 0) continue + + let classifiedTurns = cachedFile.turns.map(cachedTurnToClassified) + + if (dateRange) { + classifiedTurns = classifiedTurns.filter(turn => { + if (turn.assistantCalls.length === 0) return false + const firstCallTs = turn.assistantCalls[0]!.timestamp + if (!firstCallTs) return false + const ts = new Date(firstCallTs) + return ts >= dateRange.start && ts <= dateRange.end + }) + } + + if (classifiedTurns.length === 0) continue + + const sessionId = basename(filePath, '.jsonl') + const projectPath = cachedFile.canonicalCwd ?? unsanitizePath(dirName) + const mcpInv = cachedFile.mcpInventory.length > 0 ? cachedFile.mcpInventory : undefined + const session = buildSessionSummary(sessionId, dirName, classifiedTurns, mcpInv) + + if (session.apiCalls > 0) { + const projectKey = cachedFile.canonicalCwd + ? normalizeProjectPathKey(cachedFile.canonicalCwd) + : `slug:${dirName}` + const existing = projectMap.get(projectKey) + if (existing) { + existing.sessions.push(session) + } else { + projectMap.set(projectKey, { project: dirName, projectPath, sessions: [session] }) } } } - // If a slug has both cwd-keyed and slug-keyed entries (mixed sessions where - // some carry a canonical cwd and some don't), fold the slug-keyed sessions - // into the cwd-keyed entry so the canonical projectPath is preserved - // regardless of file iteration order. + // Fold slug-keyed entries into cwd-keyed entries const cwdKeyByDirName = new Map() for (const [key, entry] of projectMap) { if (!key.startsWith('slug:') && !cwdKeyByDirName.has(entry.project)) { @@ -541,73 +1509,293 @@ function providerCallToTurn(call: ParsedProviderCall): ParsedTurn { } } +// ── Cache Conversion ─────────────────────────────────────────────────── + +function apiCallToCachedCall(call: ParsedApiCall): CachedCall { + return { + provider: call.provider, + model: call.model, + usage: { ...call.usage, cacheCreationOneHourTokens: call.cacheCreationOneHourTokens ?? 0 }, + speed: call.speed, + timestamp: call.timestamp, + tools: call.tools, + bashCommands: call.bashCommands, + skills: call.skills, + deduplicationKey: call.deduplicationKey, + } +} + +function parsedTurnToCachedTurn(turn: ParsedTurn): CachedTurn { + return { + timestamp: turn.timestamp, + sessionId: turn.sessionId, + userMessage: turn.userMessage.slice(0, 2000), + calls: turn.assistantCalls.map(apiCallToCachedCall), + } +} + +function providerCallToCachedTurn(call: ParsedProviderCall): CachedTurn { + return { + timestamp: call.timestamp, + sessionId: call.sessionId, + userMessage: call.userMessage.slice(0, 2000), + calls: [{ + provider: call.provider, + model: call.model, + usage: { + inputTokens: call.inputTokens, + outputTokens: call.outputTokens, + cacheCreationInputTokens: call.cacheCreationInputTokens, + cacheReadInputTokens: call.cacheReadInputTokens, + cachedInputTokens: call.cachedInputTokens, + reasoningTokens: call.reasoningTokens, + webSearchRequests: call.webSearchRequests, + cacheCreationOneHourTokens: 0, + }, + speed: call.speed, + timestamp: call.timestamp, + tools: call.tools, + bashCommands: call.bashCommands, + skills: [], + deduplicationKey: call.deduplicationKey, + project: call.project, + projectPath: call.projectPath, + }], + } +} + +function cachedCallToApiCall(call: CachedCall): ParsedApiCall { + const u = call.usage + const outputForCost = call.provider === 'claude' + ? u.outputTokens + : u.outputTokens + u.reasoningTokens + const costUSD = calculateCost( + call.model, u.inputTokens, outputForCost, + u.cacheCreationInputTokens, u.cacheReadInputTokens, + u.webSearchRequests, call.speed, u.cacheCreationOneHourTokens, + ) + return { + provider: call.provider, + model: call.model, + usage: { + inputTokens: u.inputTokens, + outputTokens: u.outputTokens, + cacheCreationInputTokens: u.cacheCreationInputTokens, + cacheReadInputTokens: u.cacheReadInputTokens, + cachedInputTokens: u.cachedInputTokens, + reasoningTokens: u.reasoningTokens, + webSearchRequests: u.webSearchRequests, + }, + costUSD, + tools: call.tools, + mcpTools: extractMcpTools(call.tools), + skills: call.skills, + hasAgentSpawn: call.tools.includes('Agent'), + hasPlanMode: call.tools.includes('EnterPlanMode'), + speed: call.speed, + timestamp: call.timestamp, + bashCommands: call.bashCommands, + deduplicationKey: call.deduplicationKey, + cacheCreationOneHourTokens: u.cacheCreationOneHourTokens || undefined, + } +} + +function cachedTurnToClassified(turn: CachedTurn): ClassifiedTurn { + const parsed: ParsedTurn = { + userMessage: turn.userMessage, + assistantCalls: turn.calls.map(cachedCallToApiCall), + timestamp: turn.timestamp, + sessionId: turn.sessionId, + } + return classifyTurn(parsed) +} + +// ── Cache-Aware Parsing Helpers ──────────────────────────────────────── + +async function parseClaudeEntries( + filePath: string, + tracker: { lastCompleteLineOffset: number }, +): Promise { + const entries: JournalEntry[] = [] + let hasLines = false + for await (const line of readSessionLines(filePath, undefined, { + largeLineAsBuffer: true, + byteOffsetTracker: tracker, + })) { + hasLines = true + const entry = parseJsonlLine(line) + if (entry) entries.push(compactEntry(entry)) + } + if (!hasLines || entries.length === 0) return null + return entries +} + +function getOrCreateProviderSection(cache: SessionCache, provider: string): ProviderSection { + const envFp = computeEnvFingerprint(provider) + const existing = cache.providers[provider] + if (existing && existing.envFingerprint === envFp) return existing + const section = { envFingerprint: envFp, files: {} } + cache.providers[provider] = section + return section +} + +const warnedProviderReadFailures = new Set() + +function warnProviderReadFailureOnce(providerName: string, err: unknown): void { + const key = `${providerName}:sqlite-busy` + if (warnedProviderReadFailures.has(key)) return + warnedProviderReadFailures.add(key) + if (isSqliteBusyError(err)) { + process.stderr.write( + `codeburn: skipped ${providerName} data because its SQLite database is temporarily locked; will retry on the next refresh.\n` + ) + } +} + async function parseProviderSources( providerName: string, sources: Array<{ path: string; project: string }>, seenKeys: Set, + diskCache: SessionCache, dateRange?: DateRange, ): Promise { const provider = await getProvider(providerName) if (!provider) return [] - const sessionMap = new Map() + const section = getOrCreateProviderSection(diskCache, providerName) + const allDiscoveredFiles = new Set() + + type SourceInfo = { source: { path: string; project: string }; fp: NonNullable>> } + const unchangedSources: Array<{ source: { path: string; project: string }; cached: CachedFile }> = [] + const changedSources: SourceInfo[] = [] + for (const source of sources) { + allDiscoveredFiles.add(source.path) + const fp = await fingerprintFile(source.path) + if (!fp) continue + + const action = reconcileFile(fp, section.files[source.path]) + if (action.action === 'unchanged') { + unchangedSources.push({ source, cached: section.files[source.path]! }) + } else { + changedSources.push({ source, fp }) + } + } + + // Parser dedup: cross-provider keys + cached file keys. + // Separate from seenKeys so parsing doesn't suppress query-time output. + const parserDedup = new Set(seenKeys) + for (const { cached } of unchangedSources) { + for (const turn of cached.turns) { + for (const call of turn.calls) { + parserDedup.add(call.deduplicationKey) + } + } + } + + // Parse changed files, update cache + let didParse = false try { - for (const source of sources) { + for (const { source, fp } of changedSources) { if (dateRange) { - try { - const s = await stat(source.path) - if (s.mtimeMs < dateRange.start.getTime()) continue - } catch { /* fall through; treat unknown stat as "may contain data" */ } + if (fp.mtimeMs < dateRange.start.getTime()) continue } + + // Clear stale entry before parse — if parse fails, file is excluded + delete section.files[source.path] + const parser = provider.createSessionParser( { path: source.path, project: source.project, provider: providerName }, - seenKeys, + parserDedup, ) - for await (const call of parser.parse()) { - if (dateRange) { - if (!call.timestamp) continue - const ts = new Date(call.timestamp) - if (ts < dateRange.start || ts > dateRange.end) continue + try { + const turns: CachedTurn[] = [] + for await (const call of parser.parse()) { + turns.push(providerCallToCachedTurn(call)) } - - const turn = providerCallToTurn(call) - const classified = classifyTurn(turn) - const key = `${providerName}:${call.sessionId}:${source.project}` - - const existing = sessionMap.get(key) - if (existing) { - existing.turns.push(classified) - } else { - sessionMap.set(key, { project: source.project, turns: [classified] }) + section.files[source.path] = { fingerprint: fp, mcpInventory: [], turns } + didParse = true + } catch (err) { + if (isSqliteBusyError(err)) { + warnProviderReadFailureOnce(providerName, err) + continue } + throw err } } } finally { - if (providerName === 'codex') await flushCodexCache() - if (providerName === 'antigravity') { + if (didParse && providerName === 'codex') await flushCodexCache() + if (didParse && providerName === 'antigravity') { const liveIds = new Set(sources.map(s => basename(s.path, '.pb'))) await flushAntigravityCache(liveIds) } } - const projectMap = new Map() - for (const [key, { project, turns }] of sessionMap) { + // Remove deleted files from cache + for (const cachedPath of Object.keys(section.files)) { + if (!allDiscoveredFiles.has(cachedPath)) { + delete section.files[cachedPath] + } + } + + // Query-time: derive SessionSummary from all cached turns. + // Uses seenKeys (shared across providers) for cross-provider dedup. + const sessionMap = new Map() + + for (const source of sources) { + const cachedFile = section.files[source.path] + if (!cachedFile) continue + + for (const turn of cachedFile.turns) { + const hasDup = turn.calls.some(c => seenKeys.has(c.deduplicationKey)) + if (hasDup) continue + + for (const c of turn.calls) seenKeys.add(c.deduplicationKey) + + if (dateRange) { + const callTs = turn.calls[0]?.timestamp + if (!callTs) continue + const ts = new Date(callTs) + if (ts < dateRange.start || ts > dateRange.end) continue + } + + const classified = cachedTurnToClassified(turn) + const project = turn.calls[0]?.project ?? source.project + const key = `${providerName}:${turn.sessionId}:${project}` + + const existing = sessionMap.get(key) + if (existing) { + existing.turns.push(classified) + if (!existing.projectPath && turn.calls[0]?.projectPath) { + existing.projectPath = turn.calls[0]!.projectPath + } + } else { + sessionMap.set(key, { project, projectPath: turn.calls[0]?.projectPath, turns: [classified] }) + } + } + } + + const projectMap = new Map() + for (const [key, { project, projectPath, turns }] of sessionMap) { const sessionId = key.split(':')[1] ?? key const session = buildSessionSummary(sessionId, project, turns) if (session.apiCalls > 0) { - const existing = projectMap.get(project) ?? [] - existing.push(session) - projectMap.set(project, existing) + const existing = projectMap.get(project) + if (existing) { + existing.sessions.push(session) + if (!existing.projectPath && projectPath) existing.projectPath = projectPath + } else { + projectMap.set(project, { projectPath, sessions: [session] }) + } } } const projects: ProjectSummary[] = [] - for (const [dirName, sessions] of projectMap) { + for (const [dirName, { projectPath, sessions }] of projectMap) { projects.push({ project: dirName, - projectPath: unsanitizePath(dirName), + projectPath: projectPath ?? unsanitizePath(dirName), sessions, totalCostUSD: sessions.reduce((s, sess) => s + sess.totalCostUSD, 0), totalApiCalls: sessions.reduce((s, sess) => s + sess.apiCalls, 0), @@ -671,11 +1859,43 @@ export function filterProjectsByName( return result } +function turnIsInDateRange(turn: ClassifiedTurn, dateRange: DateRange): boolean { + if (turn.assistantCalls.length === 0) return false + const firstCallTs = turn.assistantCalls[0]!.timestamp + if (!firstCallTs) return false + const ts = new Date(firstCallTs) + return ts >= dateRange.start && ts <= dateRange.end +} + +export function filterProjectsByDateRange(projects: ProjectSummary[], dateRange: DateRange): ProjectSummary[] { + const filtered: ProjectSummary[] = [] + for (const project of projects) { + const sessions: SessionSummary[] = [] + for (const session of project.sessions) { + const turns = session.turns.filter(turn => turnIsInDateRange(turn, dateRange)) + if (turns.length === 0) continue + sessions.push(buildSessionSummary(session.sessionId, session.project, turns, session.mcpInventory)) + } + if (sessions.length === 0) continue + filtered.push({ + project: project.project, + projectPath: project.projectPath, + sessions, + totalCostUSD: sessions.reduce((s, sess) => s + sess.totalCostUSD, 0), + totalApiCalls: sessions.reduce((s, sess) => s + sess.apiCalls, 0), + }) + } + return filtered.sort((a, b) => b.totalCostUSD - a.totalCostUSD) +} + export async function parseAllSessions(dateRange?: DateRange, providerFilter?: string): Promise { const key = cacheKey(dateRange, providerFilter) const cached = sessionCache.get(key) if (cached && Date.now() - cached.ts < CACHE_TTL_MS) return cached.data + const diskCache = await loadCache() + await cleanupOrphanedTempFiles() + const seenMsgIds = new Set() const seenKeys = new Set() const allSources = await discoverAllSessions(providerFilter) @@ -684,7 +1904,7 @@ export async function parseAllSessions(dateRange?: DateRange, providerFilter?: s const nonClaudeSources = allSources.filter(s => s.provider !== 'claude') const claudeDirs = claudeSources.map(s => ({ path: s.path, name: s.project })) - const claudeProjects = await scanProjectDirs(claudeDirs, seenMsgIds, dateRange) + const claudeProjects = await scanProjectDirs(claudeDirs, seenMsgIds, diskCache, dateRange) const providerGroups = new Map>() for (const source of nonClaudeSources) { @@ -695,10 +1915,12 @@ export async function parseAllSessions(dateRange?: DateRange, providerFilter?: s const otherProjects: ProjectSummary[] = [] for (const [providerName, sources] of providerGroups) { - const projects = await parseProviderSources(providerName, sources, seenKeys, dateRange) + const projects = await parseProviderSources(providerName, sources, seenKeys, diskCache, dateRange) otherProjects.push(...projects) } + try { await saveCache(diskCache) } catch {} + const mergedMap = new Map() for (const p of [...claudeProjects, ...otherProjects]) { const existing = mergedMap.get(p.project) diff --git a/src/plan-usage.ts b/src/plan-usage.ts index 78117343..648d3c91 100644 --- a/src/plan-usage.ts +++ b/src/plan-usage.ts @@ -1,5 +1,6 @@ -import { readPlan, type Plan } from './config.js' +import { readPlans, type Plan, type PlanMap } from './config.js' import { parseAllSessions } from './parser.js' +import { PLAN_PROVIDERS } from './plans.js' import type { DateRange, ProjectSummary } from './types.js' const MS_PER_DAY = 24 * 60 * 60 * 1000 @@ -79,13 +80,15 @@ export function projectMonthEnd( for (const project of projects) { for (const session of project.sessions) { for (const turn of session.turns) { - if (!turn.timestamp) continue - const ts = new Date(turn.timestamp) - if (Number.isNaN(ts.getTime())) continue - if (ts < periodStart || ts > today) continue - const dayKey = toLocalDateKey(ts) - const turnCost = turn.assistantCalls.reduce((sum, call) => sum + call.costUSD, 0) - dayCosts.set(dayKey, (dayCosts.get(dayKey) ?? 0) + turnCost) + for (const call of turn.assistantCalls) { + const timestamp = call.timestamp || turn.timestamp + if (!timestamp) continue + const ts = new Date(timestamp) + if (Number.isNaN(ts.getTime())) continue + if (ts < periodStart || ts > today) continue + const dayKey = toLocalDateKey(ts) + dayCosts.set(dayKey, (dayCosts.get(dayKey) ?? 0) + call.costUSD) + } } } } @@ -126,21 +129,84 @@ export function getPlanUsageFromProjects(plan: Plan, projects: ProjectSummary[], } } +function getPlanScopedProjects(plan: Plan, projects: ProjectSummary[], today: Date): ProjectSummary[] { + const { periodStart } = computePeriodFromResetDay(plan.resetDay, today) + const provider = plan.provider + + // These scoped clones are consumed only by plan usage math; cost/call rollups + // are recomputed below, while unrelated breakdown fields remain unchanged. + return projects + .map(project => { + const sessions = project.sessions + .map(session => { + const turns = session.turns + .map(turn => { + const assistantCalls = turn.assistantCalls.filter(call => { + if (provider !== 'all' && call.provider !== provider) return false + const timestamp = call.timestamp || turn.timestamp + if (!timestamp) return false + const ts = new Date(timestamp) + return !Number.isNaN(ts.getTime()) && ts >= periodStart && ts <= today + }) + return assistantCalls.length > 0 ? { ...turn, assistantCalls } : null + }) + .filter((turn): turn is NonNullable => turn !== null) + + const totalCostUSD = turns.reduce( + (sum, turn) => sum + turn.assistantCalls.reduce((turnSum, call) => turnSum + call.costUSD, 0), + 0, + ) + const apiCalls = turns.reduce((sum, turn) => sum + turn.assistantCalls.length, 0) + return apiCalls > 0 ? { ...session, turns, totalCostUSD, apiCalls } : null + }) + .filter((session): session is NonNullable => session !== null) + + const totalCostUSD = sessions.reduce((sum, session) => sum + session.totalCostUSD, 0) + const totalApiCalls = sessions.reduce((sum, session) => sum + session.apiCalls, 0) + return totalApiCalls > 0 ? { ...project, sessions, totalCostUSD, totalApiCalls } : null + }) + .filter((project): project is NonNullable => project !== null) +} + export async function getPlanUsage(plan: Plan, today = new Date()): Promise { const { periodStart } = computePeriodFromResetDay(plan.resetDay, today) const range: DateRange = { start: periodStart, end: today, } - const provider = plan.provider === 'all' ? 'all' : plan.provider - const projects = await parseAllSessions(range, provider) + const projects = await parseAllSessions(range, plan.provider) return getPlanUsageFromProjects(plan, projects, today) } export async function getPlanUsageOrNull(today = new Date()): Promise { - const plan = await readPlan() - if (!isActivePlan(plan)) return null - return getPlanUsage(plan, today) + return (await getPlanUsages(today))[0] ?? null +} + +export function activePlansFromMap(plans: PlanMap): Plan[] { + return PLAN_PROVIDERS + .map(provider => plans[provider]) + .filter(isActivePlan) +} + +export async function getPlanUsages(today = new Date()): Promise { + const plans = activePlansFromMap(await readPlans()) + if (plans.length === 0) return [] + + const starts = plans.map(plan => computePeriodFromResetDay(plan.resetDay, today).periodStart.getTime()) + const range: DateRange = { + start: new Date(Math.min(...starts)), + end: today, + } + + if (plans.length === 1) { + const plan = plans[0]! + const projects = await parseAllSessions(range, plan.provider) + return [getPlanUsageFromProjects(plan, projects, today)] + } + + const projects = await parseAllSessions(range, 'all') + + return plans.map(plan => getPlanUsageFromProjects(plan, getPlanScopedProjects(plan, projects, today), today)) } export function isActivePlan(plan: Plan | undefined): plan is Plan { diff --git a/src/providers/antigravity.ts b/src/providers/antigravity.ts index 3f9667e4..95f96c92 100644 --- a/src/providers/antigravity.ts +++ b/src/providers/antigravity.ts @@ -14,7 +14,7 @@ const CACHE_VERSION = 2 const RPC_TIMEOUT_MS = 5000 const MAX_RESPONSE_BYTES = 16 * 1024 * 1024 -type ServerInfo = { +export type ServerInfo = { port: number csrfToken: string } @@ -31,7 +31,7 @@ type UsageEntry = { responseId?: string } -type GeneratorMetadata = { +export type GeneratorMetadata = { stepIndices?: number[] chatModel?: { model: string @@ -42,6 +42,20 @@ type GeneratorMetadata = { } } +type ModelMapResponse = { + models?: Record + response?: { + models?: Record + } +} + +type GeneratorMetadataResponse = { + generatorMetadata?: GeneratorMetadata[] + response?: { + generatorMetadata?: GeneratorMetadata[] + } +} + type CachedCascade = { mtimeMs: number sizeBytes: number @@ -59,6 +73,9 @@ let memCache: AntigravityCache | null = null let cacheDirty = false let httpsAgent: https.Agent | undefined +const SERVER_PORT_FLAGS = ['https_server_port', 'extension_server_port'] +const CSRF_TOKEN_FLAGS = ['csrf_token', 'extension_server_csrf_token'] + function getAgent(): https.Agent { if (!httpsAgent) httpsAgent = new https.Agent({ rejectUnauthorized: false }) return httpsAgent @@ -72,6 +89,72 @@ function getCachePath(): string { return join(getCacheDir(), 'antigravity-results.json') } +function execFileText(command: string, args: string[], timeout = 3000): Promise { + return new Promise((resolve, reject) => { + execFile(command, args, { encoding: 'utf-8', timeout, maxBuffer: 1024 * 1024 }, (err, stdout) => { + if (err) reject(err) + else resolve(stdout) + }) + }) +} + +function getFlagValue(line: string, names: string[]): string | null { + for (const name of names) { + const match = line.match(new RegExp(`--${name}(?:=|\\s+)(?:"([^"]+)"|'([^']+)'|([^\\s]+))`, 'i')) + const value = match?.[1] ?? match?.[2] ?? match?.[3] + if (value && !value.startsWith('--')) return value + } + return null +} + +function isLikelyCsrfToken(value: string): boolean { + return value.length >= 16 && /^[A-Za-z0-9._~:/+=-]+$/.test(value) +} + +export function parseAntigravityServerInfoFromLine(line: string): ServerInfo | null { + const lower = line.toLowerCase() + if (!lower.includes('language_server') || !lower.includes('antigravity')) return null + + const rawPort = getFlagValue(line, SERVER_PORT_FLAGS) + const csrfToken = getFlagValue(line, CSRF_TOKEN_FLAGS) + if (!rawPort || !csrfToken) return null + if (!isLikelyCsrfToken(csrfToken)) return null + + const port = Number(rawPort) + if (!Number.isInteger(port) || port <= 0 || port > 65535) return null + + return { port, csrfToken } +} + +export function parseAntigravityServerInfo(lines: string[]): ServerInfo | null { + for (const line of lines) { + const server = parseAntigravityServerInfoFromLine(line) + if (server) return server + } + return null +} + +export function extractAntigravityModelMap(resp: unknown): ModelMap { + if (!resp || typeof resp !== 'object') return {} + const data = resp as ModelMapResponse + const models = data.response?.models ?? data.models + const map: ModelMap = {} + if (!models) return map + for (const [key, info] of Object.entries(models)) { + if (info && typeof info === 'object' && typeof info.model === 'string') { + map[info.model] = key + } + } + return map +} + +export function extractAntigravityGeneratorMetadata(resp: unknown): GeneratorMetadata[] { + if (!resp || typeof resp !== 'object') return [] + const data = resp as GeneratorMetadataResponse + const metadata = data.response?.generatorMetadata ?? data.generatorMetadata + return Array.isArray(metadata) ? metadata : [] +} + async function loadCache(): Promise { if (memCache) return memCache try { @@ -124,27 +207,27 @@ async function flushCache(liveCascadeIds?: Set): Promise { } catch { /* best-effort */ } } +async function readProcessCommandLines(): Promise { + if (process.platform === 'win32') { + const script = [ + "$ErrorActionPreference = 'SilentlyContinue'", + '[Console]::OutputEncoding = [System.Text.Encoding]::UTF8', + "Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -and $_.CommandLine -like '*language_server*' -and $_.CommandLine -like '*antigravity*' } | ForEach-Object { $_.CommandLine }", + ].join('; ') + const output = await execFileText('powershell.exe', ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-Command', script], 5000) + return output.split(/\r?\n/) + } + + const output = await execFileText('ps', ['-ww', '-eo', 'args']) + return output.split('\n') +} + async function detectServer(): Promise { if (cachedServer !== undefined) return cachedServer try { - const output = await new Promise((resolve, reject) => { - execFile('ps', ['-eo', 'args'], { encoding: 'utf-8', timeout: 3000 }, (err, stdout) => { - if (err) reject(err) - else resolve(stdout) - }) - }) - for (const line of output.split('\n')) { - if (!line.includes('language_server') || !line.includes('antigravity')) continue - if (!line.includes('--https_server_port')) continue - - const csrfMatch = line.match(/--csrf_token\s+([0-9a-f-]{32,})/) - const portMatch = line.match(/--https_server_port\s+(\d+)/) - if (csrfMatch && portMatch) { - cachedServer = { csrfToken: csrfMatch[1]!, port: parseInt(portMatch[1]!, 10) } - return cachedServer - } - } - } catch { /* ps failed or timed out */ } + cachedServer = parseAntigravityServerInfo(await readProcessCommandLines()) + return cachedServer + } catch { /* process discovery failed or timed out */ } cachedServer = null return null } @@ -199,20 +282,12 @@ async function rpc(server: ServerInfo, method: string, body: Record { if (cachedModelMap) return cachedModelMap - const map: ModelMap = {} try { - const resp = await rpc(server, 'GetAvailableModels') as { - response?: { models?: Record } - } - const models = resp?.response?.models - if (models) { - for (const [key, info] of Object.entries(models)) { - if (info.model) map[info.model] = key - } - } + cachedModelMap = extractAntigravityModelMap(await rpc(server, 'GetAvailableModels')) + return cachedModelMap } catch { /* best-effort */ } - cachedModelMap = map - return map + cachedModelMap = {} + return cachedModelMap } // Strip Antigravity-specific suffixes so the pricing DB can match @@ -275,10 +350,9 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars let metadata: GeneratorMetadata[] try { - const resp = await rpc(server, 'GetCascadeTrajectoryGeneratorMetadata', { cascadeId }) as { - generatorMetadata?: GeneratorMetadata[] - } - metadata = resp?.generatorMetadata ?? [] + metadata = extractAntigravityGeneratorMetadata( + await rpc(server, 'GetCascadeTrajectoryGeneratorMetadata', { cascadeId }), + ) } catch { if (cached) { for (const call of cached.calls) { diff --git a/src/providers/cline.ts b/src/providers/cline.ts new file mode 100644 index 00000000..73177064 --- /dev/null +++ b/src/providers/cline.ts @@ -0,0 +1,73 @@ +import { stat } from 'fs/promises' +import { homedir } from 'os' +import { basename, join } from 'path' + +import { discoverClineTasks, createClineParser, getVSCodeGlobalStoragePath } from './vscode-cline-parser.js' +import type { Provider, SessionSource, SessionParser } from './types.js' + +const EXTENSION_ID = 'saoudrizwan.claude-dev' + +export function getClineDataPath(): string { + return join(homedir(), '.cline', 'data') +} + +function normalizeOverrideDirs(overrideDirs?: string | string[]): string[] | undefined { + if (overrideDirs === undefined) return undefined + // Cline has two default roots, so tests and future callers can override one or both. + return Array.isArray(overrideDirs) ? overrideDirs : [overrideDirs] +} + +async function dedupeTaskSources(sources: SessionSource[]): Promise { + const candidates = await Promise.all(sources.map(async source => ({ + source, + mtimeMs: (await stat(join(source.path, 'ui_messages.json')).catch(() => null))?.mtimeMs ?? 0, + }))) + + const seenTaskIds = new Set() + const deduped: SessionSource[] = [] + + for (const { source } of candidates.sort((a, b) => b.mtimeMs - a.mtimeMs)) { + const taskId = basename(source.path) + if (seenTaskIds.has(taskId)) continue + seenTaskIds.add(taskId) + deduped.push(source) + } + + return deduped +} + +export function createClineProvider(overrideDirs?: string | string[]): Provider { + const configuredDirs = normalizeOverrideDirs(overrideDirs) + + return { + name: 'cline', + displayName: 'Cline', + + modelDisplayName(model: string): string { + return model + }, + + toolDisplayName(rawTool: string): string { + return rawTool + }, + + async discoverSessions(): Promise { + const baseDirs = configuredDirs ?? [ + getVSCodeGlobalStoragePath(EXTENSION_ID), + getClineDataPath(), + ] + + const sources = await Promise.all( + baseDirs.map(dir => discoverClineTasks(EXTENSION_ID, 'cline', 'Cline', dir)), + ) + + return dedupeTaskSources(sources.flat()) + }, + + createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { + return createClineParser(source, seenKeys, 'cline') + }, + } +} + +export const cline = createClineProvider() diff --git a/src/providers/codebuff.ts b/src/providers/codebuff.ts new file mode 100644 index 00000000..6ee746c1 --- /dev/null +++ b/src/providers/codebuff.ts @@ -0,0 +1,460 @@ +import { readdir, readFile, stat } from 'fs/promises' +import { basename, dirname, join } from 'path' +import { homedir } from 'os' + +import { calculateCost } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' +import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' + +// Codebuff (formerly Manicode) uses a credit-based billing system. The local +// chat-messages.json doesn't record per-call token counts the way Claude Code +// or Codex do -- only `credits` on completed assistant messages. We convert +// credits to USD using Codebuff's retail pay-as-you-go rate so the cost shows +// up in the dashboard even when tokens are absent. The rate intentionally +// rounds up to the public PAYG tier ($0.01 / credit) so we never understate +// spend; users on a subscription plan get a conservative upper bound. +const USD_PER_CREDIT = 0.01 + +// Codebuff's chat history lives under `~/.config/manicode/` (the legacy +// product name is still on disk). Development and staging channels use +// `manicode-dev` and `manicode-staging` -- we walk all three when present. +const CHANNELS = ['manicode', 'manicode-dev', 'manicode-staging'] as const + +const modelDisplayNames: Record = { + codebuff: 'Codebuff', + 'codebuff-base': 'Codebuff Base', + 'codebuff-base2': 'Codebuff Base 2', + 'codebuff-lite': 'Codebuff Lite', + 'codebuff-max': 'Codebuff Max', +} + +// Codebuff's native tool names map to codeburn's canonical tool set so +// classifier heuristics (edit/read/bash/etc.) behave consistently with the +// other providers. +const toolNameMap: Record = { + read_files: 'Read', + read_file: 'Read', + code_search: 'Grep', + glob: 'Glob', + find_files: 'Glob', + str_replace: 'Edit', + edit_file: 'Edit', + write_file: 'Write', + run_terminal_command: 'Bash', + terminal: 'Bash', + spawn_agents: 'Agent', + spawn_agent: 'Agent', + write_todos: 'TodoWrite', + create_plan: 'TodoWrite', + browser_logs: 'WebFetch', + web_search: 'WebSearch', + fetch_url: 'WebFetch', +} + +// Tool names we ignore for classification -- they're not useful signals for +// distinguishing "coding" vs "exploration" vs "planning" work. +const IGNORED_TOOLS = new Set(['suggest_followups', 'end_turn']) + +type CodebuffUsage = { + inputTokens?: number + input_tokens?: number + promptTokens?: number + prompt_tokens?: number + outputTokens?: number + output_tokens?: number + completionTokens?: number + completion_tokens?: number + cacheCreationInputTokens?: number + cache_creation_input_tokens?: number + cacheReadInputTokens?: number + cache_read_input_tokens?: number + promptTokensDetails?: { cachedTokens?: number } + prompt_tokens_details?: { cached_tokens?: number } +} + +type CodebuffBlock = { + type?: string + content?: string + toolName?: string + input?: Record + output?: string + agentName?: string + agentType?: string + status?: string + blocks?: CodebuffBlock[] +} + +type CodebuffHistoryMessage = { + role?: string + providerOptions?: { + codebuff?: { model?: string; usage?: CodebuffUsage } + usage?: CodebuffUsage + } +} + +type CodebuffMetadata = { + model?: string + modelId?: string + timestamp?: string | number + usage?: CodebuffUsage + codebuff?: { model?: string; usage?: CodebuffUsage } + runState?: { + cwd?: string + sessionState?: { + cwd?: string + projectContext?: { cwd?: string } + fileContext?: { cwd?: string } + mainAgentState?: { + agentType?: string + messageHistory?: CodebuffHistoryMessage[] + } + } + } +} + +type CodebuffChatMessage = { + id?: string + variant?: string + role?: string + content?: string + timestamp?: string | number + credits?: number + blocks?: CodebuffBlock[] + metadata?: CodebuffMetadata +} + +function getCodebuffBaseDir(override?: string): string { + if (override && override.trim()) return override + const envPath = process.env['CODEBUFF_DATA_DIR'] + if (envPath && envPath.trim()) return envPath + return join(homedir(), '.config', 'manicode') +} + +function pickNumber(...vals: Array): number | undefined { + for (const v of vals) { + if (typeof v === 'number' && Number.isFinite(v)) return v + } + return undefined +} + +function normalizeUsage(u: CodebuffUsage | undefined): { + input: number + output: number + cacheRead: number + cacheWrite: number +} { + if (!u) return { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } + return { + input: pickNumber(u.inputTokens, u.input_tokens, u.promptTokens, u.prompt_tokens) ?? 0, + output: pickNumber(u.outputTokens, u.output_tokens, u.completionTokens, u.completion_tokens) ?? 0, + cacheRead: + pickNumber( + u.cacheReadInputTokens, + u.cache_read_input_tokens, + u.promptTokensDetails?.cachedTokens, + u.prompt_tokens_details?.cached_tokens, + ) ?? 0, + cacheWrite: pickNumber(u.cacheCreationInputTokens, u.cache_creation_input_tokens) ?? 0, + } +} + +function coerceTimestamp(value: string | number | undefined): string { + if (value == null) return '' + if (typeof value === 'number') { + return Number.isFinite(value) ? new Date(value).toISOString() : '' + } + const parsed = Date.parse(value) + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : value +} + +function parseChatIdToIso(chatId: string): string { + const iso = chatId.replace(/(\d{4}-\d{2}-\d{2}T\d{2})-(\d{2})-(\d{2})/, '$1:$2:$3') + const parsed = Date.parse(iso) + return Number.isFinite(parsed) ? new Date(parsed).toISOString() : '' +} + +function extractCwd(meta: CodebuffMetadata | undefined): string | null { + const rs = meta?.runState + if (!rs) return null + return ( + rs.sessionState?.projectContext?.cwd ?? + rs.sessionState?.fileContext?.cwd ?? + rs.sessionState?.cwd ?? + rs.cwd ?? + null + ) +} + +function extractAgentType(meta: CodebuffMetadata | undefined): string | null { + return meta?.runState?.sessionState?.mainAgentState?.agentType ?? null +} + +function collectBlockTools(blocks: CodebuffBlock[] | undefined, acc: { tools: string[]; bash: string[] }): void { + if (!Array.isArray(blocks)) return + for (const block of blocks) { + if (!block || typeof block !== 'object') continue + if (block.type === 'tool' && typeof block.toolName === 'string') { + const raw = block.toolName + if (!IGNORED_TOOLS.has(raw)) { + acc.tools.push(toolNameMap[raw] ?? raw) + } + if ((raw === 'run_terminal_command' || raw === 'terminal') && block.input) { + const cmd = block.input['command'] + if (typeof cmd === 'string') { + acc.bash.push(...extractBashCommands(cmd)) + } + } + } + if (block.type === 'agent' && Array.isArray(block.blocks)) { + collectBlockTools(block.blocks, acc) + } + } +} + +function resolveModel(meta: CodebuffMetadata | undefined, stashedModel: string | null): string { + const direct = meta?.model ?? meta?.modelId ?? meta?.codebuff?.model + if (direct) return direct + if (stashedModel) return stashedModel + const agentType = extractAgentType(meta) + if (agentType) return `codebuff-${agentType}` + return 'codebuff' +} + +function usageFromHistory(meta: CodebuffMetadata | undefined): { + model: string | null + input: number + output: number + cacheRead: number + cacheWrite: number +} { + const hist = meta?.runState?.sessionState?.mainAgentState?.messageHistory + if (!Array.isArray(hist)) return { model: null, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } + for (let i = hist.length - 1; i >= 0; i--) { + const entry = hist[i] + if (!entry || entry.role !== 'assistant' || !entry.providerOptions) continue + const u = normalizeUsage(entry.providerOptions.usage ?? entry.providerOptions.codebuff?.usage) + if (u.input > 0 || u.output > 0 || u.cacheRead > 0 || u.cacheWrite > 0) { + return { model: entry.providerOptions.codebuff?.model ?? null, ...u } + } + } + return { model: null, input: 0, output: 0, cacheRead: 0, cacheWrite: 0 } +} + +async function readJson(filePath: string): Promise { + try { + const raw = await readFile(filePath, 'utf-8') + return JSON.parse(raw) as T + } catch { + return null + } +} + +async function discoverChannel(root: string): Promise { + const sources: SessionSource[] = [] + const projectsDir = join(root, 'projects') + + let projectNames: string[] + try { + projectNames = await readdir(projectsDir) + } catch { + return sources + } + + for (const projectName of projectNames) { + const chatsDir = join(projectsDir, projectName, 'chats') + let chatIds: string[] + try { + chatIds = await readdir(chatsDir) + } catch { + continue + } + + for (const chatId of chatIds) { + const chatDir = join(chatsDir, chatId) + const dirStat = await stat(chatDir).catch(() => null) + if (!dirStat?.isDirectory()) continue + + const messagesPath = join(chatDir, 'chat-messages.json') + const messagesStat = await stat(messagesPath).catch(() => null) + if (!messagesStat?.isFile()) continue + + // Resolve the real cwd from run-state.json so sessions group by the + // originating project directory instead of the sanitized chat folder + // name (which is often the same for many users). + const runState = await readJson( + join(chatDir, 'run-state.json'), + ) + const cwd = extractCwd({ runState: runState ?? undefined }) + const project = cwd ? basename(cwd) : projectName + + sources.push({ path: chatDir, project, provider: 'codebuff' }) + } + } + + return sources +} + +async function discoverSessionsInBase(baseDir: string): Promise { + const results: SessionSource[] = [] + + // Honor an explicit override: walk only the provided directory even if it + // matches one of the channel names literally. + if (process.env['CODEBUFF_DATA_DIR'] || baseDir !== join(homedir(), '.config', 'manicode')) { + const rootStat = await stat(baseDir).catch(() => null) + if (!rootStat?.isDirectory()) return results + results.push(...await discoverChannel(baseDir)) + return results + } + + const configDir = join(homedir(), '.config') + for (const channel of CHANNELS) { + const root = join(configDir, channel) + const rootStat = await stat(root).catch(() => null) + if (!rootStat?.isDirectory()) continue + results.push(...await discoverChannel(root)) + } + return results +} + +// Downstream aggregation groups sessions by `(provider, sessionId, project)` +// (see src/parser.ts). Codebuff chat folders are ISO timestamps, which means +// the same `chatId` can legitimately appear under each channel root +// (`manicode`, `manicode-dev`, `manicode-staging`) and even resolve to the +// same project cwd. To keep those sessions distinct we include the channel +// identity in the sessionId. The channel is derived from the fixed path +// structure Codebuff writes on disk: `/projects//chats/`. +// Returns null when the path doesn't match that shape so the caller can fall +// back to a plain chatId. +// +// We use '/' as the channel/chatId separator rather than ':' because +// src/parser.ts builds its session key as `${provider}:${sessionId}:${project}` +// and reconstructs the sessionId with `key.split(':')[1]` -- any colon inside +// sessionId would get truncated to just the channel name downstream. +function extractChannelFromChatDir(chatDir: string): string | null { + const chatsDir = dirname(chatDir) + if (basename(chatsDir) !== 'chats') return null + const projectDir = dirname(chatsDir) + const projectsDir = dirname(projectDir) + if (basename(projectsDir) !== 'projects') return null + const channel = basename(dirname(projectsDir)) + return channel ? channel : null +} + +function createParser(source: SessionSource, seenKeys: Set): SessionParser { + return { + async *parse(): AsyncGenerator { + const chatDir = source.path + const chatId = basename(chatDir) + const channel = extractChannelFromChatDir(chatDir) + const sessionId = channel ? `${channel}/${chatId}` : chatId + const fallbackTs = parseChatIdToIso(chatId) + + const messages = await readJson( + join(chatDir, 'chat-messages.json'), + ) + if (!Array.isArray(messages)) return + + let pendingUserMessage = '' + + for (const [idx, msg] of messages.entries()) { + if (!msg || typeof msg !== 'object') continue + + const variant = msg.variant ?? msg.role + if (variant === 'user') { + if (typeof msg.content === 'string' && msg.content.length > 0) { + pendingUserMessage = msg.content + } + continue + } + + if (variant !== 'ai' && variant !== 'agent' && variant !== 'assistant') continue + + const credits = typeof msg.credits === 'number' && Number.isFinite(msg.credits) ? msg.credits : 0 + const directUsage = normalizeUsage(msg.metadata?.usage ?? msg.metadata?.codebuff?.usage) + const stashedUsage = usageFromHistory(msg.metadata) + + const hasDirect = + directUsage.input > 0 || + directUsage.output > 0 || + directUsage.cacheRead > 0 || + directUsage.cacheWrite > 0 + const usage = hasDirect ? directUsage : stashedUsage + const stashedModel = stashedUsage.model + + // Skip messages with neither credits nor tokens -- they're typically + // in-progress mode dividers or empty framing blocks. + if (credits === 0 && usage.input === 0 && usage.output === 0 && usage.cacheRead === 0 && usage.cacheWrite === 0) { + continue + } + + const model = resolveModel(msg.metadata, stashedModel) + const timestamp = coerceTimestamp(msg.timestamp ?? msg.metadata?.timestamp) || fallbackTs + + const dedupId = msg.id ?? String(idx) + const dedupKey = `codebuff:${chatDir}:${dedupId}` + if (seenKeys.has(dedupKey)) continue + seenKeys.add(dedupKey) + + const acc = { tools: [] as string[], bash: [] as string[] } + collectBlockTools(msg.blocks, acc) + + // Prefer calculated cost from tokens when available (multi-provider + // models routed through Codebuff still show up in LiteLLM); otherwise + // fall back to the credit-based approximation. + let costUSD = calculateCost(model, usage.input, usage.output, usage.cacheWrite, usage.cacheRead, 0) + if (costUSD === 0 && credits > 0) { + costUSD = credits * USD_PER_CREDIT + } + + yield { + provider: 'codebuff', + model, + inputTokens: usage.input, + outputTokens: usage.output, + cacheCreationInputTokens: usage.cacheWrite, + cacheReadInputTokens: usage.cacheRead, + cachedInputTokens: usage.cacheRead, + reasoningTokens: 0, + webSearchRequests: 0, + costUSD, + tools: acc.tools, + bashCommands: acc.bash, + timestamp, + speed: 'standard', + deduplicationKey: dedupKey, + userMessage: pendingUserMessage, + sessionId, + } + + pendingUserMessage = '' + } + }, + } +} + +export function createCodebuffProvider(baseDir?: string): Provider { + const dir = getCodebuffBaseDir(baseDir) + + return { + name: 'codebuff', + displayName: 'Codebuff', + + modelDisplayName(model: string): string { + return modelDisplayNames[model] ?? model + }, + + toolDisplayName(rawTool: string): string { + return toolNameMap[rawTool] ?? rawTool + }, + + async discoverSessions(): Promise { + return discoverSessionsInBase(dir) + }, + + createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { + return createParser(source, seenKeys) + }, + } +} + +export const codebuff = createCodebuffProvider() diff --git a/src/providers/codex.ts b/src/providers/codex.ts index 1c712453..ca20deb7 100644 --- a/src/providers/codex.ts +++ b/src/providers/codex.ts @@ -65,6 +65,8 @@ type CodexTokenUsage = { } const CHARS_PER_TOKEN = 4 +const RAW_HEAD_BYTES = 64 * 1024 +const LARGE_TEXT_CAP = 2000 function getCodexDir(override?: string): string { return override ?? process.env['CODEX_HOME'] ?? join(homedir(), '.codex') @@ -126,6 +128,116 @@ async function isValidCodexSession(filePath: string): Promise<{ valid: boolean; return { valid, meta: valid ? entry : undefined } } +function getRawJsonStringField(head: string, field: string): string | undefined { + const re = new RegExp(`"${field}"\\s*:\\s*"((?:\\\\.|[^"\\\\])*)"`) + const match = re.exec(head) + if (!match) return undefined + try { + return JSON.parse(`"${match[1]}"`) as string + } catch { + return match[1] + } +} + +function payloadHead(head: string): string { + const idx = head.indexOf('"payload"') + return idx === -1 ? head : head.slice(idx) +} + +function countJsonStringBytes(source: Buffer, valueStart: number): number { + let count = 0 + for (let i = valueStart; i < source.length; i++) { + const ch = source[i] + if (ch === 0x5c) { + i++ + count++ + continue + } + if (ch === 0x22) return count + count++ + } + return count +} + +function extractFirstJsonText(source: Buffer, cap = LARGE_TEXT_CAP): string { + const key = Buffer.from('"text"') + const idx = source.indexOf(key) + if (idx === -1) return '' + const colon = source.indexOf(0x3a, idx + key.length) + if (colon === -1) return '' + const qStart = source.indexOf(0x22, colon + 1) + if (qStart === -1) return '' + const chunks: number[] = [] + for (let i = qStart + 1; i < source.length && chunks.length < cap; i++) { + const ch = source[i] + if (ch === 0x5c) { + const next = source[++i] + if (next === 0x6e) chunks.push(0x0a) + else if (next === 0x72) chunks.push(0x0d) + else if (next === 0x74) chunks.push(0x09) + else if (next !== undefined) chunks.push(next) + continue + } + if (ch === 0x22) break + chunks.push(ch) + } + return Buffer.from(chunks).toString('utf-8') +} + +function countFirstJsonText(source: Buffer): number { + const key = Buffer.from('"text"') + const idx = source.indexOf(key) + if (idx === -1) return 0 + const colon = source.indexOf(0x3a, idx + key.length) + if (colon === -1) return 0 + const qStart = source.indexOf(0x22, colon + 1) + if (qStart === -1) return 0 + return countJsonStringBytes(source, qStart + 1) +} + +function parseCodexLine(line: string | Buffer): CodexEntry | null { + if (typeof line === 'string') { + const trimmed = line.trim() + if (!trimmed) return null + try { + return JSON.parse(trimmed) as CodexEntry + } catch { + return null + } + } + + if (line.length === 0) return null + const head = line.subarray(0, RAW_HEAD_BYTES).toString('utf-8') + const type = getRawJsonStringField(head, 'type') + if (!type) return null + const pHead = payloadHead(head) + const payloadType = getRawJsonStringField(pHead, 'type') + const role = getRawJsonStringField(pHead, 'role') + + const entry: CodexEntry = { + type, + timestamp: getRawJsonStringField(head, 'timestamp'), + payload: { + type: payloadType, + role, + cwd: getRawJsonStringField(pHead, 'cwd'), + model_provider: getRawJsonStringField(pHead, 'model_provider'), + originator: getRawJsonStringField(pHead, 'originator'), + session_id: getRawJsonStringField(pHead, 'session_id'), + model: getRawJsonStringField(pHead, 'model'), + name: getRawJsonStringField(pHead, 'name'), + }, + } + + if (type === 'response_item' && payloadType === 'message' && role === 'user') { + entry.payload!.content = [{ type: 'input_text', text: extractFirstJsonText(line) }] + } else if (type === 'response_item' && payloadType === 'message' && role === 'assistant') { + entry.payload!.content = [{ type: 'output_text', text: 'x'.repeat(Math.min(countFirstJsonText(line), LARGE_TEXT_CAP)) }] + } + + return entry +} + async function discoverSessionsInDir(codexDir: string): Promise { const sessionsDir = join(codexDir, 'sessions') const sources: SessionSource[] = [] @@ -224,18 +336,12 @@ function createParser(source: SessionSource, seenKeys: Set): SessionPars // Stream the session file line by line. Heavy Codex sessions can exceed // 250 MB on disk; reading the entire file into a string would either hit // the readSessionFile cap or push V8 toward its 512 MB string limit - // after split('\n'). readSessionLines streams via readline so memory - // stays bounded to the longest line. - for await (const rawLine of readSessionLines(source.path)) { + // after split('\n'). readSessionLines streams raw buffers and hands + // huge lines to the compact parser without full string conversion. + for await (const rawLine of readSessionLines(source.path, undefined, { largeLineAsBuffer: true })) { sawAnyLine = true - const line = rawLine.trim() - if (!line) continue - let entry: CodexEntry - try { - entry = JSON.parse(line) as CodexEntry - } catch { - continue - } + const entry = parseCodexLine(rawLine) + if (!entry) continue if (entry.type === 'session_meta') { sessionId = entry.payload?.session_id ?? basename(source.path, '.jsonl') diff --git a/src/providers/cursor.ts b/src/providers/cursor.ts index 63625126..ebd7f918 100644 --- a/src/providers/cursor.ts +++ b/src/providers/cursor.ts @@ -329,7 +329,8 @@ const USER_MESSAGES_QUERY = ` // the whole template. The original combined string is preserved as // BUBBLE_QUERY_SINCE for any caller that doesn't want the cap. const BUBBLE_QUERY_SINCE_HEAD = BUBBLE_QUERY_BASE + ` - AND (json_extract(value, '$.createdAt') > ? OR json_extract(value, '$.createdAt') IS NULL)` + AND json_extract(value, '$.createdAt') IS NOT NULL + AND json_extract(value, '$.createdAt') > ?` const BUBBLE_QUERY_SINCE_TAIL = ` ORDER BY ROWID ASC ` @@ -458,6 +459,7 @@ function parseBubbles(db: SqliteDatabase, seenKeys: Set): { calls: Parse } const createdAt = row.created_at ?? '' + if (!createdAt) continue // The JSON `conversationId` field on bubbles is empty in current // Cursor builds. The real composerId lives in the row key // `bubbleId::`. Extract from the key so the @@ -487,7 +489,7 @@ function parseBubbles(db: SqliteDatabase, seenKeys: Set): { calls: Parse const costUSD = calculateCost(pricingModel, inputTokens, outputTokens, 0, 0, 0) - const timestamp = createdAt || new Date().toISOString() + const timestamp = createdAt const userQuestion = takeUserMessage(userMessages, conversationId) const assistantText = blobToText(row.user_text) const userText = (userQuestion + ' ' + assistantText).trim() diff --git a/src/providers/ibm-bob.ts b/src/providers/ibm-bob.ts new file mode 100644 index 00000000..5aec0f65 --- /dev/null +++ b/src/providers/ibm-bob.ts @@ -0,0 +1,59 @@ +import { join } from 'path' +import { homedir } from 'os' + +import { getShortModelName } from '../models.js' +import { discoverClineTasksInBaseDirs, createClineParser } from './vscode-cline-parser.js' +import type { Provider, SessionSource, SessionParser } from './types.js' + +const PROVIDER_NAME = 'ibm-bob' +const DISPLAY_NAME = 'IBM Bob' +const EXTENSION_ID = 'ibm.bob-code' +const FALLBACK_MODEL = 'ibm-bob-auto' + +export function getIBMBobGlobalStorageDirs(): string[] { + const home = homedir() + if (process.platform === 'darwin') { + return [ + join(home, 'Library', 'Application Support', 'IBM Bob', 'User', 'globalStorage', EXTENSION_ID), + join(home, 'Library', 'Application Support', 'Bob-IDE', 'User', 'globalStorage', EXTENSION_ID), + ] + } + if (process.platform === 'win32') { + const appData = process.env['APPDATA'] ?? join(home, 'AppData', 'Roaming') + return [ + join(appData, 'IBM Bob', 'User', 'globalStorage', EXTENSION_ID), + join(appData, 'Bob-IDE', 'User', 'globalStorage', EXTENSION_ID), + ] + } + const configHome = process.env['XDG_CONFIG_HOME'] ?? join(home, '.config') + return [ + join(configHome, 'IBM Bob', 'User', 'globalStorage', EXTENSION_ID), + join(configHome, 'Bob-IDE', 'User', 'globalStorage', EXTENSION_ID), + ] +} + +export function createIBMBobProvider(overrideDir?: string): Provider { + return { + name: PROVIDER_NAME, + displayName: DISPLAY_NAME, + + modelDisplayName(model: string): string { + return getShortModelName(model) + }, + + toolDisplayName(rawTool: string): string { + return rawTool + }, + + async discoverSessions(): Promise { + const dirs = overrideDir ? [overrideDir] : getIBMBobGlobalStorageDirs() + return discoverClineTasksInBaseDirs(dirs, PROVIDER_NAME, DISPLAY_NAME) + }, + + createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { + return createClineParser(source, seenKeys, PROVIDER_NAME, FALLBACK_MODEL) + }, + } +} + +export const ibmBob = createIBMBobProvider() diff --git a/src/providers/index.ts b/src/providers/index.ts index 38ed4902..288bea8d 100644 --- a/src/providers/index.ts +++ b/src/providers/index.ts @@ -1,10 +1,15 @@ import { claude } from './claude.js' +import { cline } from './cline.js' +import { codebuff } from './codebuff.js' import { codex } from './codex.js' import { copilot } from './copilot.js' import { droid } from './droid.js' import { gemini } from './gemini.js' +import { ibmBob } from './ibm-bob.js' import { kiloCode } from './kilo-code.js' import { kiro } from './kiro.js' +import { kimi } from './kimi.js' +import { mistralVibe } from './mistral-vibe.js' import { openclaw } from './openclaw.js' import { pi, omp } from './pi.js' import { qwen } from './qwen.js' @@ -101,7 +106,7 @@ async function loadCrush(): Promise { } } -const coreProviders: Provider[] = [claude, codex, copilot, droid, gemini, kiloCode, kiro, openclaw, pi, omp, qwen, rooCode] +const coreProviders: Provider[] = [claude, cline, codebuff, codex, copilot, droid, gemini, ibmBob, kiloCode, kiro, kimi, mistralVibe, openclaw, pi, omp, qwen, rooCode] export async function getAllProviders(): Promise { const [ag, gs, cursor, opencode, cursorAgent, crush] = await Promise.all([loadAntigravity(), loadGoose(), loadCursor(), loadOpenCode(), loadCursorAgent(), loadCrush()]) diff --git a/src/providers/kimi.ts b/src/providers/kimi.ts new file mode 100644 index 00000000..75242cc8 --- /dev/null +++ b/src/providers/kimi.ts @@ -0,0 +1,394 @@ +import { createHash } from 'crypto' +import { readdir, readFile, stat } from 'fs/promises' +import { basename, dirname, join } from 'path' +import { homedir } from 'os' + +import { extractBashCommands } from '../bash-utils.js' +import { readSessionLines } from '../fs-utils.js' +import { calculateCost, getShortModelName } from '../models.js' +import type { ParsedProviderCall, Provider, SessionParser, SessionSource } from './types.js' + +type JsonObject = Record + +const toolNameMap: Record = { + Shell: 'Bash', + Bash: 'Bash', + bash: 'Bash', + ReadFile: 'Read', + ReadMediaFile: 'Read', + WriteFile: 'Write', + StrReplaceFile: 'Edit', + Grep: 'Grep', + Glob: 'Glob', + SearchWeb: 'WebSearch', + FetchURL: 'WebFetch', + Agent: 'Agent', + AgentTool: 'Agent', + TaskList: 'Agent', + TaskOutput: 'Agent', + TaskStop: 'Agent', + AskUserQuestion: 'AskUser', + SetTodoList: 'TodoWrite', + Think: 'Think', + EnterPlanMode: 'EnterPlanMode', + ExitPlanMode: 'ExitPlanMode', + SendDMail: 'DMail', +} + +function asObject(value: unknown): JsonObject | null { + return value && typeof value === 'object' && !Array.isArray(value) ? value as JsonObject : null +} + +function stringField(obj: JsonObject | null, key: string): string | undefined { + const value = obj?.[key] + return typeof value === 'string' ? value : undefined +} + +function numericField(obj: JsonObject, ...keys: string[]): number { + for (const key of keys) { + const raw = obj[key] + const n = typeof raw === 'number' ? raw : typeof raw === 'string' ? Number(raw) : NaN + if (Number.isFinite(n) && n > 0) return Math.trunc(n) + } + return 0 +} + +function getShareDir(overrideDir?: string): string { + return overrideDir ?? process.env['KIMI_SHARE_DIR'] ?? join(homedir(), '.kimi') +} + +function md5(text: string): string { + return createHash('md5').update(text, 'utf-8').digest('hex') +} + +function projectNameFromPath(pathValue: string): string { + const cleaned = pathValue.replace(/\/+$/, '') + return basename(cleaned) || cleaned || 'kimi' +} + +async function loadProjectNames(shareDir: string): Promise> { + const projects = new Map() + const raw = await readFile(join(shareDir, 'kimi.json'), 'utf-8').catch(() => null) + if (!raw) return projects + + let data: unknown + try { + data = JSON.parse(raw) + } catch { + return projects + } + + const workDirs = asObject(data)?.['work_dirs'] + if (!Array.isArray(workDirs)) return projects + + for (const entry of workDirs) { + const obj = asObject(entry) + const pathValue = stringField(obj, 'path') + if (!pathValue) continue + const hash = md5(pathValue) + const project = projectNameFromPath(pathValue) + projects.set(hash, project) + + const kaos = stringField(obj, 'kaos') + if (kaos && kaos !== 'local') projects.set(`${kaos}_${hash}`, project) + } + + return projects +} + +function parseTomlString(raw: string): string | null { + const value = raw.trim() + if (!value) return null + if (value.startsWith('"')) { + const match = value.match(/^"((?:[^"\\]|\\.)*)"/) + if (!match) return null + try { + return JSON.parse(`"${match[1]}"`) as string + } catch { + return match[1] ?? null + } + } + if (value.startsWith("'")) { + const match = value.match(/^'([^']*)'/) + return match?.[1] ?? null + } + const match = value.match(/^([^#\s]+)/) + return match?.[1] ?? null +} + +function parseDefaultModelKey(configToml: string): string | null { + for (const line of configToml.split('\n')) { + const match = line.match(/^\s*default_model\s*=\s*(.+)$/) + if (!match) continue + return parseTomlString(match[1]!) + } + return null +} + +function parseModelSectionName(line: string): string | null { + const match = line.trim().match(/^\[models\.(?:"([^"]+)"|'([^']+)'|([^\]]+))\]$/) + if (!match) return null + return (match[1] ?? match[2] ?? match[3] ?? '').trim() || null +} + +function parseModelIdForKey(configToml: string, modelKey: string): string | null { + let inSection = false + for (const line of configToml.split('\n')) { + const section = parseModelSectionName(line) + if (section !== null) { + inSection = section === modelKey + continue + } + if (!inSection) continue + if (/^\s*\[/.test(line)) { + inSection = false + continue + } + const match = line.match(/^\s*model\s*=\s*(.+)$/) + if (!match) continue + return parseTomlString(match[1]!) + } + return null +} + +async function getConfiguredModel(shareDir: string): Promise { + const envModel = process.env['KIMI_MODEL_NAME']?.trim() + if (envModel) return envModel + + const raw = await readFile(join(shareDir, 'config.toml'), 'utf-8').catch(() => null) + if (!raw) return 'kimi-auto' + + const defaultModel = parseDefaultModelKey(raw) + if (!defaultModel) return 'kimi-auto' + + return parseModelIdForKey(raw, defaultModel) ?? defaultModel +} + +function parseJsonObject(text: string | undefined): JsonObject | null { + if (!text) return null + try { + return asObject(JSON.parse(text)) + } catch { + return null + } +} + +function extractUserText(value: unknown): string { + if (typeof value === 'string') return value.slice(0, 500) + if (!Array.isArray(value)) return '' + + return value + .map(part => stringField(asObject(part), 'text') ?? '') + .filter(Boolean) + .join(' ') + .slice(0, 500) +} + +function timestampToIso(value: unknown): string { + if (typeof value === 'string') return value + if (typeof value !== 'number' || !Number.isFinite(value)) return '' + + const millis = value > 1_000_000_000_000 ? value : value * 1000 + const date = new Date(millis) + return Number.isFinite(date.getTime()) ? date.toISOString() : '' +} + +function extractEnvelope(record: JsonObject): { type: string; payload: JsonObject; timestamp: string } | null { + const message = asObject(record['message']) + const envelope = message ?? record + const type = stringField(envelope, 'type') + const payload = asObject(envelope['payload']) + if (!type || !payload) return null + return { type, payload, timestamp: timestampToIso(record['timestamp']) } +} + +function extractUsage(payload: JsonObject): { + inputTokens: number + outputTokens: number + cacheReadInputTokens: number + cacheCreationInputTokens: number +} | null { + const usage = asObject(payload['token_usage']) ?? asObject(payload['usage']) + if (!usage) return null + + const cacheReadInputTokens = numericField(usage, 'input_cache_read', 'cache_read_input_tokens', 'cached_input_tokens') + const cacheCreationInputTokens = numericField(usage, 'input_cache_creation', 'cache_creation_input_tokens') + let inputTokens = numericField(usage, 'input_other', 'input_tokens') + if (inputTokens === 0) { + const totalInput = numericField(usage, 'input') + inputTokens = Math.max(0, totalInput - cacheReadInputTokens - cacheCreationInputTokens) + } + const outputTokens = numericField(usage, 'output', 'output_tokens') + + if (inputTokens === 0 && outputTokens === 0 && cacheReadInputTokens === 0 && cacheCreationInputTokens === 0) { + return null + } + + return { inputTokens, outputTokens, cacheReadInputTokens, cacheCreationInputTokens } +} + +function extractTool(payload: JsonObject): { tool: string; bashCommands: string[] } | null { + const fn = asObject(payload['function']) + const rawName = stringField(fn, 'name') ?? stringField(payload, 'name') + if (!rawName) return null + + const tool = toolNameMap[rawName] ?? rawName + const argsText = stringField(fn, 'arguments') ?? stringField(payload, 'arguments') + const args = parseJsonObject(argsText) + const command = stringField(args, 'command') + const bashCommands = tool === 'Bash' && command ? extractBashCommands(command) : [] + + return { tool, bashCommands } +} + +function createParser(source: SessionSource, shareDir: string, seenKeys: Set): SessionParser { + return { + async *parse(): AsyncGenerator { + const configuredModel = await getConfiguredModel(shareDir) + const tools = new Set() + const bashCommands = new Set() + let currentUserMessage = '' + const sessionId = basename(dirname(source.path)) + let index = 0 + + for await (const line of readSessionLines(source.path)) { + if (!line.trim()) continue + + let record: JsonObject | null = null + try { + record = asObject(JSON.parse(line)) + } catch { + continue + } + if (!record) continue + + const envelope = extractEnvelope(record) + if (!envelope || envelope.type === 'metadata') continue + + if (envelope.type === 'TurnBegin' || envelope.type === 'SteerInput') { + currentUserMessage = extractUserText(envelope.payload['user_input']) + continue + } + + if (envelope.type === 'TurnEnd') { + currentUserMessage = '' + tools.clear() + bashCommands.clear() + continue + } + + if (envelope.type === 'ToolCall' || envelope.type === 'ToolCallRequest') { + const extracted = extractTool(envelope.payload) + if (!extracted) continue + tools.add(extracted.tool) + for (const command of extracted.bashCommands) bashCommands.add(command) + continue + } + + if (envelope.type !== 'StatusUpdate') continue + + const usage = extractUsage(envelope.payload) + if (!usage) continue + + const rawMessageId = stringField(envelope.payload, 'message_id') + const dedupKey = `kimi:${sessionId}:${rawMessageId ?? index}` + index++ + if (seenKeys.has(dedupKey)) continue + seenKeys.add(dedupKey) + + const model = stringField(envelope.payload, 'model') ?? stringField(envelope.payload, 'model_name') ?? configuredModel + const costUSD = calculateCost( + model, + usage.inputTokens, + usage.outputTokens, + usage.cacheCreationInputTokens, + usage.cacheReadInputTokens, + 0, + ) + + yield { + provider: 'kimi', + model, + inputTokens: usage.inputTokens, + outputTokens: usage.outputTokens, + cacheCreationInputTokens: usage.cacheCreationInputTokens, + cacheReadInputTokens: usage.cacheReadInputTokens, + cachedInputTokens: usage.cacheReadInputTokens, + reasoningTokens: 0, + webSearchRequests: 0, + costUSD, + tools: [...tools], + bashCommands: [...bashCommands], + timestamp: envelope.timestamp, + speed: 'standard', + deduplicationKey: dedupKey, + userMessage: currentUserMessage, + sessionId, + } + + tools.clear() + bashCommands.clear() + } + }, + } +} + +async function addWireSource(sources: SessionSource[], filePath: string, project: string): Promise { + const s = await stat(filePath).catch(() => null) + if (!s?.isFile()) return + sources.push({ path: filePath, project, provider: 'kimi' }) +} + +export function createKimiProvider(overrideDir?: string): Provider { + const shareDir = getShareDir(overrideDir) + + return { + name: 'kimi', + displayName: 'Kimi', + + modelDisplayName(model: string): string { + return getShortModelName(model) + }, + + toolDisplayName(rawTool: string): string { + return toolNameMap[rawTool] ?? rawTool + }, + + async discoverSessions(): Promise { + const sources: SessionSource[] = [] + const sessionsRoot = join(shareDir, 'sessions') + const projectNames = await loadProjectNames(shareDir) + const workDirs = await readdir(sessionsRoot, { withFileTypes: true }).catch(() => []) + + for (const workDir of workDirs) { + if (!workDir.isDirectory()) continue + + const project = projectNames.get(workDir.name) ?? workDir.name + const workDirPath = join(sessionsRoot, workDir.name) + const sessionDirs = await readdir(workDirPath, { withFileTypes: true }).catch(() => []) + + for (const sessionDir of sessionDirs) { + if (!sessionDir.isDirectory()) continue + + const sessionPath = join(workDirPath, sessionDir.name) + await addWireSource(sources, join(sessionPath, 'wire.jsonl'), project) + + const subagentsPath = join(sessionPath, 'subagents') + const subagents = await readdir(subagentsPath, { withFileTypes: true }).catch(() => []) + for (const subagent of subagents) { + if (!subagent.isDirectory()) continue + await addWireSource(sources, join(subagentsPath, subagent.name, 'wire.jsonl'), project) + } + } + } + + return sources + }, + + createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { + return createParser(source, shareDir, seenKeys) + }, + } +} + +export const kimi = createKimiProvider() diff --git a/src/providers/mistral-vibe.ts b/src/providers/mistral-vibe.ts new file mode 100644 index 00000000..7feb9884 --- /dev/null +++ b/src/providers/mistral-vibe.ts @@ -0,0 +1,355 @@ +import { readdir, stat } from 'fs/promises' +import { basename, join } from 'path' +import { homedir } from 'os' + +import { readSessionFile, readSessionLines } from '../fs-utils.js' +import { calculateCost } from '../models.js' +import { extractBashCommands } from '../bash-utils.js' +import type { Provider, SessionSource, SessionParser, ParsedProviderCall } from './types.js' + +const METADATA_FILENAME = 'meta.json' +const MESSAGES_FILENAME = 'messages.jsonl' +const DEFAULT_MODEL = 'mistral-medium-3.5' + +const modelDisplayNames: Record = { + 'mistral-medium-3.5': 'Mistral Medium 3.5', + 'mistral-vibe-cli-latest': 'Mistral Vibe CLI', + 'devstral-small': 'Devstral Small', + 'devstral-small-latest': 'Devstral Small', + devstral: 'Devstral', + local: 'Local', +} + +const toolNameMap: Record = { + bash: 'Bash', + read_file: 'Read', + write_file: 'Write', + search_replace: 'Edit', + grep: 'Grep', + task: 'Agent', + todo: 'TodoWrite', + skill: 'Skill', + web_fetch: 'WebFetch', + web_search: 'WebSearch', + ask_user_question: 'AskUser', + exit_plan_mode: 'ExitPlanMode', +} + +type VibeStats = { + session_prompt_tokens?: number + session_completion_tokens?: number + input_price_per_million?: number + output_price_per_million?: number + tokens_per_second?: number +} + +type VibeModelConfig = { + name?: string + alias?: string + input_price?: number + output_price?: number +} + +type VibeMetadata = { + session_id?: string + start_time?: string + end_time?: string | null + environment?: { + working_directory?: string | null + } + stats?: VibeStats + config?: { + active_model?: string + models?: VibeModelConfig[] + } + title?: string | null +} + +type VibeToolCall = { + function?: { + name?: string + arguments?: string | Record | null + } +} + +type VibeMessage = { + role?: string + content?: unknown + tool_calls?: VibeToolCall[] | null +} + +function getMistralVibeSessionsDir(override?: string): string { + if (override) return override + const configuredHome = process.env['VIBE_HOME'] + const vibeHome = configuredHome ? expandHome(configuredHome) : join(homedir(), '.vibe') + return join(vibeHome, 'logs', 'session') +} + +function expandHome(path: string): string { + if (path === '~') return homedir() + if (path.startsWith('~/')) return join(homedir(), path.slice(2)) + return path +} + +async function isFile(path: string): Promise { + const s = await stat(path).catch(() => null) + return Boolean(s?.isFile()) +} + +async function isDirectory(path: string): Promise { + const s = await stat(path).catch(() => null) + return Boolean(s?.isDirectory()) +} + +async function hasSessionFiles(dir: string): Promise { + const [hasMetadata, hasMessages] = await Promise.all([ + isFile(join(dir, METADATA_FILENAME)), + isFile(join(dir, MESSAGES_FILENAME)), + ]) + return hasMetadata && hasMessages +} + +async function readJsonFile(path: string): Promise { + const raw = await readSessionFile(path) + if (raw === null) return null + try { + const parsed = JSON.parse(raw) as unknown + return typeof parsed === 'object' && parsed !== null ? parsed as T : null + } catch { + return null + } +} + +async function discoverSessionDirs(root: string): Promise { + const sessionDirs: string[] = [] + + let entries: string[] + try { + entries = (await readdir(root)).sort() + } catch { + return sessionDirs + } + + for (const entry of entries) { + const dir = join(root, entry) + if (!await isDirectory(dir)) continue + + if (await hasSessionFiles(dir)) { + sessionDirs.push(dir) + } + + const agentsDir = join(dir, 'agents') + if (!await isDirectory(agentsDir)) continue + + let agentEntries: string[] + try { + agentEntries = (await readdir(agentsDir)).sort() + } catch { + continue + } + + for (const agentEntry of agentEntries) { + const agentDir = join(agentsDir, agentEntry) + if (await isDirectory(agentDir) && await hasSessionFiles(agentDir)) { + sessionDirs.push(agentDir) + } + } + } + + return sessionDirs +} + +function activeModelConfig(metadata: VibeMetadata): VibeModelConfig | null { + const activeModel = metadata.config?.active_model + const models = metadata.config?.models + if (!activeModel || !Array.isArray(models)) return null + return models.find(m => m.alias === activeModel || m.name === activeModel) ?? null +} + +function resolveModel(metadata: VibeMetadata): string { + const activeModel = metadata.config?.active_model + if (activeModel) return activeModel + const configured = activeModelConfig(metadata) + return configured?.alias ?? configured?.name ?? DEFAULT_MODEL +} + +function safeNumber(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : 0 +} + +function calculateSessionCost(metadata: VibeMetadata, model: string, inputTokens: number, outputTokens: number): number { + const stats = metadata.stats ?? {} + const configured = activeModelConfig(metadata) + const inputPrice = safeNumber(stats.input_price_per_million) || safeNumber(configured?.input_price) + const outputPrice = safeNumber(stats.output_price_per_million) || safeNumber(configured?.output_price) + + if (inputPrice > 0 || outputPrice > 0) { + return (inputTokens / 1_000_000) * inputPrice + (outputTokens / 1_000_000) * outputPrice + } + + return calculateCost(model, inputTokens, outputTokens, 0, 0, 0) +} + +function normalizeContent(content: unknown): string { + if (typeof content === 'string') return content + if (Array.isArray(content)) { + return content + .map(part => { + if (typeof part === 'string') return part + if (part && typeof part === 'object' && 'text' in part && typeof part.text === 'string') return part.text + return '' + }) + .filter(Boolean) + .join(' ') + } + return '' +} + +function parseToolArguments(raw: string | Record | null | undefined): Record { + if (!raw) return {} + if (typeof raw === 'object') return raw + try { + const parsed = JSON.parse(raw) as unknown + return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed as Record : {} + } catch { + return {} + } +} + +function extractTools(messages: VibeMessage[]): { tools: string[]; bashCommands: string[] } { + const tools: string[] = [] + const bashCommands: string[] = [] + + for (const message of messages) { + if (message.role !== 'assistant') continue + for (const toolCall of message.tool_calls ?? []) { + const rawName = toolCall.function?.name + if (!rawName) continue + + const mappedName = toolNameMap[rawName] ?? rawName + tools.push(mappedName) + + if (mappedName !== 'Bash') continue + const args = parseToolArguments(toolCall.function?.arguments) + const command = args['command'] + if (typeof command === 'string') { + bashCommands.push(...extractBashCommands(command)) + } + } + } + + return { + tools: [...new Set(tools)], + bashCommands: [...new Set(bashCommands)], + } +} + +async function readMessages(path: string): Promise { + const messages: VibeMessage[] = [] + for await (const line of readSessionLines(path)) { + if (!line.trim()) continue + try { + const parsed = JSON.parse(line) as unknown + if (parsed && typeof parsed === 'object') messages.push(parsed as VibeMessage) + } catch { + continue + } + } + return messages +} + +function firstUserMessage(messages: VibeMessage[], fallback?: string | null): string { + for (const message of messages) { + if (message.role !== 'user') continue + const text = normalizeContent(message.content).trim() + if (text) return text.slice(0, 500) + } + return (fallback ?? '').slice(0, 500) +} + +function createParser(source: SessionSource, seenKeys: Set): SessionParser { + return { + async *parse(): AsyncGenerator { + const metadataPath = join(source.path, METADATA_FILENAME) + const messagesPath = join(source.path, MESSAGES_FILENAME) + const metadata = await readJsonFile(metadataPath) + if (!metadata) return + + const stats = metadata.stats ?? {} + const inputTokens = safeNumber(stats.session_prompt_tokens) + const outputTokens = safeNumber(stats.session_completion_tokens) + if (inputTokens === 0 && outputTokens === 0) return + + const sessionId = metadata.session_id || basename(source.path) + const deduplicationKey = `mistral-vibe:${sessionId}` + if (seenKeys.has(deduplicationKey)) return + seenKeys.add(deduplicationKey) + + const messages = await readMessages(messagesPath) + const model = resolveModel(metadata) + const { tools, bashCommands } = extractTools(messages) + const costUSD = calculateSessionCost(metadata, model, inputTokens, outputTokens) + + yield { + provider: 'mistral-vibe', + model, + inputTokens, + outputTokens, + cacheCreationInputTokens: 0, + cacheReadInputTokens: 0, + cachedInputTokens: 0, + reasoningTokens: 0, + webSearchRequests: 0, + costUSD, + tools, + bashCommands, + timestamp: metadata.end_time ?? metadata.start_time ?? '', + speed: 'standard', + deduplicationKey, + userMessage: firstUserMessage(messages, metadata.title), + sessionId, + } + }, + } +} + +export function createMistralVibeProvider(sessionsDir?: string): Provider { + const dir = getMistralVibeSessionsDir(sessionsDir) + + return { + name: 'mistral-vibe', + displayName: 'Mistral Vibe', + + modelDisplayName(model: string): string { + return modelDisplayNames[model] ?? model + }, + + toolDisplayName(rawTool: string): string { + return toolNameMap[rawTool] ?? rawTool + }, + + async discoverSessions(): Promise { + const dirs = await discoverSessionDirs(dir) + const sources: SessionSource[] = [] + + for (const sessionDir of dirs) { + const metadata = await readJsonFile(join(sessionDir, METADATA_FILENAME)) + if (!metadata) continue + const cwd = metadata.environment?.working_directory + sources.push({ + path: sessionDir, + project: cwd ? basename(cwd) : basename(sessionDir), + provider: 'mistral-vibe', + }) + } + + return sources + }, + + createSessionParser(source: SessionSource, seenKeys: Set): SessionParser { + return createParser(source, seenKeys) + }, + } +} + +export const mistralVibe = createMistralVibeProvider() diff --git a/src/providers/opencode.ts b/src/providers/opencode.ts index b39230c7..6b0f8ed8 100644 --- a/src/providers/opencode.ts +++ b/src/providers/opencode.ts @@ -4,7 +4,7 @@ import { homedir } from 'os' import { calculateCost, getShortModelName } from '../models.js' import { extractBashCommands } from '../bash-utils.js' -import { isSqliteAvailable, getSqliteLoadError, openDatabase, blobToText, type SqliteDatabase } from '../sqlite.js' +import { isSqliteAvailable, getSqliteLoadError, openDatabase, blobToText, isSqliteBusyError, type SqliteDatabase } from '../sqlite.js' import type { Provider, SessionSource, @@ -64,6 +64,25 @@ const toolNameMap: Record = { patch: 'Patch', } +function normalizeToolName(rawTool?: string): string { + if (!rawTool) return '' + if (rawTool.startsWith('mcp__')) return rawTool + + const builtIn = toolNameMap[rawTool] + if (builtIn) return builtIn + + // OpenCode stores MCP calls as `_` with no separate server field. + // Built-ins are handled above, and server ids are assumed not to contain `_`. + const serverSeparator = rawTool.indexOf('_') + if (serverSeparator > 0 && serverSeparator < rawTool.length - 1) { + const server = rawTool.slice(0, serverSeparator) + const tool = rawTool.slice(serverSeparator + 1) + return `mcp__${server}__${tool}` + } + + return rawTool +} + function sanitize(dir: string): string { return dir.replace(/^\//, '').replace(/\//g, '-') } @@ -107,7 +126,8 @@ function validateSchemaDetailed(db: SqliteDatabase): SchemaCheckResult { for (const table of required) { try { db.query<{ cnt: number }>(`SELECT COUNT(*) as cnt FROM ${table} LIMIT 1`) - } catch { + } catch (err) { + if (isSqliteBusyError(err)) throw err missing.push(table) } } @@ -232,7 +252,7 @@ function createParser( const msgParts = partsByMsg.get(msg.id) ?? [] const toolParts = msgParts.filter((p) => p.type === 'tool') const tools = toolParts - .map((p) => toolNameMap[p.tool ?? ''] ?? p.tool ?? '') + .map((p) => normalizeToolName(p.tool)) .filter(Boolean) const bashCommands = toolParts diff --git a/src/providers/types.ts b/src/providers/types.ts index 4e9a98a7..90d5e1c2 100644 --- a/src/providers/types.ts +++ b/src/providers/types.ts @@ -27,6 +27,8 @@ export type ParsedProviderCall = { deduplicationKey: string userMessage: string sessionId: string + project?: string + projectPath?: string } export type Provider = { diff --git a/src/providers/vscode-cline-parser.ts b/src/providers/vscode-cline-parser.ts index d1d26c0f..ffad9390 100644 --- a/src/providers/vscode-cline-parser.ts +++ b/src/providers/vscode-cline-parser.ts @@ -24,6 +24,23 @@ export function getVSCodeGlobalStoragePath(extensionId: string): string { export async function discoverClineTasks(extensionId: string, providerName: string, displayName: string, overrideDir?: string): Promise { const baseDir = overrideDir ?? getVSCodeGlobalStoragePath(extensionId) + return discoverClineTasksInBaseDirs([baseDir], providerName, displayName) +} + +export async function discoverClineTasksInBaseDirs(baseDirs: string[], providerName: string, displayName: string): Promise { + const sources: SessionSource[] = [] + const seen = new Set() + for (const baseDir of baseDirs) { + for (const source of await discoverClineTasksInBaseDir(baseDir, providerName, displayName)) { + if (seen.has(source.path)) continue + seen.add(source.path) + sources.push(source) + } + } + return sources +} + +async function discoverClineTasksInBaseDir(baseDir: string, providerName: string, displayName: string): Promise { const tasksDir = join(baseDir, 'tasks') const sources: SessionSource[] = [] @@ -50,28 +67,43 @@ export async function discoverClineTasks(extensionId: string, providerName: stri } const MODEL_TAG_RE = /([^<]+)<\/model>/ +const WORKSPACE_DIR_RE = /Current Workspace Directory \(([^)]+)\)/ + +type HistoryMeta = { model: string; workspace: string | null } -function extractModelFromHistory(taskDir: string): Promise { +function extractHistoryMeta(taskDir: string, fallbackModel: string): Promise { return readFile(join(taskDir, 'api_conversation_history.json'), 'utf-8') .then(raw => { const msgs = JSON.parse(raw) as Array<{ role?: string; content?: Array<{ text?: string }> }> - if (!Array.isArray(msgs)) return 'cline-auto' + if (!Array.isArray(msgs)) return { model: fallbackModel, workspace: null } + let model: string | null = null + let workspace: string | null = null for (const msg of msgs) { if (msg.role !== 'user' || !Array.isArray(msg.content)) continue for (const block of msg.content) { - const match = typeof block.text === 'string' && MODEL_TAG_RE.exec(block.text) - if (match) { - const raw = match[1] - return raw.includes('/') ? raw.split('/').pop()! : raw + if (typeof block.text !== 'string') continue + if (!model) { + const mm = MODEL_TAG_RE.exec(block.text) + if (mm) model = mm[1].includes('/') ? mm[1].split('/').pop()! : mm[1] + } + if (!workspace) { + const wm = WORKSPACE_DIR_RE.exec(block.text) + if (wm) workspace = wm[1] } + if (model && workspace) break } + if (model && workspace) break } - return 'cline-auto' + return { model: model ?? fallbackModel, workspace } }) - .catch(() => 'cline-auto') + .catch(() => ({ model: fallbackModel, workspace: null })) +} + +function workspaceToProject(workspace: string): string { + return basename(workspace) || workspace } -export function createClineParser(source: SessionSource, seenKeys: Set, providerName: string): SessionParser { +export function createClineParser(source: SessionSource, seenKeys: Set, providerName: string, fallbackModel = 'cline-auto'): SessionParser { return { async *parse(): AsyncGenerator { const taskDir = source.path @@ -93,7 +125,10 @@ export function createClineParser(source: SessionSource, seenKeys: Set, if (!Array.isArray(uiMessages)) return - const model = await extractModelFromHistory(taskDir) + const meta = await extractHistoryMeta(taskDir, fallbackModel) + const model = meta.model + const project = meta.workspace ? workspaceToProject(meta.workspace) : undefined + const projectPath = meta.workspace ?? undefined let userMessage = '' for (const msg of uiMessages) { @@ -156,6 +191,8 @@ export function createClineParser(source: SessionSource, seenKeys: Set, deduplicationKey: dedupKey, userMessage: index === 0 ? userMessage : '', sessionId: taskId, + project, + projectPath, } } }, diff --git a/src/session-cache.ts b/src/session-cache.ts new file mode 100644 index 00000000..41c91b24 --- /dev/null +++ b/src/session-cache.ts @@ -0,0 +1,319 @@ +import { readFile, stat, open, rename, unlink, readdir, mkdir } from 'fs/promises' +import { existsSync } from 'fs' +import { createHash, randomBytes } from 'crypto' +import { join } from 'path' +import { homedir } from 'os' + +// ── Types ────────────────────────────────────────────────────────────── + +export type CachedUsage = { + inputTokens: number + outputTokens: number + cacheCreationInputTokens: number + cacheReadInputTokens: number + cachedInputTokens: number + reasoningTokens: number + webSearchRequests: number + cacheCreationOneHourTokens: number +} + +export type CachedCall = { + provider: string + model: string + usage: CachedUsage + speed: 'standard' | 'fast' + timestamp: string + tools: string[] + bashCommands: string[] + skills: string[] + deduplicationKey: string + project?: string + projectPath?: string +} + +export type CachedTurn = { + timestamp: string + sessionId: string + userMessage: string + calls: CachedCall[] +} + +export type FileFingerprint = { + dev: number + ino: number + mtimeMs: number + sizeBytes: number +} + +export type CachedFile = { + fingerprint: FileFingerprint + lastCompleteLineOffset?: number + canonicalCwd?: string + mcpInventory: string[] + turns: CachedTurn[] +} + +export type ProviderSection = { + envFingerprint: string + files: Record +} + +export type SessionCache = { + version: number + providers: Record +} + +// ── Constants ────────────────────────────────────────────────────────── + +export const CACHE_VERSION = 1 + +const CACHE_FILE = 'session-cache.json' +const TEMP_FILE_MAX_AGE_MS = 5 * 60 * 1000 + +const PROVIDER_ENV_VARS: Record = { + claude: ['CLAUDE_CONFIG_DIRS', 'CLAUDE_CONFIG_DIR'], + codex: ['CODEX_HOME'], + droid: ['FACTORY_DIR'], + cursor: ['XDG_DATA_HOME'], + 'cursor-agent': ['XDG_DATA_HOME'], + opencode: ['XDG_DATA_HOME'], + goose: ['XDG_DATA_HOME'], + crush: ['XDG_DATA_HOME'], + antigravity: ['CODEBURN_CACHE_DIR'], + qwen: ['QWEN_DATA_DIR'], + 'ibm-bob': ['XDG_CONFIG_HOME'], +} + +// ── Cache Dir ────────────────────────────────────────────────────────── + +function getCacheDir(): string { + return process.env['CODEBURN_CACHE_DIR'] ?? join(homedir(), '.cache', 'codeburn') +} + +function getCachePath(): string { + return join(getCacheDir(), CACHE_FILE) +} + +// ── Env Fingerprint ──────────────────────────────────────────────────── + +export function computeEnvFingerprint(provider: string): string { + const vars = PROVIDER_ENV_VARS[provider] ?? [] + const parts = vars.map(v => `${v}=${process.env[v] ?? ''}`) + return createHash('sha256').update(parts.join('\0')).digest('hex').slice(0, 16) +} + +// ── Load / Save ──────────────────────────────────────────────────────── + +export function emptyCache(): SessionCache { + return { version: CACHE_VERSION, providers: {} } +} + +function isNum(v: unknown): v is number { + return typeof v === 'number' && Number.isFinite(v) +} + +function isStringArray(v: unknown): v is string[] { + return Array.isArray(v) && v.every(e => typeof e === 'string') +} + +function isOptionalString(v: unknown): boolean { + return v === undefined || typeof v === 'string' +} + +function isOptionalNum(v: unknown): boolean { + return v === undefined || isNum(v) +} + +function validateFingerprint(fp: unknown): fp is FileFingerprint { + if (!fp || typeof fp !== 'object') return false + const f = fp as Record + return isNum(f['dev']) && isNum(f['ino']) && isNum(f['mtimeMs']) && isNum(f['sizeBytes']) +} + +function validateUsage(u: unknown): u is CachedUsage { + if (!u || typeof u !== 'object') return false + const o = u as Record + return isNum(o['inputTokens']) && isNum(o['outputTokens']) + && isNum(o['cacheCreationInputTokens']) && isNum(o['cacheReadInputTokens']) + && isNum(o['cachedInputTokens']) && isNum(o['reasoningTokens']) + && isNum(o['webSearchRequests']) && isNum(o['cacheCreationOneHourTokens']) +} + +function validateCall(c: unknown): c is CachedCall { + if (!c || typeof c !== 'object') return false + const o = c as Record + return typeof o['provider'] === 'string' + && typeof o['model'] === 'string' + && typeof o['deduplicationKey'] === 'string' + && typeof o['timestamp'] === 'string' + && (o['speed'] === 'standard' || o['speed'] === 'fast') + && isStringArray(o['tools']) + && isStringArray(o['bashCommands']) + && isStringArray(o['skills']) + && isOptionalString(o['project']) + && isOptionalString(o['projectPath']) + && validateUsage(o['usage']) +} + +function validateTurn(t: unknown): t is CachedTurn { + if (!t || typeof t !== 'object') return false + const o = t as Record + return typeof o['timestamp'] === 'string' + && typeof o['sessionId'] === 'string' + && typeof o['userMessage'] === 'string' + && Array.isArray(o['calls']) + && (o['calls'] as unknown[]).every(validateCall) +} + +function validateCachedFile(f: unknown): f is CachedFile { + if (!f || typeof f !== 'object') return false + const o = f as Record + return validateFingerprint(o['fingerprint']) + && isOptionalNum(o['lastCompleteLineOffset']) + && isOptionalString(o['canonicalCwd']) + && isStringArray(o['mcpInventory']) + && Array.isArray(o['turns']) + && (o['turns'] as unknown[]).every(validateTurn) +} + +function validateProviderSection(s: unknown): s is ProviderSection { + if (!s || typeof s !== 'object') return false + const o = s as Record + if (typeof o['envFingerprint'] !== 'string') return false + if (!o['files'] || typeof o['files'] !== 'object' || Array.isArray(o['files'])) return false + return Object.values(o['files'] as Record).every(validateCachedFile) +} + +function validateCache(raw: unknown): raw is SessionCache { + if (!raw || typeof raw !== 'object') return false + const o = raw as Record + if (o['version'] !== CACHE_VERSION) return false + if (!o['providers'] || typeof o['providers'] !== 'object' || Array.isArray(o['providers'])) return false + return Object.values(o['providers'] as Record).every(validateProviderSection) +} + +export async function loadCache(): Promise { + try { + const raw = await readFile(getCachePath(), 'utf-8') + const parsed = JSON.parse(raw) + if (!validateCache(parsed)) return emptyCache() + return parsed + } catch { + return emptyCache() + } +} + +export async function saveCache(cache: SessionCache): Promise { + 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(cache) + + 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 + } +} + +// ── File Fingerprinting ──────────────────────────────────────────────── + +export async function fingerprintFile(filePath: string): Promise { + try { + const s = await stat(filePath) + return { dev: s.dev, ino: s.ino, mtimeMs: s.mtimeMs, sizeBytes: s.size } + } catch { + return null + } +} + +// ── Reconciliation ───────────────────────────────────────────────────── + +export type ReconcileAction = + | { action: 'unchanged' } + | { action: 'appended'; readFromOffset: number } + | { action: 'modified' } + | { action: 'new' } + +export function reconcileFile( + current: FileFingerprint, + cached: CachedFile | undefined, +): ReconcileAction { + if (!cached) return { action: 'new' } + + const fp = cached.fingerprint + + if ( + fp.dev === current.dev && + fp.ino === current.ino && + fp.mtimeMs === current.mtimeMs && + fp.sizeBytes === current.sizeBytes + ) { + return { action: 'unchanged' } + } + + if ( + cached.lastCompleteLineOffset !== undefined && + fp.dev === current.dev && + fp.ino === current.ino && + current.sizeBytes > fp.sizeBytes + ) { + return { action: 'appended', readFromOffset: cached.lastCompleteLineOffset } + } + + return { action: 'modified' } +} + +// ── Dedup Merge ──────────────────────────────────────────────────────── +// When appending incremental data, streaming Claude messages can re-emit +// the same dedup key with updated usage. Merge by key: keep the earliest +// timestamp, take incoming usage/tools/bashCommands/skills (latest wins). + +export function mergeCallByDedupKey( + existing: CachedCall, + incoming: CachedCall, +): CachedCall { + return { + ...incoming, + timestamp: existing.timestamp < incoming.timestamp + ? existing.timestamp + : incoming.timestamp, + } +} + +// ── Temp Cleanup ─────────────────────────────────────────────────────── + +export async function cleanupOrphanedTempFiles(): Promise { + const dir = getCacheDir() + if (!existsSync(dir)) return + + try { + const entries = await readdir(dir) + const now = Date.now() + + const prefix = 'session-cache.json.' + for (const entry of entries) { + if (!entry.startsWith(prefix) || !entry.endsWith('.tmp')) continue + try { + const fullPath = join(dir, entry) + const s = await stat(fullPath) + if (now - s.mtimeMs > TEMP_FILE_MAX_AGE_MS) { + await unlink(fullPath) + } + } catch {} + } + } catch {} +} + + diff --git a/src/sqlite.ts b/src/sqlite.ts index 9242c63f..3fb3c6a8 100644 --- a/src/sqlite.ts +++ b/src/sqlite.ts @@ -16,6 +16,7 @@ export type SqliteDatabase = { type DatabaseSyncCtor = new (path: string, options?: { readOnly?: boolean }) => { prepare(sql: string): { all(...params: unknown[]): Row[] } + exec?(sql: string): void close(): void } @@ -97,12 +98,35 @@ export function getSqliteLoadError(): string { return loadError ?? 'SQLite driver not available' } +export function isSqliteBusyError(err: unknown): boolean { + const e = err as { code?: unknown; errcode?: unknown; errstr?: unknown; message?: unknown } | null + const code = typeof e?.code === 'string' ? e.code : '' + const errcode = typeof e?.errcode === 'number' ? e.errcode : null + const message = [ + typeof e?.message === 'string' ? e.message : '', + typeof e?.errstr === 'string' ? e.errstr : '', + ].join(' ') + + return ( + errcode === 5 || + errcode === 6 || + code === 'SQLITE_BUSY' || + code === 'SQLITE_LOCKED' || + /\bSQLITE_(BUSY|LOCKED)\b|database (?:is |table is )?locked/i.test(message) + ) +} + export function openDatabase(path: string): SqliteDatabase { if (!loadDriver() || DatabaseSync === null) { throw new Error(getSqliteLoadError()) } const db = new DatabaseSync(path, { readOnly: true }) + try { + db.exec?.('PRAGMA busy_timeout = 1000') + } catch { + // Best effort. Some Node sqlite builds may not expose exec on DatabaseSync. + } return { query(sql: string, params: unknown[] = []): T[] { diff --git a/src/types.ts b/src/types.ts index e5562e8f..312906dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -25,6 +25,10 @@ export type ApiUsage = { input_tokens: number output_tokens: number cache_creation_input_tokens?: number + cache_creation?: { + ephemeral_5m_input_tokens?: number + ephemeral_1h_input_tokens?: number + } cache_read_input_tokens?: number server_tool_use?: { web_search_requests?: number @@ -79,6 +83,7 @@ export type ParsedApiCall = { timestamp: string bashCommands: string[] deduplicationKey: string + cacheCreationOneHourTokens?: number } export type TaskCategory = diff --git a/tests/blob-to-text.test.ts b/tests/blob-to-text.test.ts index f54717e1..aeb7ce30 100644 --- a/tests/blob-to-text.test.ts +++ b/tests/blob-to-text.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { blobToText } from '../src/sqlite.js' +import { blobToText, isSqliteBusyError } from '../src/sqlite.js' describe('blobToText', () => { it('returns empty string for null', () => { @@ -37,3 +37,17 @@ describe('blobToText', () => { expect(blobToText(new Uint8Array(0))).toBe('') }) }) + +describe('isSqliteBusyError', () => { + it('detects node:sqlite busy errors by errcode', () => { + expect(isSqliteBusyError({ code: 'ERR_SQLITE_ERROR', errcode: 5, errstr: 'database is locked' })).toBe(true) + }) + + it('detects sqlite locked messages', () => { + expect(isSqliteBusyError(new Error('SQLITE_LOCKED: database table is locked'))).toBe(true) + }) + + it('ignores unrelated sqlite errors', () => { + expect(isSqliteBusyError(new Error('no such table: session'))).toBe(false) + }) +}) diff --git a/tests/cli-plan.test.ts b/tests/cli-plan.test.ts index b146f2ad..14e2b121 100644 --- a/tests/cli-plan.test.ts +++ b/tests/cli-plan.test.ts @@ -5,41 +5,136 @@ import { spawnSync } from 'node:child_process' import { describe, it, expect } from 'vitest' +const CLI_PLAN_TIMEOUT_MS = 10_000 + function runCli(args: string[], home: string) { return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], { cwd: process.cwd(), env: { ...process.env, HOME: home, + USERPROFILE: home, // os.homedir() uses USERPROFILE on Windows + HOMEPATH: home, + HOMEDRIVE: '', }, encoding: 'utf-8', }) } describe('codeburn plan command', () => { - it('persists plan set and clears on reset', async () => { + it('persists provider-keyed plans and clears on reset', async () => { const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) try { const setResult = runCli(['plan', 'set', 'claude-max'], home) expect(setResult.status).toBe(0) + const setCodexResult = runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home) + expect(setCodexResult.status).toBe(0) + const configPath = join(home, '.config', 'codeburn', 'config.json') const configRaw = await readFile(configPath, 'utf-8') - const config = JSON.parse(configRaw) as { plan?: { id?: string; monthlyUsd?: number } } - expect(config.plan?.id).toBe('claude-max') - expect(config.plan?.monthlyUsd).toBe(200) + const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string; monthlyUsd?: number }; codex?: { id?: string; monthlyUsd?: number } } } + expect(config.plans?.claude?.id).toBe('claude-max') + expect(config.plans?.claude?.monthlyUsd).toBe(200) + expect(config.plans?.codex?.id).toBe('custom') + expect(config.plans?.codex?.monthlyUsd).toBe(200) const resetResult = runCli(['plan', 'reset'], home) expect(resetResult.status).toBe(0) const afterResetRaw = await readFile(configPath, 'utf-8') - const afterReset = JSON.parse(afterResetRaw) as { plan?: unknown } + const afterReset = JSON.parse(afterResetRaw) as { plan?: unknown; plans?: unknown } expect(afterReset.plan).toBeUndefined() + expect(afterReset.plans).toBeUndefined() } finally { await rm(home, { recursive: true, force: true }) } - }) + }, CLI_PLAN_TIMEOUT_MS) + + it('resets one provider without removing other plans', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) + + try { + expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0) + expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0) + expect(runCli(['plan', 'reset', '--provider', 'codex'], home).status).toBe(0) + + const configPath = join(home, '.config', 'codeburn', 'config.json') + const configRaw = await readFile(configPath, 'utf-8') + const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string }; codex?: unknown } } + expect(config.plans?.claude?.id).toBe('claude-max') + expect(config.plans?.codex).toBeUndefined() + } finally { + await rm(home, { recursive: true, force: true }) + } + }, CLI_PLAN_TIMEOUT_MS) + + it('resets the all-provider plan without removing provider-specific plans', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) + + try { + expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0) + expect(runCli(['plan', 'reset', '--provider', 'all'], home).status).toBe(0) + + const configPath = join(home, '.config', 'codeburn', 'config.json') + const configRaw = await readFile(configPath, 'utf-8') + const config = JSON.parse(configRaw) as { plans?: { claude?: { id?: string }; all?: unknown } } + expect(config.plans?.claude?.id).toBe('claude-max') + expect(config.plans?.all).toBeUndefined() + } finally { + await rm(home, { recursive: true, force: true }) + } + }, CLI_PLAN_TIMEOUT_MS) + + it('shows all configured plans as json', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) + + try { + expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0) + expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0) + + const result = runCli(['plan', '--format', 'json'], home) + expect(result.status).toBe(0) + const payload = JSON.parse(result.stdout) as { id?: string; provider?: string; plans?: { claude?: { id?: string }; codex?: { id?: string } } } + expect(payload.id).toBe('claude-max') + expect(payload.provider).toBe('claude') + expect(payload.plans?.claude?.id).toBe('claude-max') + expect(payload.plans?.codex?.id).toBe('custom') + } finally { + await rm(home, { recursive: true, force: true }) + } + }, CLI_PLAN_TIMEOUT_MS) + + it('filters shown plans by provider', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) + + try { + expect(runCli(['plan', 'set', 'claude-max'], home).status).toBe(0) + expect(runCli(['plan', 'set', 'custom', '--monthly-usd', '200', '--provider', 'codex'], home).status).toBe(0) + + const result = runCli(['plan', '--format', 'json', '--provider', 'codex'], home) + expect(result.status).toBe(0) + const payload = JSON.parse(result.stdout) as { id?: string; provider?: string; plans?: unknown } + expect(payload.id).toBe('custom') + expect(payload.provider).toBe('codex') + expect(payload.plans).toMatchObject({ codex: { id: 'custom' } }) + } finally { + await rm(home, { recursive: true, force: true }) + } + }, CLI_PLAN_TIMEOUT_MS) + + it('rejects all-provider scope for preset plans', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) + + try { + const result = runCli(['plan', 'set', 'claude-max', '--provider', 'all'], home) + expect(result.status).toBe(1) + expect(result.stderr).toContain('omit --provider or use --provider claude') + } finally { + await rm(home, { recursive: true, force: true }) + } + }, CLI_PLAN_TIMEOUT_MS) it('shows invalid reset-day value in error output', async () => { const home = await mkdtemp(join(tmpdir(), 'codeburn-cli-plan-')) @@ -51,5 +146,5 @@ describe('codeburn plan command', () => { } finally { await rm(home, { recursive: true, force: true }) } - }) + }, CLI_PLAN_TIMEOUT_MS) }) diff --git a/tests/cli-status-menubar.test.ts b/tests/cli-status-menubar.test.ts new file mode 100644 index 00000000..1513b5c3 --- /dev/null +++ b/tests/cli-status-menubar.test.ts @@ -0,0 +1,108 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { spawnSync } from 'node:child_process' + +import { describe, expect, it } from 'vitest' + +function runCli(args: string[], home: string) { + return spawnSync(process.execPath, ['--import', 'tsx', 'src/cli.ts', ...args], { + cwd: process.cwd(), + env: { + ...process.env, + CLAUDE_CONFIG_DIR: join(home, '.claude'), + HOME: home, + TZ: 'UTC', + }, + encoding: 'utf-8', + timeout: 30_000, + }) +} + +function userLine(sessionId: string, timestamp: string): string { + return JSON.stringify({ + type: 'user', + sessionId, + timestamp, + message: { role: 'user', content: 'do the thing' }, + }) +} + +function assistantLine(sessionId: string, timestamp: string, messageId: string): string { + return JSON.stringify({ + type: 'assistant', + sessionId, + timestamp, + message: { + id: messageId, + type: 'message', + role: 'assistant', + model: 'claude-sonnet-4-5', + content: [ + { type: 'text', text: 'done' }, + { type: 'tool_use', id: 'tu-1', name: 'Edit', input: { file_path: '/tmp/x', old_string: 'a', new_string: 'b' } }, + ], + usage: { input_tokens: 500, output_tokens: 50 }, + }, + }) +} + +describe('codeburn status --format menubar-json', () => { + it('returns valid MenubarPayload with expected top-level fields', async () => { + const home = await mkdtemp(join(tmpdir(), 'codeburn-menubar-')) + + try { + const projectDir = join(home, '.claude', 'projects', 'myapp') + await mkdir(projectDir, { recursive: true }) + + const now = new Date() + const h = now.getUTCHours() + const base = h >= 2 ? new Date(now.getTime() - 2 * 3600_000) : new Date(now.getTime() - h * 3600_000 - 60_000) + const ts1 = base.toISOString().replace(/\.\d+Z$/, 'Z') + const ts2 = new Date(base.getTime() + 60_000).toISOString().replace(/\.\d+Z$/, 'Z') + const ts3 = new Date(base.getTime() + 120_000).toISOString().replace(/\.\d+Z$/, 'Z') + const ts4 = new Date(base.getTime() + 180_000).toISOString().replace(/\.\d+Z$/, 'Z') + + await writeFile( + join(projectDir, 'session.jsonl'), + [ + userLine('s1', ts1), + assistantLine('s1', ts2, 'msg-1'), + userLine('s1', ts3), + assistantLine('s1', ts4, 'msg-2'), + ].join('\n'), + ) + + const result = runCli([ + 'status', + '--format', 'menubar-json', + '--period', 'today', + '--provider', 'all', + '--no-optimize', + ], home) + + expect(result.status, `stderr: ${result.stderr}`).toBe(0) + + const payload = JSON.parse(result.stdout) as Record + + expect(payload).toHaveProperty('generated') + expect(payload).toHaveProperty('current') + expect(payload).toHaveProperty('optimize') + expect(payload).toHaveProperty('history') + + const current = payload['current'] as Record + expect(current['cost']).toBeGreaterThan(0) + expect(current['calls']).toBe(2) + expect(current['sessions']).toBe(1) + expect(current).toHaveProperty('oneShotRate') + expect(current).toHaveProperty('topActivities') + expect(current).toHaveProperty('topModels') + expect(current).toHaveProperty('providers') + + const history = payload['history'] as { daily: unknown[] } + expect(Array.isArray(history.daily)).toBe(true) + } finally { + await rm(home, { recursive: true, force: true }) + } + }) +}) diff --git a/tests/daily-cache.test.ts b/tests/daily-cache.test.ts index 5ec26612..2f384ccc 100644 --- a/tests/daily-cache.test.ts +++ b/tests/daily-cache.test.ts @@ -104,6 +104,36 @@ describe('loadDailyCache', () => { expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v2.bak'))).toBe(true) }) + it('discards a v5 cache because cached Claude costs predate 1-hour cache pricing', async () => { + const saved = { + version: 5, + lastComputedDate: '2026-05-01', + days: [{ + date: '2026-05-01', + cost: 0.37575, + calls: 1, + sessions: 1, + inputTokens: 0, + outputTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 60_120, + editTurns: 0, + oneShotTurns: 0, + models: { 'Opus 4.7': { calls: 1, cost: 0.37575, inputTokens: 0, outputTokens: 0, cacheReadTokens: 0, cacheWriteTokens: 60_120 } }, + categories: {}, + providers: { claude: { calls: 1, cost: 0.37575 } }, + }], + } + const { writeFile, mkdir } = await import('fs/promises') + await mkdir(TMP_CACHE_ROOT, { recursive: true }) + await writeFile(join(TMP_CACHE_ROOT, 'daily-cache.json'), JSON.stringify(saved), 'utf-8') + const cache = await loadDailyCache() + expect(cache.version).toBe(DAILY_CACHE_VERSION) + expect(cache.days).toEqual([]) + expect(cache.lastComputedDate).toBeNull() + expect(existsSync(join(TMP_CACHE_ROOT, 'daily-cache.json.v5.bak'))).toBe(true) + }) + it('round-trips a valid cache through save and load', async () => { const saved: DailyCache = { version: DAILY_CACHE_VERSION, diff --git a/tests/dashboard.test.ts b/tests/dashboard.test.ts index 0d36e2ed..da802f11 100644 --- a/tests/dashboard.test.ts +++ b/tests/dashboard.test.ts @@ -1,5 +1,8 @@ +import { homedir } from 'os' + import { describe, it, expect } from 'vitest' +import { shortProject } from '../src/dashboard.js' import { formatCost } from '../src/format.js' import type { ProjectSummary, SessionSummary } from '../src/types.js' @@ -53,7 +56,7 @@ function makeProject(name: string, sessions: SessionSummary[]): ProjectSummary { // Logic replicated from TopSessions component function getTopSessions(projects: ProjectSummary[], n = 5) { - const all = projects.flatMap(p => p.sessions.map(s => ({ ...s, projectName: p.project }))) + const all = projects.flatMap(p => p.sessions.map(s => ({ ...s, projectPath: p.projectPath }))) return [...all].sort((a, b) => b.totalCostUSD - a.totalCostUSD).slice(0, n) } @@ -99,6 +102,36 @@ describe('TopSessions - top-5 selection', () => { }) }) +describe('shortProject - path shortening', () => { + const home = homedir() + + it('preserves directory names containing dashes', () => { + expect(shortProject(`${home}/work/my-project`)).toBe('work/my-project') + }) + + it('preserves directory names containing dots', () => { + expect(shortProject(`${home}/work/my.app.io`)).toBe('work/my.app.io') + }) + + it('returns "home" for the home dir itself', () => { + expect(shortProject(home)).toBe('home') + }) + + it('does not strip a sibling whose name shares the home prefix', () => { + const sibling = `${home}-backup/proj` + expect(shortProject(sibling).endsWith('proj')).toBe(true) + expect(shortProject(sibling)).not.toMatch(/^-/) + }) + + it('keeps only the last 3 segments for deeply nested paths', () => { + expect(shortProject(`${home}/a/b/c/d/e/f`)).toBe('d/e/f') + }) + + it('handles paths outside the home dir', () => { + expect(shortProject('/opt/myproject')).toBe('opt/myproject') + }) +}) + describe('avg/s in ProjectBreakdown', () => { it('returns dash for a project with no sessions', () => { const project = makeProject('proj', []) diff --git a/tests/day-aggregator.test.ts b/tests/day-aggregator.test.ts index 9ca92390..c58937bb 100644 --- a/tests/day-aggregator.test.ts +++ b/tests/day-aggregator.test.ts @@ -46,8 +46,8 @@ describe('aggregateProjectsIntoDays', () => { sessions: [{ sessionId: 's1', project: 'p', - firstTimestamp: '2026-04-09T10:00:00Z', - lastTimestamp: '2026-04-10T08:00:00Z', + firstTimestamp: '2026-04-09T10:00:00', + lastTimestamp: '2026-04-10T08:00:00', totalCostUSD: 10, totalInputTokens: 0, totalOutputTokens: 0, @@ -57,14 +57,14 @@ describe('aggregateProjectsIntoDays', () => { turns: [ { userMessage: 'hi', - timestamp: '2026-04-09T10:00:00Z', + timestamp: '2026-04-09T10:00:00', sessionId: 's1', category: 'coding', retries: 0, hasEdits: true, assistantCalls: [ - makeCall('2026-04-09T10:00:00Z', 4), - makeCall('2026-04-10T08:00:00Z', 6), + makeCall('2026-04-09T10:00:00', 4), + makeCall('2026-04-10T08:00:00', 6), ], }, ], @@ -92,8 +92,8 @@ describe('aggregateProjectsIntoDays', () => { sessions: [{ sessionId: 's1', project: 'p', - firstTimestamp: '2026-04-09T10:00:00Z', - lastTimestamp: '2026-04-09T10:05:00Z', + firstTimestamp: '2026-04-09T10:00:00', + lastTimestamp: '2026-04-09T10:05:00', totalCostUSD: 3, totalInputTokens: 0, totalOutputTokens: 0, @@ -103,12 +103,12 @@ describe('aggregateProjectsIntoDays', () => { turns: [ { userMessage: 'hi', - timestamp: '2026-04-09T10:00:00Z', + timestamp: '2026-04-09T10:00:00', sessionId: 's1', category: 'coding', retries: 0, hasEdits: true, - assistantCalls: [makeCall('2026-04-09T10:00:00Z', 3)], + assistantCalls: [makeCall('2026-04-09T10:00:00', 3)], }, ], modelBreakdown: {}, @@ -138,8 +138,8 @@ describe('aggregateProjectsIntoDays', () => { sessions: [{ sessionId: 's1', project: 'p', - firstTimestamp: '2026-04-09T23:59:00Z', - lastTimestamp: '2026-04-10T00:10:00Z', + firstTimestamp: '2026-04-09T23:59:00', + lastTimestamp: '2026-04-10T00:10:00', totalCostUSD: 1, totalInputTokens: 0, totalOutputTokens: 0, totalCacheReadTokens: 0, totalCacheWriteTokens: 0, apiCalls: 0, @@ -151,7 +151,7 @@ describe('aggregateProjectsIntoDays', () => { }), ] const days = aggregateProjectsIntoDays(projects) - const expectedDate = dateKey('2026-04-09T23:59:00Z') + const expectedDate = dateKey('2026-04-09T23:59:00') expect(days[0]!.date).toBe(expectedDate) expect(days[0]!.sessions).toBe(1) }) @@ -162,18 +162,18 @@ describe('aggregateProjectsIntoDays', () => { sessions: [{ sessionId: 's1', project: 'p', - firstTimestamp: '2026-04-10T10:00:00Z', - lastTimestamp: '2026-04-10T10:00:00Z', + firstTimestamp: '2026-04-10T10:00:00', + lastTimestamp: '2026-04-10T10:00:00', totalCostUSD: 10, totalInputTokens: 0, totalOutputTokens: 0, totalCacheReadTokens: 0, totalCacheWriteTokens: 0, apiCalls: 2, turns: [ { - userMessage: 'x', timestamp: '2026-04-10T10:00:00Z', sessionId: 's1', + userMessage: 'x', timestamp: '2026-04-10T10:00:00', sessionId: 's1', category: 'coding', retries: 0, hasEdits: false, assistantCalls: [ - makeCall('2026-04-10T10:00:00Z', 7, 'Opus 4.7', 'claude'), - makeCall('2026-04-10T10:00:00Z', 3, 'gpt-5', 'codex'), + makeCall('2026-04-10T10:00:00', 7, 'Opus 4.7', 'claude'), + makeCall('2026-04-10T10:00:00', 3, 'gpt-5', 'codex'), ], }, ], diff --git a/tests/fs-utils.test.ts b/tests/fs-utils.test.ts index 6510900b..22640226 100644 --- a/tests/fs-utils.test.ts +++ b/tests/fs-utils.test.ts @@ -5,7 +5,6 @@ import { join } from 'path' import { MAX_SESSION_FILE_BYTES, - STREAM_THRESHOLD_BYTES, readSessionFile, readSessionLines, } from '../src/fs-utils.js' @@ -34,11 +33,12 @@ describe('readSessionFile', () => { expect(await readSessionFile(p)).toBe('hello\nworld\n') }) - it('returns content for files at the stream threshold via stream path', async () => { - const p = await tmpPath(Buffer.alloc(STREAM_THRESHOLD_BYTES, 'a')) + it('returns content for large files under the full-file cap', async () => { + const size = 8 * 1024 * 1024 + const p = await tmpPath(Buffer.alloc(size, 'a')) const got = await readSessionFile(p) expect(got).not.toBeNull() - expect(got!.length).toBe(STREAM_THRESHOLD_BYTES) + expect(got!.length).toBe(size) }) it('returns null and skips files over the cap', async () => { @@ -88,6 +88,28 @@ describe('readSessionLines', () => { expect(lines).toEqual(['line1', 'line2', 'line3']) }) + it('skips old large lines before materializing the full line', async () => { + const oldLine = `{"type":"assistant","timestamp":"2026-01-01T00:00:00Z","payload":"${'x'.repeat(100_000)}"}` + const newLine = '{"type":"assistant","timestamp":"2026-05-01T00:00:00Z"}' + const p = await tmpPath(`${oldLine}\n${newLine}\n`) + const lines: string[] = [] + for await (const line of readSessionLines(p, head => head.includes('2026-01-01'))) { + lines.push(line) + } + expect(lines).toEqual([newLine]) + }) + + it('yields large lines as Buffers when requested', async () => { + const largeLine = `{"type":"assistant","timestamp":"2026-05-01T00:00:00Z","payload":"${'x'.repeat(100_000)}"}` + const p = await tmpPath(`${largeLine}\nsmall\n`) + const lines: Array = [] + for await (const line of readSessionLines(p, undefined, { largeLineAsBuffer: true })) { + lines.push(line) + } + expect(Buffer.isBuffer(lines[0])).toBe(true) + expect(lines[1]).toBe('small') + }) + it('does not leak file descriptors when generator is abandoned early', async () => { const content = Array.from({ length: 1000 }, (_, i) => `line-${i}`).join('\n') const p = await tmpPath(content) @@ -95,4 +117,56 @@ describe('readSessionLines', () => { await gen.next() await gen.return(undefined) }) + + it('reads from startByteOffset, yielding only lines after the offset', async () => { + const content = 'line1\nline2\nline3\n' + const p = await tmpPath(content) + const offset = Buffer.byteLength('line1\n') + const lines: string[] = [] + for await (const line of readSessionLines(p, undefined, { startByteOffset: offset })) { + lines.push(line) + } + expect(lines).toEqual(['line2', 'line3']) + }) + + it('byteOffsetTracker tracks position after last complete newline', async () => { + const content = 'aaa\nbbb\nccc\n' + const p = await tmpPath(content) + const tracker = { lastCompleteLineOffset: 0 } + const lines: string[] = [] + for await (const line of readSessionLines(p, undefined, { byteOffsetTracker: tracker })) { + lines.push(line) + } + expect(lines).toEqual(['aaa', 'bbb', 'ccc']) + expect(tracker.lastCompleteLineOffset).toBe(Buffer.byteLength(content)) + }) + + it('byteOffsetTracker accounts for startByteOffset', async () => { + const content = 'line1\nline2\nline3\n' + const p = await tmpPath(content) + const offset = Buffer.byteLength('line1\n') + const tracker = { lastCompleteLineOffset: 0 } + for await (const _line of readSessionLines(p, undefined, { startByteOffset: offset, byteOffsetTracker: tracker })) {} + expect(tracker.lastCompleteLineOffset).toBe(Buffer.byteLength(content)) + }) + + it('byteOffsetTracker excludes trailing partial line (no final newline)', async () => { + const content = 'line1\nline2\npartial' + const p = await tmpPath(content) + const tracker = { lastCompleteLineOffset: 0 } + for await (const _line of readSessionLines(p, undefined, { byteOffsetTracker: tracker })) {} + expect(tracker.lastCompleteLineOffset).toBe(Buffer.byteLength('line1\nline2\n')) + }) + + it('byteOffsetTracker updates for skipped lines too', async () => { + const content = 'skip-me\nkeep-me\n' + const p = await tmpPath(content) + const tracker = { lastCompleteLineOffset: 0 } + const lines: string[] = [] + for await (const line of readSessionLines(p, head => head.includes('skip-me'), { byteOffsetTracker: tracker })) { + lines.push(line) + } + expect(lines).toEqual(['keep-me']) + expect(tracker.lastCompleteLineOffset).toBe(Buffer.byteLength(content)) + }) }) diff --git a/tests/menubar-installer.test.ts b/tests/menubar-installer.test.ts new file mode 100644 index 00000000..a37cdab2 --- /dev/null +++ b/tests/menubar-installer.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest' +import { + resolveLatestMenubarReleaseAssets, + resolveMenubarReleaseAssets, + type ReleaseResponse, +} from '../src/menubar-installer.js' + +function asset(name: string) { + return { name, browser_download_url: `https://example.test/${name}` } +} + +describe('resolveMenubarReleaseAssets', () => { + it('ignores dev zips and pairs the checksum with the versioned zip', () => { + const release: ReleaseResponse = { + tag_name: 'mac-v0.9.8', + assets: [ + asset('CodeBurnMenubar-dev.zip'), + asset('CodeBurnMenubar-dev.zip.sha256'), + asset('CodeBurnMenubar-v0.9.8.zip'), + asset('CodeBurnMenubar-v0.9.8.zip.sha256'), + ], + } + + const resolved = resolveMenubarReleaseAssets(release) + + expect(resolved.zip.name).toBe('CodeBurnMenubar-v0.9.8.zip') + expect(resolved.checksum?.name).toBe('CodeBurnMenubar-v0.9.8.zip.sha256') + }) + + it('fails when a release only contains dev assets', () => { + const release: ReleaseResponse = { + tag_name: 'mac-v0.9.8', + assets: [ + asset('CodeBurnMenubar-dev.zip'), + asset('CodeBurnMenubar-dev.zip.sha256'), + ], + } + + expect(() => resolveMenubarReleaseAssets(release)).toThrow(/versioned zip/) + }) + + it('fails when the versioned checksum is missing', () => { + const release: ReleaseResponse = { + tag_name: 'mac-v0.9.8', + assets: [ + asset('CodeBurnMenubar-v0.9.8.zip'), + ], + } + + expect(() => resolveMenubarReleaseAssets(release)).toThrow(/Missing checksum/) + }) + + it('selects the newest mac release instead of the newest repo release', () => { + const releases: ReleaseResponse[] = [ + { + tag_name: 'v0.9.9', + assets: [ + asset('codeburn-0.9.9.tgz'), + ], + }, + { + tag_name: 'mac-v0.9.8', + assets: [ + asset('CodeBurnMenubar-v0.9.8.zip'), + asset('CodeBurnMenubar-v0.9.8.zip.sha256'), + ], + }, + ] + + const resolved = resolveLatestMenubarReleaseAssets(releases) + + expect(resolved.release.tag_name).toBe('mac-v0.9.8') + expect(resolved.zip.name).toBe('CodeBurnMenubar-v0.9.8.zip') + }) +}) diff --git a/tests/models-hoist.test.ts b/tests/models-hoist.test.ts index 13af3e57..324e6ff6 100644 --- a/tests/models-hoist.test.ts +++ b/tests/models-hoist.test.ts @@ -50,6 +50,10 @@ const KNOWN_NAMES = [ 'kiro-auto', 'cline-auto', 'qwen-auto', + 'kimi-auto', + 'kimi-for-coding', + 'kimi-k2-thinking-turbo', + 'kimi-k2.6', 'o3', 'o4-mini', 'deepseek-coder', @@ -86,6 +90,14 @@ describe('post-hoist resolution stability', () => { expect(getShortModelName('claude-3-5-haiku')).toBe('Haiku 3.5') }) + it('kimi managed aliases resolve to priced Kimi models', () => { + expect(getShortModelName('kimi-auto')).toBe('Kimi (auto)') + expect(getShortModelName('kimi-for-coding')).toBe('Kimi K2 Thinking') + expect(getShortModelName('kimi-k2-thinking-turbo')).toBe('Kimi K2 Thinking Turbo') + expect(getShortModelName('kimi-k2.6')).toBe('Kimi K2.6') + expect(getModelCosts('kimi-auto')?.inputCostPerToken).toBeGreaterThan(0) + }) + it('getModelCosts returns positive token costs for every known name', () => { for (const name of KNOWN_NAMES) { const c = getModelCosts(name) diff --git a/tests/models.test.ts b/tests/models.test.ts index 9fdf87bf..41ccb5e8 100644 --- a/tests/models.test.ts +++ b/tests/models.test.ts @@ -158,6 +158,18 @@ describe('calculateCost - OMP names produce non-zero cost', () => { }) }) +describe('calculateCost - Claude cache write durations', () => { + it('prices 1-hour cache writes at 1.6x the 5-minute cache write rate', () => { + const fiveMinute = calculateCost('claude-opus-4-7', 0, 0, 1_000_000, 0, 0) + const oneHour = calculateCost('claude-opus-4-7', 0, 0, 1_000_000, 0, 0, 'standard', 1_000_000) + const mixed = calculateCost('claude-opus-4-7', 0, 0, 100_000, 0, 0, 'standard', 60_000) + + expect(fiveMinute).toBeCloseTo(6.25, 6) + expect(oneHour).toBeCloseTo(10, 6) + expect(mixed).toBeCloseTo(0.85, 6) + }) +}) + describe('existing model names still resolve', () => { it('canonical claude-opus-4-6', () => { expect(getModelCosts('claude-opus-4-6')).not.toBeNull() diff --git a/tests/optimize-fs.test.ts b/tests/optimize-fs.test.ts index 4ec41de1..29d583e1 100644 --- a/tests/optimize-fs.test.ts +++ b/tests/optimize-fs.test.ts @@ -325,7 +325,7 @@ describe('scanJsonlFile', () => { message: { content: [{ type: 'tool_use', name: 'Bash', input: {} }] }, })) await scanJsonlFile(filePath, 'p1', undefined) - expect(readSessionLinesSpy).toHaveBeenCalledWith(filePath) + expect(readSessionLinesSpy).toHaveBeenCalledWith(filePath, undefined, { largeLineAsBuffer: true }) expect(readSessionFileSpy).not.toHaveBeenCalled() readSessionLinesSpy.mockRestore() readSessionFileSpy.mockRestore() diff --git a/tests/parser-claude-cwd.test.ts b/tests/parser-claude-cwd.test.ts index 65c96db0..179ad7cb 100644 --- a/tests/parser-claude-cwd.test.ts +++ b/tests/parser-claude-cwd.test.ts @@ -31,7 +31,14 @@ function dayRange(day: string): DateRange { } } -async function writeClaudeSession(projectSlug: string, sessionId: string, cwd: string, timestamp: string): Promise { +async function writeClaudeSession( + projectSlug: string, + sessionId: string, + cwd: string, + timestamp: string, + usage: Record = { input_tokens: 100, output_tokens: 50 }, + model = 'claude-sonnet-4-5', +): Promise { const projectDir = join(tmpDir, 'projects', projectSlug) await mkdir(projectDir, { recursive: true }) const filePath = join(projectDir, `${sessionId}.jsonl`) @@ -44,12 +51,9 @@ async function writeClaudeSession(projectSlug: string, sessionId: string, cwd: s id: `msg-${sessionId}`, type: 'message', role: 'assistant', - model: 'claude-sonnet-4-5', + model, content: [], - usage: { - input_tokens: 100, - output_tokens: 50, - }, + usage, }, }) + '\n') @@ -158,3 +162,51 @@ describe('Claude cwd project paths', () => { expect(projects[0]!.projectPath).toBe('fallback/slug') }) }) + +describe('Claude cache creation pricing', () => { + it('prices 1-hour cache writes from usage.cache_creation at the 2x input rate', async () => { + await writeClaudeSession( + 'cache-pricing', + 'one-hour-cache', + '/tmp/cache-pricing', + '2099-05-05T10:00:00.000Z', + { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 60_120, + cache_creation: { + ephemeral_5m_input_tokens: 0, + ephemeral_1h_input_tokens: 60_120, + }, + }, + 'claude-opus-4-7', + ) + + const projects = await parseAllSessions(dayRange('2099-05-05'), 'claude') + + expect(projects).toHaveLength(1) + expect(projects[0]!.sessions[0]!.totalCacheWriteTokens).toBe(60_120) + expect(projects[0]!.totalCostUSD).toBeCloseTo(0.6012, 6) + }) + + it('falls back to the legacy 5-minute cache write rate when split fields are absent', async () => { + await writeClaudeSession( + 'legacy-cache-pricing', + 'legacy-cache', + '/tmp/legacy-cache-pricing', + '2099-05-06T10:00:00.000Z', + { + input_tokens: 0, + output_tokens: 0, + cache_creation_input_tokens: 60_120, + }, + 'claude-opus-4-7', + ) + + const projects = await parseAllSessions(dayRange('2099-05-06'), 'claude') + + expect(projects).toHaveLength(1) + expect(projects[0]!.sessions[0]!.totalCacheWriteTokens).toBe(60_120) + expect(projects[0]!.totalCostUSD).toBeCloseTo(0.37575, 6) + }) +}) diff --git a/tests/parser-compact-entry.test.ts b/tests/parser-compact-entry.test.ts new file mode 100644 index 00000000..6777d948 --- /dev/null +++ b/tests/parser-compact-entry.test.ts @@ -0,0 +1,434 @@ +import { describe, it, expect } from 'vitest' + +import { compactEntry } from '../src/parser.js' +import type { JournalEntry } from '../src/types.js' + +function entry(overrides: Partial & Record): JournalEntry { + return { type: 'user', ...overrides } as JournalEntry +} + +describe('compactEntry', () => { + it('preserves type, timestamp, sessionId, cwd', () => { + const raw = entry({ type: 'user', timestamp: 't1', sessionId: 's1', cwd: '/foo' }) + const c = compactEntry(raw) + expect(c.type).toBe('user') + expect(c.timestamp).toBe('t1') + expect(c.sessionId).toBe('s1') + expect(c.cwd).toBe('/foo') + }) + + it('strips unknown catch-all fields', () => { + const raw = entry({ + type: 'assistant', + toolResult: { type: 'tool_result', content: 'x'.repeat(10_000) }, + someHugeField: 'y'.repeat(10_000), + }) + const c = compactEntry(raw) + expect((c as Record)['toolResult']).toBeUndefined() + expect((c as Record)['someHugeField']).toBeUndefined() + }) + + it('preserves deferred_tools_delta attachment with copied names', () => { + const raw = entry({ + type: 'attachment', + attachment: { + type: 'deferred_tools_delta', + addedNames: ['mcp__svc__t1', 'Bash'], + extraData: 'should be dropped', + }, + }) + const c = compactEntry(raw) + const att = (c as Record)['attachment'] as Record + expect(att['type']).toBe('deferred_tools_delta') + expect(att['addedNames']).toEqual(['mcp__svc__t1', 'Bash']) + expect(att['extraData']).toBeUndefined() + }) + + it('copies addedNames into a new array (not by reference)', () => { + const originalNames = ['mcp__a__b', 'Bash'] + const raw = entry({ + type: 'attachment', + attachment: { type: 'deferred_tools_delta', addedNames: originalNames }, + }) + const c = compactEntry(raw) + const att = (c as Record)['attachment'] as { addedNames: string[] } + expect(att.addedNames).not.toBe(originalNames) + expect(att.addedNames).toEqual(originalNames) + }) + + it('caps addedNames at 1000 entries', () => { + const names = Array.from({ length: 2000 }, (_, i) => `mcp__svc__t${i}`) + const raw = entry({ + type: 'attachment', + attachment: { type: 'deferred_tools_delta', addedNames: names }, + }) + const c = compactEntry(raw) + const att = (c as Record)['attachment'] as { addedNames: string[] } + expect(att.addedNames).toHaveLength(1000) + }) + + it('filters non-string entries from addedNames', () => { + const raw = entry({ + type: 'attachment', + attachment: { type: 'deferred_tools_delta', addedNames: [42, null, 'mcp__a__b', undefined] }, + }) + const c = compactEntry(raw) + const att = (c as Record)['attachment'] as { addedNames: string[] } + expect(att.addedNames).toEqual(['mcp__a__b']) + }) + + it('drops non-deferred_tools_delta attachments', () => { + const raw = entry({ + type: 'attachment', + attachment: { type: 'other', data: 'x'.repeat(10_000) }, + }) + const c = compactEntry(raw) + expect((c as Record)['attachment']).toBeUndefined() + }) + + it('caps user message string content at 2000', () => { + const longText = 'a'.repeat(5000) + const raw = entry({ + type: 'user', + message: { role: 'user' as const, content: longText }, + }) + const c = compactEntry(raw) + expect(c.message!.role).toBe('user') + const content = (c.message as { content: string }).content + expect(content.length).toBe(2000) + }) + + it('caps total user text across all blocks at 2000', () => { + const raw = entry({ + type: 'user', + message: { + role: 'user' as const, + content: [ + { type: 'text' as const, text: 'a'.repeat(1500) }, + { type: 'text' as const, text: 'b'.repeat(1500) }, + { type: 'text' as const, text: 'c'.repeat(1500) }, + { type: 'image' as const, source: 'big data' }, + ], + }, + }) + const c = compactEntry(raw) + const content = (c.message as { content: Array<{ type: string; text: string }> }).content + expect(content).toHaveLength(2) + expect(content[0]!.text.length).toBe(1500) + expect(content[1]!.text.length).toBe(500) + }) + + it('compacts assistant tool_use blocks, dropping text and thinking, preserving id', () => { + const raw = entry({ + type: 'assistant', + timestamp: 't1', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + id: 'msg_123', + usage: { input_tokens: 100, output_tokens: 200 }, + content: [ + { type: 'text', text: 'x'.repeat(50_000) }, + { type: 'thinking', thinking: 'y'.repeat(50_000) }, + { type: 'tool_use', id: 'tu1', name: 'Read', input: { file_path: '/foo', huge: 'z'.repeat(10_000) } }, + { type: 'tool_use', id: 'tu2', name: 'Edit', input: { old_string: 'a'.repeat(5000), new_string: 'b'.repeat(5000) } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ type: string; id?: string; name?: string; input?: Record }> } + expect(msg.content).toHaveLength(2) + expect(msg.content[0]!.name).toBe('Read') + expect(msg.content[0]!.id).toBe('tu1') + expect(msg.content[0]!.input).toEqual({ file_path: '/foo' }) + expect(msg.content[1]!.name).toBe('Edit') + expect(msg.content[1]!.id).toBe('tu2') + expect(msg.content[1]!.input).toEqual({}) + }) + + it('caps tool_use blocks at 500 per message', () => { + const blocks = Array.from({ length: 600 }, (_, i) => ({ + type: 'tool_use' as const, + id: `tu${i}`, + name: `Tool${i}`, + input: {}, + })) + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: blocks, + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: unknown[] } + expect(msg.content).toHaveLength(500) + }) + + it('preserves model, usage (destructured), and id on assistant messages', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + id: 'msg_abc', + usage: { + input_tokens: 50, + output_tokens: 100, + cache_read_input_tokens: 25, + extraGarbage: 'should not survive', + }, + content: [], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { model: string; id: string; usage: Record } + expect(msg.model).toBe('claude-opus-4-6') + expect(msg.id).toBe('msg_abc') + expect(msg.usage['input_tokens']).toBe(50) + expect(msg.usage['output_tokens']).toBe(100) + expect(msg.usage['cache_read_input_tokens']).toBe(25) + expect(msg.usage['extraGarbage']).toBeUndefined() + }) + + it('deep-copies usage nested objects, stripping extra keys', () => { + const cacheCreation = { ephemeral_5m_input_tokens: 100, ephemeral_1h_input_tokens: 200, extraJunk: 'big' } + const serverToolUse = { web_search_requests: 3, web_fetch_requests: 1, extraJunk: 'big' } + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { + input_tokens: 10, + output_tokens: 10, + speed: 'fast', + cache_creation: cacheCreation, + server_tool_use: serverToolUse, + }, + content: [], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { usage: Record } + expect(msg.usage['speed']).toBe('fast') + const cc = msg.usage['cache_creation'] as Record + expect(cc['ephemeral_5m_input_tokens']).toBe(100) + expect(cc['ephemeral_1h_input_tokens']).toBe(200) + expect(cc['extraJunk']).toBeUndefined() + expect(cc).not.toBe(cacheCreation) + const stu = msg.usage['server_tool_use'] as Record + expect(stu['web_search_requests']).toBe(3) + expect(stu['web_fetch_requests']).toBe(1) + expect(stu['extraJunk']).toBeUndefined() + expect(stu).not.toBe(serverToolUse) + }) + + it('keeps Skill input.skill and input.name, type-checked and capped', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [ + { type: 'tool_use', id: 'tu', name: 'Skill', input: { skill: 'graphify', args: 'huge arg data' } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ input: Record }> } + expect(msg.content[0]!.input['skill']).toBe('graphify') + expect(msg.content[0]!.input['args']).toBeUndefined() + }) + + it('rejects non-string Skill input.skill and caps long names', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [ + { type: 'tool_use', id: 'tu1', name: 'Skill', input: { skill: { malicious: 'x'.repeat(10_000) } } }, + { type: 'tool_use', id: 'tu2', name: 'Skill', input: { skill: 'a'.repeat(500) } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ input: Record }> } + expect(msg.content[0]!.input['skill']).toBeUndefined() + expect((msg.content[1]!.input['skill'] as string).length).toBe(200) + }) + + it('keeps Bash input.command capped at 2000 for bash command extraction', () => { + const longCmd = 'npm run build && '.repeat(200) + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [ + { type: 'tool_use', id: 'tu', name: 'Bash', input: { command: longCmd, description: 'big desc' } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ input: Record }> } + const cmd = msg.content[0]!.input['command'] as string + expect(cmd.length).toBe(2000) + expect(msg.content[0]!.input['description']).toBeUndefined() + }) + + it('keeps Read file_path capped and drops unrelated input fields', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [ + { type: 'tool_use', id: 'tu', name: 'Read', input: { file_path: '/tmp/' + 'x'.repeat(3000), content: 'big' } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ input: Record }> } + expect((msg.content[0]!.input['file_path'] as string).length).toBe(2000) + expect(msg.content[0]!.input['content']).toBeUndefined() + }) + + it('keeps Agent subagent_type capped and drops prompt text', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [ + { type: 'tool_use', id: 'tu', name: 'Agent', input: { subagent_type: 'reviewer'.repeat(50), prompt: 'big' } }, + ], + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ input: Record }> } + expect((msg.content[0]!.input['subagent_type'] as string).length).toBe(200) + expect(msg.content[0]!.input['prompt']).toBeUndefined() + }) + + it('handles entry with no message field', () => { + const raw = entry({ type: 'system', timestamp: 't1', cwd: '/x' }) + const c = compactEntry(raw) + expect(c.type).toBe('system') + expect(c.timestamp).toBe('t1') + expect(c.message).toBeUndefined() + }) + + it('handles assistant message with no usage (non-standard)', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + content: [{ type: 'text', text: 'response' }], + }, + }) + const c = compactEntry(raw) + expect(c.message).toBeUndefined() + }) + + it('handles unexpected message role (neither user nor assistant)', () => { + const raw = entry({ + type: 'system', + message: { role: 'system' as never, content: 'sys prompt' }, + }) + const c = compactEntry(raw) + expect(c.message).toBeUndefined() + }) + + it('tolerates null elements in user content array', () => { + const raw = entry({ + type: 'user', + message: { + role: 'user' as const, + content: [null, undefined, { type: 'text', text: 'ok' }, 42, { type: 'text' }] as never, + }, + }) + const c = compactEntry(raw) + const content = (c.message as { content: Array<{ text: string }> }).content + expect(content).toHaveLength(1) + expect(content[0]!.text).toBe('ok') + }) + + it('tolerates assistant content that is not an array', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: 'not an array' as never, + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: unknown[] } + expect(msg.content).toEqual([]) + }) + + it('tolerates null elements in assistant content array', () => { + const raw = entry({ + type: 'assistant', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + usage: { input_tokens: 10, output_tokens: 10 }, + content: [null, { type: 'tool_use', id: 'tu1', name: 'Read', input: {} }, undefined] as never, + }, + }) + const c = compactEntry(raw) + const msg = c.message as { content: Array<{ name: string }> } + expect(msg.content).toHaveLength(1) + expect(msg.content[0]!.name).toBe('Read') + }) + + it('memory reduction: compacted entry is much smaller than raw', () => { + const hugeContent = Array.from({ length: 20 }, (_, i) => ({ + type: i % 2 === 0 ? 'text' : 'tool_result', + text: 'x'.repeat(100_000), + content: 'y'.repeat(100_000), + })) + const raw = entry({ + type: 'assistant', + timestamp: '2026-01-01T00:00:00', + message: { + type: 'message' as const, + role: 'assistant' as const, + model: 'claude-opus-4-6', + id: 'msg_1', + usage: { input_tokens: 1000, output_tokens: 500 }, + content: hugeContent as never, + }, + toolResult: { content: 'z'.repeat(500_000) }, + }) + const rawSize = JSON.stringify(raw).length + const compacted = compactEntry(raw) + const compactedSize = JSON.stringify(compacted).length + expect(rawSize).toBeGreaterThan(2_000_000) + expect(compactedSize).toBeLessThan(500) + }) +}) diff --git a/tests/parser-large-json-scanner.test.ts b/tests/parser-large-json-scanner.test.ts new file mode 100644 index 00000000..af0668b0 --- /dev/null +++ b/tests/parser-large-json-scanner.test.ts @@ -0,0 +1,87 @@ +import { describe, expect, it } from 'vitest' + +import { parseJsonlLine } from '../src/parser.js' + +function largeUserLine(): string { + return JSON.stringify({ + type: 'user', + sessionId: 's1', + timestamp: '2026-05-01T00:00:00Z', + cwd: '/repo', + message: { + role: 'user', + content: [ + { type: 'image', source: { data: 'x'.repeat(40_000) } }, + { type: 'text', text: 'hello ' + 'a'.repeat(3000) }, + ], + }, + }) +} + +function largeAssistantLine(): string { + return JSON.stringify({ + type: 'assistant', + sessionId: 's1', + timestamp: '2026-05-01T00:00:01Z', + cwd: '/repo', + message: { + id: 'm1', + type: 'message', + role: 'assistant', + model: 'claude-sonnet-4-5', + content: [ + { type: 'text', text: 'x'.repeat(40_000) }, + { type: 'tool_use', id: 'read1', name: 'Read', input: { file_path: '/tmp/file.ts', content: 'drop me' } }, + { type: 'tool_use', id: 'agent1', name: 'Agent', input: { subagent_type: 'reviewer', prompt: 'drop me' } }, + ], + usage: { + input_tokens: 100, + output_tokens: 20, + cache_read_input_tokens: 300, + }, + }, + }) +} + +describe('large JSONL compact scanner', () => { + it('extracts user text from array content without full JSON.parse', () => { + const parsed = parseJsonlLine(largeUserLine()) + expect(parsed?.type).toBe('user') + const content = parsed?.message?.role === 'user' ? parsed.message.content : '' + expect(content).toBeTypeOf('string') + expect((content as string).startsWith('hello ')).toBe(true) + expect((content as string).length).toBe(2000) + }) + + it('extracts capped tool inputs needed by optimize', () => { + const parsed = parseJsonlLine(Buffer.from(largeAssistantLine())) + const msg = parsed?.message + expect(msg?.role).toBe('assistant') + if (msg?.role !== 'assistant') return + expect(msg.usage.input_tokens).toBe(100) + expect(msg.usage.output_tokens).toBe(20) + expect(msg.usage.cache_read_input_tokens).toBe(300) + expect(msg.content).toEqual([ + { type: 'tool_use', id: 'read1', name: 'Read', input: { file_path: '/tmp/file.ts' } }, + { type: 'tool_use', id: 'agent1', name: 'Agent', input: { subagent_type: 'reviewer' } }, + ]) + }) + + it('extracts deferred MCP inventory from large attachment lines', () => { + const line = JSON.stringify({ + type: 'attachment', + sessionId: 's1', + timestamp: '2026-05-01T00:00:02Z', + padding: 'x'.repeat(40_000), + attachment: { + type: 'deferred_tools_delta', + addedNames: ['Bash', 'mcp__svc__tool'], + }, + }) + const parsed = parseJsonlLine(Buffer.from(line)) as Record + expect(parsed['attachment']).toEqual({ + type: 'deferred_tools_delta', + addedNames: ['Bash', 'mcp__svc__tool'], + }) + }) +}) diff --git a/tests/parser-large-session.test.ts b/tests/parser-large-session.test.ts new file mode 100644 index 00000000..44d3d7b7 --- /dev/null +++ b/tests/parser-large-session.test.ts @@ -0,0 +1,180 @@ +import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + +import { describe, expect, it, beforeEach, afterEach } from 'vitest' + +import { parseAllSessions, clearSessionCache } from '../src/parser.js' +import type { DateRange } from '../src/types.js' + +let home: string + +beforeEach(async () => { + home = await mkdtemp(join(tmpdir(), 'codeburn-large-')) + process.env['CLAUDE_CONFIG_DIR'] = join(home, '.claude') +}) + +afterEach(async () => { + clearSessionCache() + delete process.env['CLAUDE_CONFIG_DIR'] + await rm(home, { recursive: true, force: true }) +}) + +function userLine(sessionId: string, timestamp: string, textSize = 100): string { + return JSON.stringify({ + type: 'user', + sessionId, + timestamp, + cwd: '/projects/app', + message: { role: 'user', content: 'x'.repeat(textSize) }, + }) +} + +function assistantLine(sessionId: string, timestamp: string, messageId: string, opts?: { + contentSize?: number + toolCount?: number +}): string { + const contentSize = opts?.contentSize ?? 0 + const toolCount = opts?.toolCount ?? 1 + const content: unknown[] = [] + if (contentSize > 0) { + content.push({ type: 'text', text: 'y'.repeat(contentSize) }) + content.push({ type: 'thinking', thinking: 'z'.repeat(contentSize) }) + } + for (let i = 0; i < toolCount; i++) { + content.push({ + type: 'tool_use', + id: `tu-${i}`, + name: i === 0 ? 'Edit' : 'Read', + input: { file_path: '/tmp/x', big: 'w'.repeat(contentSize) }, + }) + } + return JSON.stringify({ + type: 'assistant', + sessionId, + timestamp, + message: { + id: messageId, + type: 'message', + role: 'assistant', + model: 'claude-sonnet-4-5', + content, + usage: { input_tokens: 1000, output_tokens: 100 }, + }, + }) +} + +function messageFirstLargeAssistantLine(sessionId: string, timestamp: string, messageId: string): string { + const hugeText = 'y'.repeat(3_000_000) + return `{"parentUuid":"u1","isSidechain":false,"message":{"model":"claude-sonnet-4-5","id":"${messageId}","type":"message","role":"assistant","content":[{"type":"text","text":"${hugeText}"},{"type":"tool_use","id":"tu-large","name":"Edit","input":{"file_path":"/tmp/x","old_string":"a","new_string":"b"}}],"usage":{"input_tokens":1000,"output_tokens":100,"cache_read_input_tokens":5000}},"uuid":"a1","timestamp":"${timestamp}","type":"assistant","sessionId":"${sessionId}","cwd":"/projects/app"}` +} + +function attachmentLine(sessionId: string, timestamp: string): string { + return JSON.stringify({ + type: 'attachment', + sessionId, + timestamp, + attachment: { + type: 'deferred_tools_delta', + addedNames: ['Bash', 'Edit', 'Read', 'mcp__hf__hub_search'], + }, + }) +} + +describe('parseAllSessions with large Claude fixture', () => { + it('correctly parses sessions with bulky text/thinking/tool_result blocks', async () => { + const projectDir = join(home, '.claude', 'projects', 'bigapp') + await mkdir(projectDir, { recursive: true }) + + const lines: string[] = [] + lines.push(attachmentLine('s1', '2026-04-10T09:00:00Z')) + for (let i = 0; i < 50; i++) { + const ts = `2026-04-10T${String(9 + Math.floor(i / 10)).padStart(2, '0')}:${String((i % 10) * 5).padStart(2, '0')}:00Z` + lines.push(userLine('s1', ts, 5000)) + lines.push(assistantLine('s1', ts.replace(':00Z', ':30Z'), `msg-${i}`, { + contentSize: 50_000, + toolCount: 3, + })) + } + + await writeFile(join(projectDir, 'session.jsonl'), lines.join('\n')) + + const range: DateRange = { + start: new Date('2026-04-10T00:00:00Z'), + end: new Date('2026-04-10T23:59:59Z'), + } + + const projects = await parseAllSessions(range, 'claude') + + expect(projects.length).toBeGreaterThan(0) + const proj = projects[0]! + expect(proj.totalApiCalls).toBe(50) + expect(proj.totalCostUSD).toBeGreaterThan(0) + + const sess = proj.sessions[0]! + expect(sess.turns.length).toBe(50) + + for (const turn of sess.turns) { + expect(turn.userMessage.length).toBeLessThanOrEqual(2000) + expect(turn.assistantCalls.length).toBe(1) + const call = turn.assistantCalls[0]! + expect(call.tools).toContain('Edit') + expect(call.tools).toContain('Read') + expect(call.model).toBe('claude-sonnet-4-5') + } + + expect(sess.mcpInventory).toContain('mcp__hf__hub_search') + }) + + it('handles malformed JSONL lines without crashing', async () => { + const projectDir = join(home, '.claude', 'projects', 'baddata') + await mkdir(projectDir, { recursive: true }) + + const lines = [ + 'not json at all', + '{"type": "user", "sessionId": "s1", "timestamp": "2026-04-10T10:00:00Z", "message": {"role": "user", "content": [null, {"type": "text", "text": "hello"}, 42]}}', + '{"type": "assistant", "sessionId": "s1", "timestamp": "2026-04-10T10:01:00Z", "message": {"id": "m1", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": "not-an-array", "usage": {"input_tokens": 100, "output_tokens": 50}}}', + '{"type": "assistant", "sessionId": "s1", "timestamp": "2026-04-10T10:02:00Z", "message": {"id": "m2", "type": "message", "role": "assistant", "model": "claude-sonnet-4-5", "content": [null, {"type": "tool_use", "id": "t1", "name": "Read", "input": {}}], "usage": {"input_tokens": 100, "output_tokens": 50}}}', + ] + + await writeFile(join(projectDir, 'session.jsonl'), lines.join('\n')) + + const range: DateRange = { + start: new Date('2026-04-10T00:00:00Z'), + end: new Date('2026-04-10T23:59:59Z'), + } + + const projects = await parseAllSessions(range, 'claude') + expect(projects.length).toBeGreaterThan(0) + + const sess = projects[0]!.sessions[0]! + expect(sess.apiCalls).toBeGreaterThanOrEqual(1) + }) + + it('parses huge message-first assistant lines without full JSON.parse expansion', async () => { + const projectDir = join(home, '.claude', 'projects', 'messagefirst') + await mkdir(projectDir, { recursive: true }) + + const lines = [ + userLine('s1', '2026-04-10T10:00:00Z', 100), + messageFirstLargeAssistantLine('s1', '2026-04-10T10:00:01Z', 'msg-large'), + ] + + await writeFile(join(projectDir, 'session.jsonl'), lines.join('\n')) + + const range: DateRange = { + start: new Date('2026-04-10T00:00:00Z'), + end: new Date('2026-04-10T23:59:59Z'), + } + + const projects = await parseAllSessions(range, 'claude') + expect(projects.length).toBeGreaterThan(0) + + const sess = projects[0]!.sessions[0]! + expect(sess.apiCalls).toBe(1) + expect(sess.totalInputTokens).toBe(1000) + expect(sess.totalOutputTokens).toBe(100) + expect(sess.totalCacheReadTokens).toBe(5000) + expect(sess.toolBreakdown['Edit']?.calls).toBe(1) + }) +}) diff --git a/tests/parser-skip-line.test.ts b/tests/parser-skip-line.test.ts new file mode 100644 index 00000000..f023f460 --- /dev/null +++ b/tests/parser-skip-line.test.ts @@ -0,0 +1,86 @@ +import { describe, it, expect } from 'vitest' + +import { shouldSkipLine } from '../src/parser.js' + +const threshold = '2026-04-01T00:00:00.000Z' + +function makeLine(type: string, timestamp: string, payloadSize = 0): string { + const payload = payloadSize > 0 ? `,"content":"${'x'.repeat(payloadSize)}"` : '' + return `{"type":"${type}","sessionId":"s1","timestamp":"${timestamp}"${payload}}` +} + +function makeLineWithLongCwd(type: string, timestamp: string, cwdLength: number): string { + const cwd = '/projects/' + 'a'.repeat(cwdLength) + return `{"type":"${type}","sessionId":"s1","cwd":"${cwd}","timestamp":"${timestamp}","message":{"role":"user","content":"hi"}}` +} + +describe('shouldSkipLine', () => { + it('skips old user lines', () => { + expect(shouldSkipLine(makeLine('user', '2026-03-01T10:00:00Z'), threshold)).toBe(true) + }) + + it('skips old assistant lines', () => { + expect(shouldSkipLine(makeLine('assistant', '2026-03-15T10:00:00Z'), threshold)).toBe(true) + }) + + it('does not skip in-range user lines', () => { + expect(shouldSkipLine(makeLine('user', '2026-04-05T10:00:00Z'), threshold)).toBe(false) + }) + + it('does not skip in-range assistant lines', () => { + expect(shouldSkipLine(makeLine('assistant', '2026-04-10T10:00:00Z'), threshold)).toBe(false) + }) + + it('never skips attachment lines regardless of timestamp', () => { + expect(shouldSkipLine(makeLine('attachment', '2026-01-01T00:00:00Z'), threshold)).toBe(false) + }) + + it('never skips system lines regardless of timestamp', () => { + expect(shouldSkipLine(makeLine('system', '2026-01-01T00:00:00Z'), threshold)).toBe(false) + }) + + it('never skips summary lines regardless of timestamp', () => { + expect(shouldSkipLine(makeLine('summary', '2026-01-01T00:00:00Z'), threshold)).toBe(false) + }) + + it('does not skip lines with no timestamp field', () => { + expect(shouldSkipLine('{"type":"user","sessionId":"s1"}', threshold)).toBe(false) + }) + + it('does not skip lines with unparseable timestamp', () => { + expect(shouldSkipLine('{"type":"user","timestamp":"bad"}', threshold)).toBe(false) + }) + + it('does not skip malformed JSON', () => { + expect(shouldSkipLine('not json at all', threshold)).toBe(false) + }) + + it('only reads top-level type and timestamp fields', () => { + const line = '{"message":{"type":"assistant","timestamp":"2026-03-01T10:00:00Z"},"type":"user","timestamp":"2026-04-05T10:00:00Z"}' + expect(shouldSkipLine(line, threshold)).toBe(false) + }) + + it('handles timestamp pushed past 200 chars by long cwd', () => { + const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 300) + expect(line.indexOf('"timestamp"')).toBeGreaterThan(200) + expect(shouldSkipLine(line, threshold)).toBe(true) + }) + + it('handles timestamp at the edge of the 2048 head window', () => { + const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 1900) + expect(line.indexOf('"timestamp"')).toBeGreaterThan(1900) + expect(shouldSkipLine(line, threshold)).toBe(true) + }) + + it('returns false when timestamp is beyond the head window', () => { + const line = makeLineWithLongCwd('user', '2026-03-01T10:00:00Z', 2100) + expect(line.indexOf('"timestamp"')).toBeGreaterThan(2048) + expect(shouldSkipLine(line, threshold)).toBe(false) + }) + + it('skips old assistant line with large payload without parsing it', () => { + const line = makeLine('assistant', '2026-02-01T10:00:00Z', 50_000_000) + expect(line.length).toBeGreaterThan(50_000_000) + expect(shouldSkipLine(line, threshold)).toBe(true) + }) +}) diff --git a/tests/plan-usage.test.ts b/tests/plan-usage.test.ts index ec281b53..b8a81c61 100644 --- a/tests/plan-usage.test.ts +++ b/tests/plan-usage.test.ts @@ -1,6 +1,12 @@ +import { mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' + import { describe, it, expect, vi, beforeEach } from 'vitest' -import { computePeriodFromResetDay, getPlanUsage, getPlanUsageFromProjects } from '../src/plan-usage.js' +import { savePlan } from '../src/config.js' +import { activePlansFromMap, computePeriodFromResetDay, getPlanUsage, getPlanUsageFromProjects, getPlanUsages } from '../src/plan-usage.js' +import type { ProjectSummary } from '../src/types.js' const { parseAllSessionsMock } = vi.hoisted(() => ({ parseAllSessionsMock: vi.fn(), @@ -119,4 +125,231 @@ describe('getPlanUsage', () => { expect(usage.budgetUsd).toBe(100) expect(usage.status).toBe('under') }) + + it('projects month-end spend from API call timestamps', () => { + const usage = getPlanUsageFromProjects({ + id: 'custom', + monthlyUsd: 100, + provider: 'all', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }, [ + { + project: 'codeburn', + projectPath: '/tmp/codeburn', + totalCostUSD: 10, + totalApiCalls: 1, + sessions: [ + { + turns: [ + { + timestamp: '2026-03-31T23:59:00.000Z', + assistantCalls: [{ costUSD: 10, timestamp: '2026-04-01T10:00:00.000Z' }], + }, + ], + }, + ], + }, + ] as ProjectSummary[], new Date('2026-04-01T12:00:00.000Z')) + + expect(Math.round(usage.projectedMonthUsd)).toBe(300) + }) + + it('returns active plans in provider display order', () => { + const plans = activePlansFromMap({ + codex: { + id: 'custom', + monthlyUsd: 200, + provider: 'codex', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }, + claude: { + id: 'claude-max', + monthlyUsd: 200, + provider: 'claude', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }, + cursor: { + id: 'none', + monthlyUsd: 0, + provider: 'cursor', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }, + }) + + expect(plans.map(plan => plan.provider)).toEqual(['claude', 'codex']) + }) + + it('keeps the provider-specific parser filter for one active plan', async () => { + const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-usage-test-')) + const previousHome = process.env['HOME'] + process.env['HOME'] = dir + + try { + await savePlan({ + id: 'claude-max', + monthlyUsd: 200, + provider: 'claude', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }) + + parseAllSessionsMock.mockResolvedValue([ + { + project: 'codeburn', + projectPath: '/tmp/codeburn', + totalCostUSD: 80, + totalApiCalls: 1, + sessions: [], + }, + ] satisfies ProjectSummary[]) + + const usages = await getPlanUsages(new Date('2026-04-10T12:00:00.000Z')) + + expect(parseAllSessionsMock).toHaveBeenCalledTimes(1) + expect(parseAllSessionsMock).toHaveBeenCalledWith( + expect.objectContaining({ start: expect.any(Date), end: expect.any(Date) }), + 'claude', + ) + expect(usages).toHaveLength(1) + expect(usages[0]?.spentApiEquivalentUsd).toBe(80) + } finally { + if (previousHome === undefined) { + delete process.env['HOME'] + } else { + process.env['HOME'] = previousHome + } + await rm(dir, { recursive: true, force: true }) + } + }) + + it('computes multiple active plan usages from one all-provider parse', async () => { + const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-usage-test-')) + const previousHome = process.env['HOME'] + process.env['HOME'] = dir + + try { + await savePlan({ + id: 'claude-max', + monthlyUsd: 200, + provider: 'claude', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }) + await savePlan({ + id: 'custom', + monthlyUsd: 100, + provider: 'codex', + resetDay: 1, + setAt: '2026-04-01T00:00:00.000Z', + }) + + parseAllSessionsMock.mockResolvedValue([ + { + project: 'codeburn', + projectPath: '/tmp/codeburn', + totalCostUSD: 150, + totalApiCalls: 2, + sessions: [ + { + sessionId: 'session-1', + project: 'codeburn', + firstTimestamp: '2026-04-03T10:00:00.000Z', + lastTimestamp: '2026-04-03T11:00:00.000Z', + totalCostUSD: 150, + totalInputTokens: 0, + totalOutputTokens: 0, + totalCacheReadTokens: 0, + totalCacheWriteTokens: 0, + apiCalls: 2, + modelBreakdown: {}, + toolBreakdown: {}, + mcpBreakdown: {}, + bashBreakdown: {}, + categoryBreakdown: {}, + skillBreakdown: {}, + turns: [ + { + userMessage: 'work', + timestamp: '2026-04-03T10:00:00.000Z', + sessionId: 'session-1', + category: 'coding', + retries: 0, + hasEdits: true, + assistantCalls: [ + { + provider: 'claude', + model: 'claude-opus-4-7', + usage: { + inputTokens: 0, + outputTokens: 0, + cacheCreationInputTokens: 0, + cacheReadInputTokens: 0, + cachedInputTokens: 0, + reasoningTokens: 0, + webSearchRequests: 0, + }, + costUSD: 100, + tools: [], + mcpTools: [], + skills: [], + hasAgentSpawn: false, + hasPlanMode: false, + speed: 'standard', + timestamp: '2026-04-03T10:00:00.000Z', + bashCommands: [], + deduplicationKey: 'claude-1', + }, + { + provider: 'codex', + model: 'gpt-5.5', + usage: { + inputTokens: 0, + outputTokens: 0, + cacheCreationInputTokens: 0, + cacheReadInputTokens: 0, + cachedInputTokens: 0, + reasoningTokens: 0, + webSearchRequests: 0, + }, + costUSD: 50, + tools: [], + mcpTools: [], + skills: [], + hasAgentSpawn: false, + hasPlanMode: false, + speed: 'standard', + timestamp: '2026-04-03T11:00:00.000Z', + bashCommands: [], + deduplicationKey: 'codex-1', + }, + ], + }, + ], + }, + ], + }, + ] satisfies ProjectSummary[]) + + const usages = await getPlanUsages(new Date('2026-04-10T12:00:00.000Z')) + + expect(parseAllSessionsMock).toHaveBeenCalledTimes(1) + expect(parseAllSessionsMock).toHaveBeenCalledWith( + expect.objectContaining({ start: expect.any(Date), end: expect.any(Date) }), + 'all', + ) + expect(usages.map(usage => usage.plan.provider)).toEqual(['claude', 'codex']) + expect(usages.map(usage => usage.spentApiEquivalentUsd)).toEqual([100, 50]) + } finally { + if (previousHome === undefined) { + delete process.env['HOME'] + } else { + process.env['HOME'] = previousHome + } + await rm(dir, { recursive: true, force: true }) + } + }) }) diff --git a/tests/plans.test.ts b/tests/plans.test.ts index 7a358ac4..4ed7cc86 100644 --- a/tests/plans.test.ts +++ b/tests/plans.test.ts @@ -4,7 +4,7 @@ import { join } from 'node:path' import { describe, it, expect } from 'vitest' -import { clearPlan, readPlan, savePlan } from '../src/config.js' +import { clearPlan, readPlan, readPlans, saveConfig, savePlan } from '../src/config.js' import { getPresetPlan, isPlanId, isPlanProvider } from '../src/plans.js' describe('plan presets', () => { @@ -27,7 +27,7 @@ describe('plan presets', () => { }) describe('plan config persistence', () => { - it('round-trips savePlan/readPlan and clearPlan', async () => { + it('round-trips per-provider plans and clears one provider at a time', async () => { const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-')) const previousHome = process.env['HOME'] process.env['HOME'] = dir @@ -40,17 +40,156 @@ describe('plan config persistence', () => { resetDay: 12, setAt: '2026-04-17T12:00:00.000Z', }) + await savePlan({ + id: 'custom', + monthlyUsd: 200, + provider: 'codex', + resetDay: 1, + setAt: '2026-04-18T12:00:00.000Z', + }) - const plan = await readPlan() - expect(plan).toMatchObject({ + const plans = await readPlans() + expect(plans.claude).toMatchObject({ id: 'claude-max', monthlyUsd: 200, provider: 'claude', resetDay: 12, }) + expect(plans.codex).toMatchObject({ + id: 'custom', + monthlyUsd: 200, + provider: 'codex', + resetDay: 1, + }) + expect(await readPlan()).toMatchObject({ id: 'claude-max', provider: 'claude' }) + + await clearPlan('codex') + expect((await readPlans()).codex).toBeUndefined() + expect((await readPlans()).claude).toMatchObject({ id: 'claude-max' }) + + await clearPlan('all') + expect((await readPlans()).claude).toMatchObject({ id: 'claude-max' }) await clearPlan() expect(await readPlan()).toBeUndefined() + expect(await readPlans()).toEqual({}) + } finally { + if (previousHome === undefined) { + delete process.env['HOME'] + } else { + process.env['HOME'] = previousHome + } + await rm(dir, { recursive: true, force: true }) + } + }) + + it('reads legacy single-plan config as a provider-keyed plan map', async () => { + const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-')) + const previousHome = process.env['HOME'] + process.env['HOME'] = dir + + try { + await saveConfig({ + plan: { + id: 'cursor-pro', + monthlyUsd: 20, + provider: 'cursor', + resetDay: 3, + setAt: '2026-04-17T12:00:00.000Z', + }, + }) + + const plans = await readPlans() + expect(plans.cursor).toMatchObject({ + id: 'cursor-pro', + monthlyUsd: 20, + provider: 'cursor', + resetDay: 3, + }) + } finally { + if (previousHome === undefined) { + delete process.env['HOME'] + } else { + process.env['HOME'] = previousHome + } + await rm(dir, { recursive: true, force: true }) + } + }) + + it('drops a hand-edited all plan when provider-specific plans are present', async () => { + const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-')) + const previousHome = process.env['HOME'] + process.env['HOME'] = dir + + try { + await saveConfig({ + plans: { + all: { + id: 'custom', + monthlyUsd: 300, + resetDay: 1, + setAt: '2026-04-17T12:00:00.000Z', + }, + claude: { + id: 'claude-max', + monthlyUsd: 200, + resetDay: 1, + setAt: '2026-04-18T12:00:00.000Z', + }, + }, + }) + + const plans = await readPlans() + expect(plans.all).toBeUndefined() + expect(plans.claude).toMatchObject({ id: 'claude-max', provider: 'claude' }) + expect(await readPlan()).toMatchObject({ id: 'claude-max', provider: 'claude' }) + } finally { + if (previousHome === undefined) { + delete process.env['HOME'] + } else { + process.env['HOME'] = previousHome + } + await rm(dir, { recursive: true, force: true }) + } + }) + + it('does not allow an all-provider plan to overlap provider-specific plans', async () => { + const dir = await mkdtemp(join(tmpdir(), 'codeburn-plan-test-')) + const previousHome = process.env['HOME'] + process.env['HOME'] = dir + + try { + await savePlan({ + id: 'custom', + monthlyUsd: 100, + provider: 'all', + resetDay: 1, + setAt: '2026-04-17T12:00:00.000Z', + }) + await savePlan({ + id: 'claude-max', + monthlyUsd: 200, + provider: 'claude', + resetDay: 1, + setAt: '2026-04-18T12:00:00.000Z', + }) + + expect(await readPlans()).toMatchObject({ + claude: { id: 'claude-max' }, + }) + expect((await readPlans()).all).toBeUndefined() + + await savePlan({ + id: 'custom', + monthlyUsd: 300, + provider: 'all', + resetDay: 1, + setAt: '2026-04-19T12:00:00.000Z', + }) + expect(await readPlans()).toMatchObject({ + all: { id: 'custom', monthlyUsd: 300 }, + }) + expect((await readPlans()).claude).toBeUndefined() } finally { if (previousHome === undefined) { delete process.env['HOME'] diff --git a/tests/provider-registry.test.ts b/tests/provider-registry.test.ts index 4497946f..2a82867d 100644 --- a/tests/provider-registry.test.ts +++ b/tests/provider-registry.test.ts @@ -3,7 +3,25 @@ import { providers, getAllProviders } from '../src/providers/index.js' describe('provider registry', () => { it('has core providers registered synchronously', () => { - expect(providers.map(p => p.name)).toEqual(['claude', 'codex', 'copilot', 'droid', 'gemini', 'kilo-code', 'kiro', 'openclaw', 'pi', 'omp', 'qwen', 'roo-code']) + expect(providers.map(p => p.name)).toEqual(['claude', 'cline', 'codebuff', 'codex', 'copilot', 'droid', 'gemini', 'ibm-bob', 'kilo-code', 'kiro', 'kimi', 'mistral-vibe', 'openclaw', 'pi', 'omp', 'qwen', 'roo-code']) + }) + + it('codebuff tool display names normalize codebuff-native names to canonical set', () => { + const codebuff = providers.find(p => p.name === 'codebuff')! + expect(codebuff.toolDisplayName('read_files')).toBe('Read') + expect(codebuff.toolDisplayName('code_search')).toBe('Grep') + expect(codebuff.toolDisplayName('str_replace')).toBe('Edit') + expect(codebuff.toolDisplayName('run_terminal_command')).toBe('Bash') + expect(codebuff.toolDisplayName('spawn_agents')).toBe('Agent') + expect(codebuff.toolDisplayName('write_todos')).toBe('TodoWrite') + expect(codebuff.toolDisplayName('unknown_tool')).toBe('unknown_tool') + }) + + it('codebuff model display names cover known agent tiers', () => { + const codebuff = providers.find(p => p.name === 'codebuff')! + expect(codebuff.modelDisplayName('codebuff')).toBe('Codebuff') + expect(codebuff.modelDisplayName('codebuff-base2')).toBe('Codebuff Base 2') + expect(codebuff.modelDisplayName('some-future-model')).toBe('some-future-model') }) it('includes sqlite providers after async load', async () => { @@ -60,6 +78,14 @@ describe('provider registry', () => { expect(claude.modelDisplayName('claude-sonnet-4-6')).toBe('Sonnet 4.6') }) + it('kimi model and tool display names are normalized', () => { + const kimi = providers.find(p => p.name === 'kimi')! + expect(kimi.modelDisplayName('kimi-auto')).toBe('Kimi (auto)') + expect(kimi.modelDisplayName('kimi-k2-thinking-turbo')).toBe('Kimi K2 Thinking Turbo') + expect(kimi.toolDisplayName('Shell')).toBe('Bash') + expect(kimi.toolDisplayName('WriteFile')).toBe('Write') + }) + it('cursor model display names handle auto mode', async () => { const all = await getAllProviders() const cursor = all.find(p => p.name === 'cursor')! diff --git a/tests/providers/antigravity.test.ts b/tests/providers/antigravity.test.ts new file mode 100644 index 00000000..9396c37f --- /dev/null +++ b/tests/providers/antigravity.test.ts @@ -0,0 +1,123 @@ +import { describe, expect, it } from 'vitest' + +import { + extractAntigravityGeneratorMetadata, + extractAntigravityModelMap, + parseAntigravityServerInfo, + parseAntigravityServerInfoFromLine, +} from '../../src/providers/antigravity.js' + +describe('antigravity provider helpers', () => { + it('parses legacy https server flags from POSIX process args', () => { + const server = parseAntigravityServerInfoFromLine( + '/Applications/Antigravity.app/language_server_macos_arm --app_data_dir antigravity --https_server_port 57101 --csrf_token 01234567-89ab-cdef-0123-456789abcdef', + ) + + expect(server).toEqual({ + port: 57101, + csrfToken: '01234567-89ab-cdef-0123-456789abcdef', + }) + }) + + it('parses Windows extension server flags and equals syntax', () => { + const server = parseAntigravityServerInfoFromLine( + 'C:\\Users\\Admin\\AppData\\Local\\Programs\\Antigravity\\resources\\app\\extensions\\antigravity\\bin\\language_server_windows_x64.exe --extension_server_port=62225 --extension_server_csrf_token=abcdef01-2345-6789-abcd-ef0123456789', + ) + + expect(server).toEqual({ + port: 62225, + csrfToken: 'abcdef01-2345-6789-abcd-ef0123456789', + }) + }) + + it('parses Windows extension server flags and space syntax', () => { + const server = parseAntigravityServerInfo([ + 'node something-unrelated', + 'language_server_windows_x64.exe --app_data_dir C:\\Users\\Admin\\.gemini\\antigravity --extension_server_port 62300 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210', + ]) + + expect(server).toEqual({ + port: 62300, + csrfToken: 'fedcba98-7654-3210-fedc-ba9876543210', + }) + }) + + it('parses quoted flag values', () => { + const server = parseAntigravityServerInfoFromLine( + 'Antigravity language_server_windows_x64.exe --extension_server_port "62301" --extension_server_csrf_token "fedcba98-7654-3210-fedc-ba9876543211"', + ) + + expect(server).toEqual({ + port: 62301, + csrfToken: 'fedcba98-7654-3210-fedc-ba9876543211', + }) + }) + + it('matches language-server and antigravity markers case-insensitively', () => { + const server = parseAntigravityServerInfoFromLine( + 'ANTIGRAVITY LANGUAGE_SERVER_WINDOWS_X64.EXE --extension_server_port 62302 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543212', + ) + + expect(server).toEqual({ + port: 62302, + csrfToken: 'fedcba98-7654-3210-fedc-ba9876543212', + }) + }) + + it('ignores process args without an antigravity marker', () => { + expect(parseAntigravityServerInfoFromLine( + 'language_server --extension_server_port 62300 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210', + )).toBeNull() + }) + + it('ignores invalid ports', () => { + expect(parseAntigravityServerInfoFromLine( + 'antigravity language_server --extension_server_port 99999 --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210', + )).toBeNull() + }) + + it('ignores chained flag names as values', () => { + expect(parseAntigravityServerInfoFromLine( + 'antigravity language_server --extension_server_port=--extension_server_csrf_token --extension_server_csrf_token fedcba98-7654-3210-fedc-ba9876543210', + )).toBeNull() + }) + + it('ignores implausibly short CSRF tokens', () => { + expect(parseAntigravityServerInfoFromLine( + 'antigravity language_server --extension_server_port 62300 --extension_server_csrf_token short', + )).toBeNull() + }) + + it('extracts model maps from wrapped and unwrapped RPC responses', () => { + expect(extractAntigravityModelMap({ + response: { models: { high: { model: 'MODEL_PLACEHOLDER_M7' } } }, + })).toEqual({ MODEL_PLACEHOLDER_M7: 'high' }) + + expect(extractAntigravityModelMap({ + models: { low: { model: 'MODEL_PLACEHOLDER_M8' } }, + })).toEqual({ MODEL_PLACEHOLDER_M8: 'low' }) + expect(extractAntigravityModelMap({ + models: { bad: null, good: { model: 'MODEL_PLACEHOLDER_M9' } }, + })).toEqual({ MODEL_PLACEHOLDER_M9: 'good' }) + expect(extractAntigravityModelMap(null)).toEqual({}) + }) + + it('extracts generator metadata from wrapped and unwrapped RPC responses', () => { + const metadata = [{ + chatModel: { + model: 'gemini-3-pro', + usage: { + model: 'gemini-3-pro', + inputTokens: '10', + outputTokens: '4', + apiProvider: 'google', + }, + }, + }] + + expect(extractAntigravityGeneratorMetadata({ response: { generatorMetadata: metadata } })).toEqual(metadata) + expect(extractAntigravityGeneratorMetadata({ generatorMetadata: metadata })).toEqual(metadata) + expect(extractAntigravityGeneratorMetadata({ response: { generatorMetadata: null } })).toEqual([]) + expect(extractAntigravityGeneratorMetadata(null)).toEqual([]) + }) +}) diff --git a/tests/providers/cline.test.ts b/tests/providers/cline.test.ts new file mode 100644 index 00000000..d739b96c --- /dev/null +++ b/tests/providers/cline.test.ts @@ -0,0 +1,139 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { mkdtemp, mkdir, writeFile, rm, utimes } from 'fs/promises' +import { join } from 'path' +import { tmpdir } from 'os' + +import { cline, createClineProvider } from '../../src/providers/cline.js' +import type { ParsedProviderCall } from '../../src/providers/types.js' + +let tmpDir: string + +async function writeTask(baseDir: string, taskId: string, opts?: { + tokensIn?: number + tokensOut?: number + model?: string + userMessage?: string + cost?: number +}): Promise { + const taskDir = join(baseDir, 'tasks', taskId) + await mkdir(taskDir, { recursive: true }) + + const messages: unknown[] = [] + if (opts?.userMessage) { + messages.push({ type: 'say', say: 'user_feedback', text: opts.userMessage, ts: 1700000000000 }) + } + const usage: Record = { + tokensIn: opts?.tokensIn ?? 100, + tokensOut: opts?.tokensOut ?? 50, + } + if (opts?.cost !== undefined) usage.cost = opts.cost + messages.push({ type: 'say', say: 'api_req_started', text: JSON.stringify(usage), ts: 1700000001000 }) + + const modelTag = opts?.model ? `${opts.model}` : '' + const history = [ + { role: 'user', content: [{ type: 'text', text: `hello\n\n${modelTag}\n` }] }, + ] + + await writeFile(join(taskDir, 'ui_messages.json'), JSON.stringify(messages)) + await writeFile(join(taskDir, 'api_conversation_history.json'), JSON.stringify(history)) + + return taskDir +} + +describe('cline provider - discovery', () => { + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'cline-test-')) + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) + }) + + it('discovers Cline tasks from VS Code globalStorage and home data roots', async () => { + const vscodeDir = join(tmpDir, 'globalStorage') + const homeDataDir = join(tmpDir, 'cline-data') + await writeTask(vscodeDir, 'task-vscode') + await writeTask(homeDataDir, 'task-home') + + const provider = createClineProvider([vscodeDir, homeDataDir]) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(2) + expect(sessions.map(s => s.provider)).toEqual(['cline', 'cline']) + expect(sessions.map(s => s.project)).toEqual(['Cline', 'Cline']) + expect(sessions.map(s => s.path).sort()).toEqual([ + join(homeDataDir, 'tasks', 'task-home'), + join(vscodeDir, 'tasks', 'task-vscode'), + ].sort()) + }) + + it('deduplicates the same task id across roots by keeping the newest task directory', async () => { + const vscodeDir = join(tmpDir, 'globalStorage') + const homeDataDir = join(tmpDir, 'cline-data') + const oldTask = await writeTask(vscodeDir, 'task-same') + const newTask = await writeTask(homeDataDir, 'task-same') + await utimes(join(oldTask, 'ui_messages.json'), new Date('2026-01-01T00:00:00Z'), new Date('2026-01-01T00:00:00Z')) + await utimes(join(newTask, 'ui_messages.json'), new Date('2026-02-01T00:00:00Z'), new Date('2026-02-01T00:00:00Z')) + + const provider = createClineProvider([vscodeDir, homeDataDir]) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]!.path).toBe(newTask) + }) + + it('skips task directories without ui_messages.json', async () => { + const vscodeDir = join(tmpDir, 'globalStorage') + await mkdir(join(vscodeDir, 'tasks', 'task-no-ui'), { recursive: true }) + + const provider = createClineProvider(vscodeDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(0) + }) +}) + +describe('cline provider - parsing', () => { + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'cline-test-')) + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) + }) + + it('parses Cline usage with cline provider identity', async () => { + const taskDir = await writeTask(tmpDir, 'task-parse', { + tokensIn: 200, + tokensOut: 100, + model: 'anthropic/claude-sonnet-4-5', + userMessage: 'build the feature', + cost: 0.07, + }) + + const source = { path: taskDir, project: 'Cline', provider: 'cline' } + const calls: ParsedProviderCall[] = [] + for await (const call of cline.createSessionParser(source, new Set()).parse()) calls.push(call) + + expect(calls).toHaveLength(1) + expect(calls[0]!.provider).toBe('cline') + expect(calls[0]!.model).toBe('claude-sonnet-4-5') + expect(calls[0]!.inputTokens).toBe(200) + expect(calls[0]!.outputTokens).toBe(100) + expect(calls[0]!.costUSD).toBe(0.07) + expect(calls[0]!.userMessage).toBe('build the feature') + expect(calls[0]!.deduplicationKey).toMatch(/^cline:task-parse:/) + }) +}) + +describe('cline provider - metadata', () => { + it('has correct name and displayName', () => { + expect(cline.name).toBe('cline') + expect(cline.displayName).toBe('Cline') + }) + + it('passes through model and tool display names', () => { + expect(cline.modelDisplayName('claude-sonnet-4-5')).toBe('claude-sonnet-4-5') + expect(cline.toolDisplayName('read_file')).toBe('read_file') + }) +}) diff --git a/tests/providers/codebuff.test.ts b/tests/providers/codebuff.test.ts new file mode 100644 index 00000000..eb6b2348 --- /dev/null +++ b/tests/providers/codebuff.test.ts @@ -0,0 +1,480 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises' +import { join } from 'path' +import { tmpdir } from 'os' + +import { createCodebuffProvider } from '../../src/providers/codebuff.js' +import type { ParsedProviderCall } from '../../src/providers/types.js' + +let tmpDir: string + +beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'codebuff-test-')) +}) + +afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) +}) + +type ToolBlock = { + type: 'tool' + toolName: string + input?: Record +} + +type TextBlock = { type: 'text'; content: string } + +type Block = ToolBlock | TextBlock + +type AiOpts = { + id?: string + credits?: number + timestamp?: string + blocks?: Block[] + metadata?: Record +} + +function aiMessage(opts: AiOpts = {}) { + const m: Record = { + id: opts.id ?? 'msg-ai-1', + variant: 'ai', + content: '', + timestamp: opts.timestamp ?? '2026-04-14T10:00:30.000Z', + } + if (opts.blocks !== undefined) m['blocks'] = opts.blocks + if (opts.credits !== undefined) m['credits'] = opts.credits + if (opts.metadata !== undefined) m['metadata'] = opts.metadata + return m +} + +function userMessage(content: string, timestamp?: string) { + return { + id: 'msg-user-1', + variant: 'user', + content, + timestamp: timestamp ?? '2026-04-14T10:00:10.000Z', + } +} + +async function writeChat( + baseDir: string, + projectName: string, + chatId: string, + messages: unknown[], + runState?: unknown, +): Promise { + const chatDir = join(baseDir, 'projects', projectName, 'chats', chatId) + await mkdir(chatDir, { recursive: true }) + await writeFile(join(chatDir, 'chat-messages.json'), JSON.stringify(messages)) + if (runState !== undefined) { + await writeFile(join(chatDir, 'run-state.json'), JSON.stringify(runState)) + } + return chatDir +} + +describe('codebuff provider - session discovery', () => { + it('discovers sessions under projects//chats//', async () => { + await writeChat( + tmpDir, + 'myproject', + '2026-04-14T10-00-00.000Z', + [userMessage('hi'), aiMessage({ credits: 10 })], + { sessionState: { projectContext: { cwd: '/Users/test/myproject' } } }, + ) + + const provider = createCodebuffProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]!.provider).toBe('codebuff') + expect(sessions[0]!.project).toBe('myproject') + expect(sessions[0]!.path).toContain('2026-04-14T10-00-00.000Z') + }) + + it('uses the cwd basename from run-state.json when present', async () => { + await writeChat( + tmpDir, + 'sanitized-folder', + '2026-04-14T11-00-00.000Z', + [aiMessage({ credits: 5 })], + { sessionState: { projectContext: { cwd: '/Users/test/real-project' } } }, + ) + + const provider = createCodebuffProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]!.project).toBe('real-project') + }) + + it('falls back to the folder name when run-state.json is missing', async () => { + await writeChat(tmpDir, 'fallback-project', '2026-04-14T12-00-00.000Z', [ + aiMessage({ credits: 3 }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]!.project).toBe('fallback-project') + }) + + it('discovers sessions across multiple projects', async () => { + await writeChat(tmpDir, 'proj-a', '2026-04-14T10-00-00.000Z', [aiMessage({ credits: 1 })]) + await writeChat(tmpDir, 'proj-b', '2026-04-14T10-30-00.000Z', [aiMessage({ credits: 2 })]) + + const provider = createCodebuffProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(2) + const projects = sessions.map(s => s.project).sort() + expect(projects).toEqual(['proj-a', 'proj-b']) + }) + + it('returns empty for a non-existent directory', async () => { + const provider = createCodebuffProvider('/nonexistent/codebuff-path') + const sessions = await provider.discoverSessions() + expect(sessions).toEqual([]) + }) + + it('skips chat folders without chat-messages.json', async () => { + const chatDir = join(tmpDir, 'projects', 'proj', 'chats', '2026-04-14T10-00-00.000Z') + await mkdir(chatDir, { recursive: true }) + // No chat-messages.json created. + + const provider = createCodebuffProvider(tmpDir) + const sessions = await provider.discoverSessions() + expect(sessions).toEqual([]) + }) +}) + +describe('codebuff provider - JSONL parsing', () => { + it('yields one call per assistant message with credits, mapping codebuff tools to canonical names', async () => { + const chatDir = await writeChat( + tmpDir, + 'proj', + '2026-04-14T10-00-00.000Z', + [ + userMessage('implement the feature'), + aiMessage({ + credits: 42, + metadata: { + runState: { sessionState: { mainAgentState: { agentType: 'base2' } } }, + }, + blocks: [ + { type: 'tool', toolName: 'read_files', input: {} }, + { type: 'tool', toolName: 'str_replace', input: {} }, + { type: 'tool', toolName: 'run_terminal_command', input: { command: 'npm test' } }, + { type: 'tool', toolName: 'suggest_followups', input: {} }, + ], + }), + ], + ) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(1) + const call = calls[0]! + expect(call.provider).toBe('codebuff') + expect(call.model).toBe('codebuff-base2') + expect(call.userMessage).toBe('implement the feature') + // `suggest_followups` is intentionally dropped from the tool breakdown. + expect(call.tools).toEqual(['Read', 'Edit', 'Bash']) + expect(call.bashCommands).toContain('npm') + // Credits × $0.01 = $0.42 when token counts are absent. + expect(call.costUSD).toBeCloseTo(0.42, 6) + expect(call.inputTokens).toBe(0) + expect(call.outputTokens).toBe(0) + }) + + it('prefers direct metadata.usage tokens when available and still records credits', async () => { + const chatDir = await writeChat(tmpDir, 'proj', '2026-04-14T10-00-00.000Z', [ + aiMessage({ + credits: 10, + metadata: { + model: 'claude-haiku-4-5-20251001', + usage: { + inputTokens: 5000, + outputTokens: 2000, + cacheCreationInputTokens: 1000, + cacheReadInputTokens: 500, + }, + }, + }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(1) + const call = calls[0]! + expect(call.model).toBe('claude-haiku-4-5-20251001') + expect(call.inputTokens).toBe(5000) + expect(call.outputTokens).toBe(2000) + expect(call.cacheCreationInputTokens).toBe(1000) + expect(call.cacheReadInputTokens).toBe(500) + expect(call.cachedInputTokens).toBe(500) + // With real token counts the calculated cost takes precedence over credits. + expect(call.costUSD).toBeGreaterThan(0) + }) + + it('falls back to providerOptions.codebuff.usage in the stashed RunState history', async () => { + const chatDir = await writeChat(tmpDir, 'proj', '2026-04-14T10-00-00.000Z', [ + aiMessage({ + credits: 7, + metadata: { + runState: { + sessionState: { + mainAgentState: { + messageHistory: [ + { role: 'user' }, + { + role: 'assistant', + providerOptions: { + codebuff: { + model: 'openai/gpt-4o', + usage: { + prompt_tokens: 2000, + completion_tokens: 800, + prompt_tokens_details: { cached_tokens: 400 }, + }, + }, + }, + }, + ], + }, + }, + }, + }, + }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(1) + expect(calls[0]!.model).toBe('openai/gpt-4o') + expect(calls[0]!.inputTokens).toBe(2000) + expect(calls[0]!.outputTokens).toBe(800) + expect(calls[0]!.cacheReadInputTokens).toBe(400) + }) + + it('skips assistant messages with no credits and no tokens', async () => { + const chatDir = await writeChat(tmpDir, 'proj', '2026-04-14T10-00-00.000Z', [ + aiMessage({ blocks: [{ type: 'text', content: 'mode-divider' }] }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(0) + }) + + it('deduplicates calls seen across multiple parses', async () => { + const chatDir = await writeChat(tmpDir, 'proj', '2026-04-14T10-00-00.000Z', [ + aiMessage({ id: 'msg-dup', credits: 3 }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const seenKeys = new Set() + + const firstRun: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, seenKeys).parse()) { + firstRun.push(call) + } + + const secondRun: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, seenKeys).parse()) { + secondRun.push(call) + } + + expect(firstRun).toHaveLength(1) + expect(secondRun).toHaveLength(0) + }) + + it('yields one call per assistant message in a multi-turn chat, preserving user messages', async () => { + const chatDir = await writeChat(tmpDir, 'proj', '2026-04-14T10-00-00.000Z', [ + userMessage('first question'), + aiMessage({ id: 'a1', credits: 5, timestamp: '2026-04-14T10:00:30.000Z' }), + userMessage('second question', '2026-04-14T10:01:00.000Z'), + aiMessage({ id: 'a2', credits: 8, timestamp: '2026-04-14T10:01:30.000Z' }), + ]) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(2) + expect(calls[0]!.userMessage).toBe('first question') + expect(calls[0]!.costUSD).toBeCloseTo(0.05, 6) + expect(calls[1]!.userMessage).toBe('second question') + expect(calls[1]!.costUSD).toBeCloseTo(0.08, 6) + }) + + it('handles a missing chat-messages.json gracefully', async () => { + const provider = createCodebuffProvider(tmpDir) + const source = { + path: join(tmpDir, 'projects', 'missing', 'chats', 'nope'), + project: 'missing', + provider: 'codebuff', + } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + expect(calls).toHaveLength(0) + }) + + it('skips a malformed chat-messages.json without throwing', async () => { + const chatDir = join(tmpDir, 'projects', 'proj', 'chats', '2026-04-14T10-00-00.000Z') + await mkdir(chatDir, { recursive: true }) + await writeFile(join(chatDir, 'chat-messages.json'), 'not-valid-json') + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + expect(calls).toHaveLength(0) + }) +}) + +describe('codebuff provider - sessionId channel scoping', () => { + it('produces distinct sessionIds for the same chatId across different channel roots', async () => { + const chatId = '2026-04-14T10-00-00.000Z' + const channelA = join(tmpDir, 'manicode') + const channelB = join(tmpDir, 'manicode-dev') + const cwd = '/Users/test/shared-project' + const runState = { sessionState: { projectContext: { cwd } } } + + const chatDirA = await writeChat( + channelA, + 'shared-project', + chatId, + [userMessage('hi'), aiMessage({ credits: 5 })], + runState, + ) + const chatDirB = await writeChat( + channelB, + 'shared-project', + chatId, + [userMessage('hi'), aiMessage({ credits: 5 })], + runState, + ) + + const providerA = createCodebuffProvider(channelA) + const providerB = createCodebuffProvider(channelB) + + const sourceA = { path: chatDirA, project: 'shared-project', provider: 'codebuff' } + const sourceB = { path: chatDirB, project: 'shared-project', provider: 'codebuff' } + + const callsA: ParsedProviderCall[] = [] + for await (const call of providerA.createSessionParser(sourceA, new Set()).parse()) { + callsA.push(call) + } + const callsB: ParsedProviderCall[] = [] + for await (const call of providerB.createSessionParser(sourceB, new Set()).parse()) { + callsB.push(call) + } + + expect(callsA).toHaveLength(1) + expect(callsB).toHaveLength(1) + // The whole point of the fix: same chatId + same project should NOT + // collapse into a single session when the chats live under different + // channel roots. + expect(callsA[0]!.sessionId).not.toBe(callsB[0]!.sessionId) + expect(callsA[0]!.sessionId).toBe(`manicode/${chatId}`) + expect(callsB[0]!.sessionId).toBe(`manicode-dev/${chatId}`) + // The sessionId must not contain ':' -- src/parser.ts keys sessions as + // `${provider}:${sessionId}:${project}` and reconstructs the session via + // `key.split(':')[1]`, so a colon would truncate the id downstream. + expect(callsA[0]!.sessionId).not.toContain(':') + expect(callsB[0]!.sessionId).not.toContain(':') + }) + + it('includes the channel name in the sessionId', async () => { + const chatId = '2026-04-14T10-00-00.000Z' + const channelRoot = join(tmpDir, 'manicode-staging') + const chatDir = await writeChat(channelRoot, 'proj', chatId, [aiMessage({ credits: 3 })]) + + const provider = createCodebuffProvider(channelRoot) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(1) + expect(calls[0]!.sessionId).toBe(`manicode-staging/${chatId}`) + expect(calls[0]!.sessionId).not.toContain(':') + }) + + it('falls back to the chatId when the path does not match the expected structure', async () => { + const chatId = '2026-04-14T10-00-00.000Z' + // Not the canonical /projects//chats/ layout. + const chatDir = join(tmpDir, 'oddly-shaped', chatId) + await mkdir(chatDir, { recursive: true }) + await writeFile( + join(chatDir, 'chat-messages.json'), + JSON.stringify([aiMessage({ credits: 2 })]), + ) + + const provider = createCodebuffProvider(tmpDir) + const source = { path: chatDir, project: 'proj', provider: 'codebuff' } + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, new Set()).parse()) { + calls.push(call) + } + + expect(calls).toHaveLength(1) + expect(calls[0]!.sessionId).toBe(chatId) + }) +}) + +describe('codebuff provider - display names', () => { + const provider = createCodebuffProvider('/tmp') + + it('has the correct identifiers', () => { + expect(provider.name).toBe('codebuff') + expect(provider.displayName).toBe('Codebuff') + }) + + it('maps known Codebuff tiers to readable names', () => { + expect(provider.modelDisplayName('codebuff')).toBe('Codebuff') + expect(provider.modelDisplayName('codebuff-base2')).toBe('Codebuff Base 2') + expect(provider.modelDisplayName('codebuff-lite')).toBe('Codebuff Lite') + }) + + it('returns the raw name for unknown models', () => { + expect(provider.modelDisplayName('claude-sonnet-4-6')).toBe('claude-sonnet-4-6') + }) + + it('normalizes tool names to the canonical set', () => { + expect(provider.toolDisplayName('read_files')).toBe('Read') + expect(provider.toolDisplayName('str_replace')).toBe('Edit') + expect(provider.toolDisplayName('run_terminal_command')).toBe('Bash') + expect(provider.toolDisplayName('unknown_tool')).toBe('unknown_tool') + }) +}) diff --git a/tests/providers/ibm-bob.test.ts b/tests/providers/ibm-bob.test.ts new file mode 100644 index 00000000..d61f92e3 --- /dev/null +++ b/tests/providers/ibm-bob.test.ts @@ -0,0 +1,164 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises' +import { join } from 'path' +import { tmpdir } from 'os' + +import { ibmBob, createIBMBobProvider } from '../../src/providers/ibm-bob.js' +import type { ParsedProviderCall } from '../../src/providers/types.js' + +let tmpDir: string + +function makeUiMessages(opts: { + tokensIn?: number + tokensOut?: number + cacheReads?: number + cacheWrites?: number + cost?: number + userMessage?: string + ts?: number +}): string { + const messages: unknown[] = [] + + if (opts.userMessage) { + messages.push({ type: 'say', say: 'user_feedback', text: opts.userMessage, ts: 1_700_000_000_000 }) + } + + const apiData: Record = { + tokensIn: opts.tokensIn ?? 100, + tokensOut: opts.tokensOut ?? 50, + cacheReads: opts.cacheReads ?? 0, + cacheWrites: opts.cacheWrites ?? 0, + } + if (opts.cost !== undefined) apiData.cost = opts.cost + + messages.push({ + type: 'say', + say: 'api_req_started', + text: JSON.stringify(apiData), + ts: opts.ts ?? 1_700_000_001_000, + }) + + return JSON.stringify(messages) +} + +function makeApiHistory(model?: string): string { + const modelTag = model ? `${model}` : '' + return JSON.stringify([ + { role: 'user', content: [{ type: 'text', text: `hello\n\n${modelTag}\n` }] }, + { role: 'assistant', content: [{ type: 'text', text: 'response' }] }, + ]) +} + +describe('ibm-bob provider - discovery and parsing', () => { + beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'ibm-bob-test-')) + }) + + afterEach(async () => { + await rm(tmpDir, { recursive: true, force: true }) + }) + + it('discovers IBM Bob task directories with ui_messages.json', async () => { + const task1 = join(tmpDir, 'tasks', 'task-a') + const task2 = join(tmpDir, 'tasks', 'task-b') + await mkdir(task1, { recursive: true }) + await mkdir(task2, { recursive: true }) + await writeFile(join(task1, 'ui_messages.json'), '[]') + await writeFile(join(task2, 'ui_messages.json'), '[]') + + const provider = createIBMBobProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(2) + expect(sessions.every(s => s.provider === 'ibm-bob')).toBe(true) + expect(sessions.every(s => s.project === 'IBM Bob')).toBe(true) + }) + + it('skips tasks without ui_messages.json', async () => { + const task = join(tmpDir, 'tasks', 'task-no-ui') + await mkdir(task, { recursive: true }) + await writeFile(join(task, 'api_conversation_history.json'), '[]') + + const provider = createIBMBobProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(0) + }) + + it('parses token usage and provider cost from Bob ui messages', async () => { + const taskDir = join(tmpDir, 'tasks', 'task-001') + await mkdir(taskDir, { recursive: true }) + await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ + tokensIn: 250, + tokensOut: 125, + cacheReads: 60, + cacheWrites: 30, + cost: 0.08, + userMessage: 'modernize this class', + })) + await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory('anthropic/claude-sonnet-4-6')) + + const source = { path: taskDir, project: 'IBM Bob', provider: 'ibm-bob' } + const calls: ParsedProviderCall[] = [] + for await (const call of ibmBob.createSessionParser(source, new Set()).parse()) calls.push(call) + + expect(calls).toHaveLength(1) + expect(calls[0]!).toMatchObject({ + provider: 'ibm-bob', + model: 'claude-sonnet-4-6', + inputTokens: 250, + outputTokens: 125, + cacheReadInputTokens: 60, + cacheCreationInputTokens: 30, + costUSD: 0.08, + userMessage: 'modernize this class', + sessionId: 'task-001', + }) + expect(calls[0]!.deduplicationKey).toBe('ibm-bob:task-001:0') + }) + + it('falls back to IBM Bob auto model when history has no model tag', async () => { + const taskDir = join(tmpDir, 'tasks', 'task-002') + await mkdir(taskDir, { recursive: true }) + await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 })) + await writeFile(join(taskDir, 'api_conversation_history.json'), makeApiHistory()) + + const source = { path: taskDir, project: 'IBM Bob', provider: 'ibm-bob' } + const calls: ParsedProviderCall[] = [] + for await (const call of ibmBob.createSessionParser(source, new Set()).parse()) calls.push(call) + + expect(calls).toHaveLength(1) + expect(calls[0]!.model).toBe('ibm-bob-auto') + expect(calls[0]!.costUSD).toBeGreaterThan(0) + }) + + it('deduplicates across parser runs', async () => { + const taskDir = join(tmpDir, 'tasks', 'task-003') + await mkdir(taskDir, { recursive: true }) + await writeFile(join(taskDir, 'ui_messages.json'), makeUiMessages({ tokensIn: 100, tokensOut: 50 })) + + const source = { path: taskDir, project: 'IBM Bob', provider: 'ibm-bob' } + const seenKeys = new Set() + + const calls1: ParsedProviderCall[] = [] + for await (const call of ibmBob.createSessionParser(source, seenKeys).parse()) calls1.push(call) + + const calls2: ParsedProviderCall[] = [] + for await (const call of ibmBob.createSessionParser(source, seenKeys).parse()) calls2.push(call) + + expect(calls1).toHaveLength(1) + expect(calls2).toHaveLength(0) + }) +}) + +describe('ibm-bob provider - metadata', () => { + it('has correct name and displayName', () => { + expect(ibmBob.name).toBe('ibm-bob') + expect(ibmBob.displayName).toBe('IBM Bob') + }) + + it('uses shared short model display names', () => { + expect(ibmBob.modelDisplayName('ibm-bob-auto')).toBe('IBM Bob (auto)') + expect(ibmBob.modelDisplayName('claude-sonnet-4-6')).toBe('Sonnet 4.6') + }) +}) diff --git a/tests/providers/kimi.test.ts b/tests/providers/kimi.test.ts new file mode 100644 index 00000000..486a03ee --- /dev/null +++ b/tests/providers/kimi.test.ts @@ -0,0 +1,192 @@ +import { createHash } from 'crypto' +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises' +import { join } from 'path' +import { tmpdir } from 'os' + +import { createKimiProvider } from '../../src/providers/kimi.js' +import type { ParsedProviderCall } from '../../src/providers/types.js' + +let tmpDir: string + +beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'kimi-test-')) +}) + +afterEach(async () => { + delete process.env.KIMI_MODEL_NAME + await rm(tmpDir, { recursive: true, force: true }) +}) + +function md5(value: string): string { + return createHash('md5').update(value, 'utf-8').digest('hex') +} + +function record(timestamp: number, type: string, payload: Record): string { + return JSON.stringify({ + timestamp, + message: { type, payload }, + }) +} + +async function writeSession(workDir: string, sessionId: string, lines: string[]): Promise { + const hash = md5(workDir) + const sessionDir = join(tmpDir, 'sessions', hash, sessionId) + await mkdir(sessionDir, { recursive: true }) + const wirePath = join(sessionDir, 'wire.jsonl') + await writeFile(wirePath, [ + JSON.stringify({ type: 'metadata', protocol_version: '2' }), + ...lines, + ].join('\n') + '\n') + return wirePath +} + +async function collect(provider: ReturnType, path: string, seen = new Set()): Promise { + const parser = provider.createSessionParser({ path, project: 'app', provider: 'kimi' }, seen) + const calls: ParsedProviderCall[] = [] + for await (const call of parser.parse()) calls.push(call) + return calls +} + +describe('Kimi provider', () => { + it('discovers session and subagent wire logs under KIMI_SHARE_DIR layout', async () => { + const workDir = '/Users/test/work/app' + const hash = md5(workDir) + await writeFile(join(tmpDir, 'kimi.json'), JSON.stringify({ + work_dirs: [{ path: workDir, kaos: 'local', last_session_id: 'sess-1' }], + })) + + const sessionDir = join(tmpDir, 'sessions', hash, 'sess-1') + const subagentDir = join(sessionDir, 'subagents', 'agent-1') + await mkdir(subagentDir, { recursive: true }) + await writeFile(join(sessionDir, 'wire.jsonl'), '\n') + await writeFile(join(subagentDir, 'wire.jsonl'), '\n') + + const sources = await createKimiProvider(tmpDir).discoverSessions() + + expect(sources).toHaveLength(2) + expect(sources.map(s => s.project)).toEqual(['app', 'app']) + expect(sources.map(s => s.provider)).toEqual(['kimi', 'kimi']) + expect(sources.map(s => s.path).sort()).toEqual([ + join(sessionDir, 'subagents', 'agent-1', 'wire.jsonl'), + join(sessionDir, 'wire.jsonl'), + ].sort()) + }) + + it('parses Kimi wire StatusUpdate usage, tools, bash commands, and configured model', async () => { + await writeFile(join(tmpDir, 'config.toml'), [ + 'default_model = "kimi-code/k2"', + '', + '[models."kimi-code/k2"]', + 'model = "kimi-k2-thinking-turbo"', + ].join('\n')) + + const wirePath = await writeSession('/Users/test/work/app', 'sess-1', [ + record(1776162400, 'TurnBegin', { user_input: 'add status endpoint' }), + record(1776162401, 'ToolCall', { + type: 'function', + id: 'call-shell', + function: { name: 'Shell', arguments: JSON.stringify({ command: 'git status && npm test' }) }, + }), + record(1776162402, 'ToolCall', { + type: 'function', + id: 'call-read', + function: { name: 'ReadFile', arguments: JSON.stringify({ path: 'src/index.ts' }) }, + }), + record(1776162403, 'StatusUpdate', { + message_id: 'msg-1', + token_usage: { + input_other: 100, + input_cache_read: 25, + input_cache_creation: 10, + output: 40, + }, + }), + ]) + + const calls = await collect(createKimiProvider(tmpDir), wirePath) + + expect(calls).toHaveLength(1) + expect(calls[0]).toMatchObject({ + provider: 'kimi', + model: 'kimi-k2-thinking-turbo', + inputTokens: 100, + outputTokens: 40, + cacheReadInputTokens: 25, + cacheCreationInputTokens: 10, + cachedInputTokens: 25, + tools: ['Bash', 'Read'], + bashCommands: ['git', 'npm'], + timestamp: '2026-04-14T10:26:43.000Z', + deduplicationKey: 'kimi:sess-1:msg-1', + userMessage: 'add status endpoint', + sessionId: 'sess-1', + }) + expect(calls[0]!.costUSD).toBeGreaterThan(0) + }) + + it('uses content parts, model payload overrides, and message-id deduplication', async () => { + process.env.KIMI_MODEL_NAME = 'kimi-k2-thinking' + const wirePath = await writeSession('/Users/test/work/app', 'sess-2', [ + record(1776023300, 'TurnBegin', { + user_input: [ + { type: 'text', text: 'refactor parser' }, + { type: 'image_url', image_url: { url: 'file://diagram.png' } }, + { type: 'text', text: 'carefully' }, + ], + }), + record(1776023301, 'ToolCallRequest', { + id: 'call-write', + name: 'WriteFile', + arguments: JSON.stringify({ path: 'src/parser.ts', content: 'x' }), + }), + record(1776023302, 'StatusUpdate', { + message_id: 'msg-2', + model_name: 'kimi-k2.6', + token_usage: { input_other: 5, output: 7 }, + }), + record(1776023303, 'StatusUpdate', { + message_id: 'msg-2', + model_name: 'kimi-k2.6', + token_usage: { input_other: 5, output: 7 }, + }), + ]) + + const calls = await collect(createKimiProvider(tmpDir), wirePath) + + expect(calls).toHaveLength(1) + expect(calls[0]).toMatchObject({ + model: 'kimi-k2.6', + userMessage: 'refactor parser carefully', + tools: ['Write'], + deduplicationKey: 'kimi:sess-2:msg-2', + }) + }) + + it('skips non-usage updates and supports legacy input total fields defensively', async () => { + const wirePath = await writeSession('/Users/test/work/app', 'sess-3', [ + record(1776023400, 'TurnBegin', { user_input: 'summarize' }), + record(1776023401, 'StatusUpdate', { context_usage: 0.5 }), + record(1776023402, 'StatusUpdate', { + message_id: 'msg-3', + token_usage: { + input: 120, + input_cache_read: 30, + input_cache_creation: 10, + output_tokens: 20, + }, + }), + ]) + + const calls = await collect(createKimiProvider(tmpDir), wirePath) + + expect(calls).toHaveLength(1) + expect(calls[0]).toMatchObject({ + inputTokens: 80, + cacheReadInputTokens: 30, + cacheCreationInputTokens: 10, + outputTokens: 20, + model: 'kimi-auto', + }) + }) +}) diff --git a/tests/providers/mistral-vibe.test.ts b/tests/providers/mistral-vibe.test.ts new file mode 100644 index 00000000..51bc03c0 --- /dev/null +++ b/tests/providers/mistral-vibe.test.ts @@ -0,0 +1,305 @@ +import { describe, it, expect, beforeEach, afterEach } from 'vitest' +import { mkdtemp, mkdir, writeFile, rm } from 'fs/promises' +import { join } from 'path' +import { tmpdir } from 'os' + +import { createMistralVibeProvider } from '../../src/providers/mistral-vibe.js' +import type { ParsedProviderCall } from '../../src/providers/types.js' + +let tmpDir: string +let originalVibeHome: string | undefined + +beforeEach(async () => { + tmpDir = await mkdtemp(join(tmpdir(), 'mistral-vibe-test-')) + originalVibeHome = process.env['VIBE_HOME'] + delete process.env['VIBE_HOME'] +}) + +afterEach(async () => { + if (originalVibeHome === undefined) { + delete process.env['VIBE_HOME'] + } else { + process.env['VIBE_HOME'] = originalVibeHome + } + await rm(tmpDir, { recursive: true, force: true }) +}) + +function metadata(opts: { + sessionId?: string + cwd?: string + input?: number + output?: number + inputPrice?: number + outputPrice?: number + activeModel?: string + modelName?: string + configInputPrice?: number + configOutputPrice?: number + endTime?: string | null + title?: string +} = {}) { + const activeModel = opts.activeModel ?? 'mistral-medium-3.5' + return { + session_id: opts.sessionId ?? 'session-abc123', + start_time: '2026-05-11T10:00:00+00:00', + end_time: Object.hasOwn(opts, 'endTime') ? opts.endTime : '2026-05-11T10:05:00+00:00', + environment: { + working_directory: opts.cwd ?? '/Users/test/mistral-project', + }, + stats: { + session_prompt_tokens: opts.input ?? 2000, + session_completion_tokens: opts.output ?? 3000, + input_price_per_million: opts.inputPrice ?? 1.5, + output_price_per_million: opts.outputPrice ?? 7.5, + tokens_per_second: 42, + }, + config: { + active_model: activeModel, + models: [ + { + alias: activeModel, + name: opts.modelName ?? 'mistral-vibe-cli-latest', + provider: 'mistral', + input_price: opts.configInputPrice ?? 1.5, + output_price: opts.configOutputPrice ?? 7.5, + }, + ], + }, + title: opts.title ?? 'implement mistral support', + total_messages: 2, + } +} + +function userMessage(content: unknown = 'implement mistral support') { + return { + role: 'user', + content, + message_id: 'msg-user-1', + } +} + +function assistantMessage(toolCalls: Array<{ name: string; args?: Record | string }> = []) { + return { + role: 'assistant', + content: 'Done', + message_id: 'msg-assistant-1', + tool_calls: toolCalls.map((call, idx) => ({ + id: `tool-${idx}`, + type: 'function', + function: { + name: call.name, + arguments: typeof call.args === 'string' ? call.args : JSON.stringify(call.args ?? {}), + }, + })), + } +} + +async function writeSession( + name: string, + meta: Record, + messages = [userMessage(), assistantMessage()], + root = tmpDir, +) { + const sessionDir = join(root, name) + await mkdir(sessionDir, { recursive: true }) + await writeFile(join(sessionDir, 'meta.json'), JSON.stringify(meta, null, 2)) + await writeFile(join(sessionDir, 'messages.jsonl'), messages.map(m => JSON.stringify(m)).join('\n') + '\n') + return sessionDir +} + +async function collect(sourcePath: string, provider = createMistralVibeProvider(tmpDir)): Promise { + const calls: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser({ + path: sourcePath, + project: 'mistral-project', + provider: 'mistral-vibe', + }, new Set()).parse()) { + calls.push(call) + } + return calls +} + +describe('mistral-vibe provider - session discovery', () => { + it('discovers Vibe session folders and derives project from metadata cwd', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata({ + sessionId: 'session-a', + cwd: '/Users/test/project-a', + })) + await mkdir(join(tmpDir, 'not-a-session'), { recursive: true }) + + const provider = createMistralVibeProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]).toEqual({ + path: sessionDir, + project: 'project-a', + provider: 'mistral-vibe', + }) + }) + + it('discovers subagent session folders nested under agents', async () => { + const parentDir = await writeSession('session_20260511_100000_parent', metadata({ + sessionId: 'parent-session', + cwd: '/Users/test/parent-project', + })) + const childDir = await writeSession('session_20260511_100001_child', metadata({ + sessionId: 'child-session', + cwd: '/Users/test/child-project', + }), [userMessage('child task'), assistantMessage()], join(parentDir, 'agents')) + + const provider = createMistralVibeProvider(tmpDir) + const sessions = await provider.discoverSessions() + + expect(sessions.map(s => s.path).sort()).toEqual([childDir, parentDir].sort()) + expect(sessions.map(s => s.project).sort()).toEqual(['child-project', 'parent-project']) + }) + + it('returns empty for a missing Vibe sessions directory', async () => { + const provider = createMistralVibeProvider('/missing/vibe/logs/session') + await expect(provider.discoverSessions()).resolves.toEqual([]) + }) + + it('uses VIBE_HOME when no override directory is provided', async () => { + const vibeHome = join(tmpDir, 'vibe-home') + process.env['VIBE_HOME'] = vibeHome + const sessionsDir = join(vibeHome, 'logs', 'session') + await writeSession('session_20260511_100000_sessiona', metadata({ + sessionId: 'env-session', + cwd: '/Users/test/env-project', + }), [userMessage(), assistantMessage()], sessionsDir) + + const provider = createMistralVibeProvider() + const sessions = await provider.discoverSessions() + + expect(sessions).toHaveLength(1) + expect(sessions[0]!.project).toBe('env-project') + }) +}) + +describe('mistral-vibe provider - parsing', () => { + it('parses cumulative session usage, tools, bash commands, and first user message', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata(), [ + userMessage([{ type: 'text', text: 'track Mistral Vibe usage' }]), + assistantMessage([ + { name: 'read_file', args: { path: 'src/index.ts' } }, + { name: 'search_replace', args: { file_path: 'src/index.ts', content: 'patch' } }, + { name: 'bash', args: { command: 'npm test && git status' } }, + ]), + ]) + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + + expect(calls).toHaveLength(1) + const call = calls[0]! + expect(call.provider).toBe('mistral-vibe') + expect(call.model).toBe('mistral-medium-3.5') + expect(call.inputTokens).toBe(2000) + expect(call.outputTokens).toBe(3000) + expect(call.costUSD).toBeCloseTo(0.0255, 8) + expect(call.tools).toEqual(['Read', 'Edit', 'Bash']) + expect(call.bashCommands).toEqual(['npm', 'git']) + expect(call.timestamp).toBe('2026-05-11T10:05:00+00:00') + expect(call.userMessage).toBe('track Mistral Vibe usage') + expect(call.sessionId).toBe('session-abc123') + expect(call.deduplicationKey).toBe('mistral-vibe:session-abc123') + }) + + it('uses configured model prices when stats omit prices', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata({ + inputPrice: 0, + outputPrice: 0, + input: 1000, + output: 1000, + })) + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + + expect(calls).toHaveLength(1) + expect(calls[0]!.costUSD).toBeCloseTo(0.009, 8) + }) + + it('falls back to LiteLLM pricing when Vibe does not provide prices', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata({ + activeModel: 'claude-sonnet-4-6', + modelName: 'claude-sonnet-4-6', + input: 1000, + output: 1000, + inputPrice: 0, + outputPrice: 0, + configInputPrice: 0, + configOutputPrice: 0, + })) + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + + expect(calls).toHaveLength(1) + expect(calls[0]!.costUSD).toBeCloseTo(0.018, 8) + }) + + it('falls back to start_time when end_time is missing', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata({ + endTime: null, + })) + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + + expect(calls[0]!.timestamp).toBe('2026-05-11T10:00:00+00:00') + }) + + it('deduplicates by session id', async () => { + const sessionDir = await writeSession('session_20260511_100000_sessiona', metadata()) + const provider = createMistralVibeProvider(tmpDir) + const source = { path: sessionDir, project: 'mistral-project', provider: 'mistral-vibe' } + const seen = new Set() + + const first: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, seen).parse()) first.push(call) + const second: ParsedProviderCall[] = [] + for await (const call of provider.createSessionParser(source, seen).parse()) second.push(call) + + expect(first).toHaveLength(1) + expect(second).toHaveLength(0) + }) + + it('skips sessions without cumulative token usage', async () => { + const sessionDir = await writeSession('session_20260511_100000_empty', metadata({ + input: 0, + output: 0, + })) + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + + expect(calls).toEqual([]) + }) + + it('skips sessions with malformed meta.json', async () => { + const sessionDir = join(tmpDir, 'session_20260511_100000_bad') + await mkdir(sessionDir, { recursive: true }) + await writeFile(join(sessionDir, 'meta.json'), '{{not json') + await writeFile(join(sessionDir, 'messages.jsonl'), JSON.stringify(userMessage()) + '\n') + + const provider = createMistralVibeProvider(tmpDir) + const sessions = await provider.discoverSessions() + expect(sessions).toHaveLength(0) + }) + + it('returns empty calls when messages.jsonl is malformed', async () => { + const sessionDir = await writeSession('session_20260511_100000_badjsonl', metadata()) + await writeFile(join(sessionDir, 'messages.jsonl'), '{{not json\n{{also bad\n') + + const calls = await collect(sessionDir, createMistralVibeProvider(tmpDir)) + expect(calls).toHaveLength(1) + expect(calls[0]!.tools).toEqual([]) + expect(calls[0]!.bashCommands).toEqual([]) + }) + + it('formats model and tool display names', () => { + const provider = createMistralVibeProvider(tmpDir) + + expect(provider.modelDisplayName('mistral-medium-3.5')).toBe('Mistral Medium 3.5') + expect(provider.modelDisplayName('devstral-small-latest')).toBe('Devstral Small') + expect(provider.toolDisplayName('search_replace')).toBe('Edit') + expect(provider.toolDisplayName('unknown_tool')).toBe('unknown_tool') + }) +}) diff --git a/tests/providers/opencode.test.ts b/tests/providers/opencode.test.ts index bd715bee..3637b79c 100644 --- a/tests/providers/opencode.test.ts +++ b/tests/providers/opencode.test.ts @@ -337,6 +337,124 @@ skipUnlessSqlite('opencode provider - session parsing', () => { expect(call.deduplicationKey).toBe('opencode:sess-1:msg-2') }) + it('normalizes opencode MCP tool names for shared MCP reporting', async () => { + const dbPath = createTestDb(tmpDir) + withTestDb(dbPath, (db) => { + insertSession(db, 'sess-1') + + insertMessage(db, 'msg-1', 'sess-1', 1700000000000, { role: 'user' }) + insertPart(db, 'part-1', 'msg-1', 'sess-1', { type: 'text', text: 'look up the ClickUp task' }) + + insertMessage(db, 'msg-2', 'sess-1', 1700000001000, { + role: 'assistant', + modelID: 'claude-opus-4-6', + cost: 0.05, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + insertPart(db, 'part-2', 'msg-2', 'sess-1', { + type: 'tool', + tool: 'clickup_clickup_get_task', + state: { status: 'completed', input: {} }, + }) + insertPart(db, 'part-3', 'msg-2', 'sess-1', { + type: 'tool', + tool: 'figma_get_file', + state: { status: 'completed', input: {} }, + }) + }) + + const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1') + + expect(calls).toHaveLength(1) + expect(calls[0]!.tools).toEqual([ + 'mcp__clickup__clickup_get_task', + 'mcp__figma__get_file', + ]) + }) + + it('preserves already-normalized MCP tool names', async () => { + const dbPath = createTestDb(tmpDir) + withTestDb(dbPath, (db) => { + insertSession(db, 'sess-1') + insertMessage(db, 'msg-1', 'sess-1', 1700000001000, { + role: 'assistant', + modelID: 'claude-opus-4-6', + cost: 0.05, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + insertPart(db, 'part-1', 'msg-1', 'sess-1', { + type: 'tool', + tool: 'mcp__github__search_code', + state: { status: 'completed', input: {} }, + }) + }) + + const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1') + + expect(calls).toHaveLength(1) + expect(calls[0]!.tools).toEqual(['mcp__github__search_code']) + }) + + it('keeps extension tool names without a server prefix as regular tools', async () => { + const dbPath = createTestDb(tmpDir) + withTestDb(dbPath, (db) => { + insertSession(db, 'sess-1') + insertMessage(db, 'msg-1', 'sess-1', 1700000001000, { + role: 'assistant', + modelID: 'claude-opus-4-6', + cost: 0.05, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + insertPart(db, 'part-1', 'msg-1', 'sess-1', { + type: 'tool', + tool: 'customtool', + state: { status: 'completed', input: {} }, + }) + }) + + const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1') + + expect(calls).toHaveLength(1) + expect(calls[0]!.tools).toEqual(['customtool']) + }) + + it('keeps malformed server-prefixed tool names as regular tools', async () => { + const dbPath = createTestDb(tmpDir) + withTestDb(dbPath, (db) => { + insertSession(db, 'sess-1') + insertMessage(db, 'msg-1', 'sess-1', 1700000001000, { + role: 'assistant', + modelID: 'claude-opus-4-6', + cost: 0.05, + tokens: { input: 100, output: 200, reasoning: 0, cache: { read: 0, write: 0 } }, + }) + insertPart(db, 'part-1', 'msg-1', 'sess-1', { + type: 'tool', + tool: '_missing_server', + state: { status: 'completed', input: {} }, + }) + insertPart(db, 'part-2', 'msg-1', 'sess-1', { + type: 'tool', + tool: 'missing_', + state: { status: 'completed', input: {} }, + }) + insertPart(db, 'part-3', 'msg-1', 'sess-1', { + type: 'tool', + tool: '_', + state: { status: 'completed', input: {} }, + }) + }) + + const calls = await collectCalls(createOpenCodeProvider(tmpDir), dbPath, 'sess-1') + + expect(calls).toHaveLength(1) + expect(calls[0]!.tools).toEqual([ + '_missing_server', + 'missing_', + '_', + ]) + }) + it('skips zero-token messages with zero cost', async () => { const dbPath = createTestDb(tmpDir) withTestDb(dbPath, (db) => { diff --git a/tests/session-cache.test.ts b/tests/session-cache.test.ts new file mode 100644 index 00000000..8da5153f --- /dev/null +++ b/tests/session-cache.test.ts @@ -0,0 +1,509 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest' +import { readFile, rm, writeFile, mkdir } from 'fs/promises' +import { existsSync } from 'fs' +import { tmpdir } from 'os' +import { join } from 'path' + +import { + CACHE_VERSION, + type CachedCall, + type CachedFile, + type CachedTurn, + type FileFingerprint, + type SessionCache, + cleanupOrphanedTempFiles, + computeEnvFingerprint, + emptyCache, + fingerprintFile, + loadCache, + mergeCallByDedupKey, + reconcileFile, + saveCache, +} from '../src/session-cache.js' + +const TMP_DIR = join(tmpdir(), `codeburn-scache-test-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`) + +beforeEach(() => { + process.env['CODEBURN_CACHE_DIR'] = TMP_DIR +}) + +afterEach(async () => { + delete process.env['CODEBURN_CACHE_DIR'] + if (existsSync(TMP_DIR)) await rm(TMP_DIR, { recursive: true }) +}) + +function makeCall(overrides: Partial = {}): CachedCall { + return { + provider: 'claude', + model: 'claude-sonnet-4-20250514', + usage: { + inputTokens: 1000, + outputTokens: 500, + cacheCreationInputTokens: 0, + cacheReadInputTokens: 0, + cachedInputTokens: 0, + reasoningTokens: 0, + webSearchRequests: 0, + cacheCreationOneHourTokens: 0, + }, + speed: 'standard', + timestamp: '2026-05-15T10:00:00Z', + tools: ['Read', 'Edit'], + bashCommands: [], + skills: [], + deduplicationKey: 'msg-abc123', + ...overrides, + } +} + +function makeTurn(overrides: Partial = {}): CachedTurn { + return { + timestamp: '2026-05-15T10:00:00Z', + sessionId: 'sess-1', + userMessage: 'fix the bug', + calls: [makeCall()], + ...overrides, + } +} + +function makeCachedFile(overrides: Partial = {}): CachedFile { + return { + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + mcpInventory: [], + turns: [makeTurn()], + ...overrides, + } +} + +// ── emptyCache ───────────────────────────────────────────────────────── + +describe('emptyCache', () => { + it('returns a valid empty cache', () => { + const cache = emptyCache() + expect(cache.version).toBe(CACHE_VERSION) + expect(cache.providers).toEqual({}) + }) +}) + +// ── loadCache / saveCache ────────────────────────────────────────────── + +describe('loadCache / saveCache', () => { + it('returns empty cache when no file exists', async () => { + const cache = await loadCache() + expect(cache.version).toBe(CACHE_VERSION) + expect(cache.providers).toEqual({}) + }) + + it('round-trips a cache through save and load', async () => { + const cache: SessionCache = { + version: CACHE_VERSION, + providers: { + claude: { + envFingerprint: 'abc123', + files: { + '/path/to/session.jsonl': makeCachedFile(), + }, + }, + }, + } + + await saveCache(cache) + const loaded = await loadCache() + expect(loaded).toEqual(cache) + }) + + it('returns empty cache on version mismatch', async () => { + const bad: SessionCache = { version: 999, providers: { claude: { envFingerprint: 'x', files: {} } } } + await mkdir(TMP_DIR, { recursive: true }) + await writeFile(join(TMP_DIR, 'session-cache.json'), JSON.stringify(bad)) + + const loaded = await loadCache() + expect(loaded.version).toBe(CACHE_VERSION) + expect(loaded.providers).toEqual({}) + }) + + it('returns empty cache on corrupt JSON', async () => { + await mkdir(TMP_DIR, { recursive: true }) + await writeFile(join(TMP_DIR, 'session-cache.json'), '{broken') + + const loaded = await loadCache() + expect(loaded.version).toBe(CACHE_VERSION) + expect(loaded.providers).toEqual({}) + }) + + it('atomic write does not leave partial file on error', async () => { + await saveCache(emptyCache()) + const raw = await readFile(join(TMP_DIR, 'session-cache.json'), 'utf-8') + expect(JSON.parse(raw)).toEqual(emptyCache()) + }) +}) + +// ── computeEnvFingerprint ────────────────────────────────────────────── + +describe('computeEnvFingerprint', () => { + it('returns stable hash for same env', () => { + const a = computeEnvFingerprint('claude') + const b = computeEnvFingerprint('claude') + expect(a).toBe(b) + expect(a).toHaveLength(16) + }) + + it('changes when env var changes', () => { + const before = computeEnvFingerprint('claude') + const orig = process.env['CLAUDE_CONFIG_DIR'] + process.env['CLAUDE_CONFIG_DIR'] = '/tmp/different' + const after = computeEnvFingerprint('claude') + if (orig === undefined) delete process.env['CLAUDE_CONFIG_DIR'] + else process.env['CLAUDE_CONFIG_DIR'] = orig + expect(before).not.toBe(after) + }) + + it('returns stable hash for unknown provider (no env vars)', () => { + const a = computeEnvFingerprint('unknown-provider') + const b = computeEnvFingerprint('unknown-provider') + expect(a).toBe(b) + }) +}) + +// ── fingerprintFile ──────────────────────────────────────────────────── + +describe('fingerprintFile', () => { + it('returns fingerprint for existing file', async () => { + await mkdir(TMP_DIR, { recursive: true }) + const filePath = join(TMP_DIR, 'test.jsonl') + await writeFile(filePath, 'line1\nline2\n') + + const fp = await fingerprintFile(filePath) + expect(fp).not.toBeNull() + expect(fp!.sizeBytes).toBe(12) + expect(fp!.dev).toBeGreaterThan(0) + expect(fp!.ino).toBeGreaterThan(0) + expect(fp!.mtimeMs).toBeGreaterThan(0) + }) + + it('returns null for non-existent file', async () => { + const fp = await fingerprintFile('/no/such/file') + expect(fp).toBeNull() + }) +}) + +// ── reconcileFile ────────────────────────────────────────────────────── + +describe('reconcileFile', () => { + it('returns "new" when no cached entry', () => { + const fp: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 } + expect(reconcileFile(fp, undefined)).toEqual({ action: 'new' }) + }) + + it('returns "unchanged" when all fields match', () => { + const fp: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 } + const cached = makeCachedFile({ fingerprint: { ...fp } }) + expect(reconcileFile(fp, cached)).toEqual({ action: 'unchanged' }) + }) + + it('returns "appended" when ino same, size grew, and has lastCompleteLineOffset', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + lastCompleteLineOffset: 4500, + }) + const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 8000 } + const result = reconcileFile(current, cached) + expect(result).toEqual({ action: 'appended', readFromOffset: 4500 }) + }) + + it('returns "modified" when ino changed', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + }) + const current: FileFingerprint = { dev: 1, ino: 200, mtimeMs: 2000, sizeBytes: 5000 } + expect(reconcileFile(current, cached)).toEqual({ action: 'modified' }) + }) + + it('returns "modified" when size shrank', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + lastCompleteLineOffset: 4500, + }) + const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 3000 } + expect(reconcileFile(current, cached)).toEqual({ action: 'modified' }) + }) + + it('returns "modified" when same size but different mtime', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + }) + const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 5000 } + expect(reconcileFile(current, cached)).toEqual({ action: 'modified' }) + }) + + it('returns "modified" for DB provider (no lastCompleteLineOffset) on any fingerprint change', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + }) + const current: FileFingerprint = { dev: 1, ino: 100, mtimeMs: 2000, sizeBytes: 8000 } + expect(reconcileFile(current, cached)).toEqual({ action: 'modified' }) + }) + + it('returns "modified" when dev changed even if ino same and size grew', () => { + const cached = makeCachedFile({ + fingerprint: { dev: 1, ino: 100, mtimeMs: 1000, sizeBytes: 5000 }, + lastCompleteLineOffset: 4500, + }) + const current: FileFingerprint = { dev: 2, ino: 100, mtimeMs: 2000, sizeBytes: 8000 } + expect(reconcileFile(current, cached)).toEqual({ action: 'modified' }) + }) +}) + +// ── mergeCallByDedupKey ──────────────────────────────────────────────── + +describe('mergeCallByDedupKey', () => { + it('keeps earlier timestamp', () => { + const existing = makeCall({ timestamp: '2026-05-15T10:00:00Z' }) + const incoming = makeCall({ timestamp: '2026-05-15T10:01:00Z' }) + const merged = mergeCallByDedupKey(existing, incoming) + expect(merged.timestamp).toBe('2026-05-15T10:00:00Z') + }) + + it('takes incoming usage (latest wins)', () => { + const existing = makeCall({ usage: { ...makeCall().usage, outputTokens: 100 } }) + const incoming = makeCall({ usage: { ...makeCall().usage, outputTokens: 999 } }) + const merged = mergeCallByDedupKey(existing, incoming) + expect(merged.usage.outputTokens).toBe(999) + }) + + it('takes incoming tools (latest wins)', () => { + const existing = makeCall({ tools: ['Read'] }) + const incoming = makeCall({ tools: ['Read', 'Edit', 'Bash'] }) + const merged = mergeCallByDedupKey(existing, incoming) + expect(merged.tools).toEqual(['Read', 'Edit', 'Bash']) + }) +}) + +// ── deep validation (loadCache) ──────────────────────────────────────── + +describe('loadCache validation', () => { + async function writeRawCache(data: unknown): Promise { + await mkdir(TMP_DIR, { recursive: true }) + await writeFile(join(TMP_DIR, 'session-cache.json'), JSON.stringify(data)) + } + + it('rejects providers as array', async () => { + await writeRawCache({ version: CACHE_VERSION, providers: [] }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects provider section missing envFingerprint', async () => { + await writeRawCache({ version: CACHE_VERSION, providers: { claude: { files: {} } } }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects provider section with files as array', async () => { + await writeRawCache({ version: CACHE_VERSION, providers: { claude: { envFingerprint: 'x', files: [] } } }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects file with invalid fingerprint (missing ino)', async () => { + await writeRawCache({ + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, mtimeMs: 1, sizeBytes: 1 }, mcpInventory: [], turns: [] }, + } } }, + }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects file with non-numeric fingerprint field', async () => { + await writeRawCache({ + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 'bad', mtimeMs: 1, sizeBytes: 1 }, mcpInventory: [], turns: [] }, + } } }, + }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects turn with missing sessionId', async () => { + const badTurn = { timestamp: 'x', userMessage: 'y', calls: [] } + await writeRawCache({ + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [badTurn] }, + } } }, + }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects call with missing usage object', async () => { + const badCall = { provider: 'claude', model: 'm', deduplicationKey: 'k', timestamp: 't', tools: [], bashCommands: [], skills: [] } + const turn = { timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [badCall] } + await writeRawCache({ + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [turn] }, + } } }, + }) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects call with NaN in usage', async () => { + const badUsage = { inputTokens: NaN, outputTokens: 0, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0 } + const call = { provider: 'claude', model: 'm', usage: badUsage, deduplicationKey: 'k', timestamp: 't', tools: [], bashCommands: [], skills: [], speed: 'standard' } + const turn = { timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [call] } + await writeRawCache({ + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [turn] }, + } } }, + }) + expect((await loadCache()).providers).toEqual({}) + }) + + function validCallJson() { + return { + provider: 'claude', model: 'm', deduplicationKey: 'k', timestamp: 't', speed: 'standard', + tools: ['Read'], bashCommands: ['ls'], skills: [], + usage: { inputTokens: 1, outputTokens: 1, cacheCreationInputTokens: 0, cacheReadInputTokens: 0, cachedInputTokens: 0, reasoningTokens: 0, webSearchRequests: 0, cacheCreationOneHourTokens: 0 }, + } + } + + function wrapCall(callOverride: Record) { + return { + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [ + { timestamp: 'x', sessionId: 's', userMessage: 'y', calls: [{ ...validCallJson(), ...callOverride }] }, + ] }, + } } }, + } + } + + function wrapFile(fileOverride: Record) { + return { + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [], ...fileOverride }, + } } }, + } + } + + it('rejects tools containing non-string element', async () => { + await writeRawCache(wrapCall({ tools: ['Read', 42] })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects bashCommands containing object element', async () => { + await writeRawCache(wrapCall({ bashCommands: [{}] })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects skills containing null element', async () => { + await writeRawCache(wrapCall({ skills: [null] })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects invalid speed value', async () => { + await writeRawCache(wrapCall({ speed: 'turbo' })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects non-string project', async () => { + await writeRawCache(wrapCall({ project: 123 })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects non-string projectPath', async () => { + await writeRawCache(wrapCall({ projectPath: true })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects mcpInventory containing non-string element', async () => { + await writeRawCache(wrapFile({ mcpInventory: ['valid', 99] })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects non-numeric lastCompleteLineOffset', async () => { + await writeRawCache(wrapFile({ lastCompleteLineOffset: 'bad' })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects NaN lastCompleteLineOffset', async () => { + await writeRawCache(wrapFile({ lastCompleteLineOffset: null })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('rejects non-string canonicalCwd', async () => { + await writeRawCache(wrapFile({ canonicalCwd: 42 })) + expect((await loadCache()).providers).toEqual({}) + }) + + it('accepts optional fields when absent', async () => { + const cache: SessionCache = { + version: CACHE_VERSION, + providers: { claude: { envFingerprint: 'x', files: { + '/f': { fingerprint: { dev: 1, ino: 2, mtimeMs: 3, sizeBytes: 4 }, mcpInventory: [], turns: [] }, + } } }, + } + await writeRawCache(cache) + expect((await loadCache())).toEqual(cache) + }) + + it('accepts a fully valid cache with all fields populated', async () => { + const cache: SessionCache = { + version: CACHE_VERSION, + providers: { + claude: { + envFingerprint: 'abc', + files: { '/f': makeCachedFile() }, + }, + }, + } + await writeRawCache(cache) + const loaded = await loadCache() + expect(loaded).toEqual(cache) + }) +}) + +// ── cleanupOrphanedTempFiles ─────────────────────────────────────────── + +describe('cleanupOrphanedTempFiles', () => { + it('removes .tmp files older than 5 minutes', async () => { + await mkdir(TMP_DIR, { recursive: true }) + + const oldTmp = join(TMP_DIR, 'session-cache.json.abc123.tmp') + await writeFile(oldTmp, 'stale') + const { utimes } = await import('fs/promises') + const oldTime = new Date(Date.now() - 10 * 60 * 1000) + await utimes(oldTmp, oldTime, oldTime) + + await cleanupOrphanedTempFiles() + expect(existsSync(oldTmp)).toBe(false) + }) + + it('preserves recent .tmp files', async () => { + await mkdir(TMP_DIR, { recursive: true }) + + const recentTmp = join(TMP_DIR, 'session-cache.json.def456.tmp') + await writeFile(recentTmp, 'recent') + + await cleanupOrphanedTempFiles() + expect(existsSync(recentTmp)).toBe(true) + }) + + it('ignores .tmp files from other caches', async () => { + await mkdir(TMP_DIR, { recursive: true }) + + const otherTmp = join(TMP_DIR, 'codex-results.json.abc123.tmp') + await writeFile(otherTmp, 'other cache temp') + const { utimes } = await import('fs/promises') + const oldTime = new Date(Date.now() - 10 * 60 * 1000) + await utimes(otherTmp, oldTime, oldTime) + + await cleanupOrphanedTempFiles() + expect(existsSync(otherTmp)).toBe(true) + }) + + it('does not fail when cache dir does not exist', async () => { + process.env['CODEBURN_CACHE_DIR'] = '/no/such/dir' + await cleanupOrphanedTempFiles() + }) +}) diff --git a/tsup.config.ts b/tsup.config.ts index 2ba26c58..957fdce8 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -1,7 +1,7 @@ import { defineConfig } from 'tsup' export default defineConfig({ - entry: ['src/cli.ts'], + entry: ['src/main.ts'], format: ['esm'], target: 'node20', outDir: 'dist', @@ -9,7 +9,4 @@ export default defineConfig({ splitting: false, sourcemap: true, dts: false, - banner: { - js: '#!/usr/bin/env node', - }, })