From a7f2917cd8cffb8cb5dde824dce006dc34783a07 Mon Sep 17 00:00:00 2001 From: Marcin Tojek Date: Tue, 11 Aug 2026 10:05:33 +0200 Subject: [PATCH 01/12] docs(docs/install/releases): backport release calendar to release/2.36 (#28011) Backport the current release calendar from `main` to `release/2.36` so the versioned `v2.36` docs page shows the correct schedule instead of the stale one frozen when the branch was cut. _Opened as a draft by Coder Agents on behalf of @mtojek._ --- docs/install/releases/index.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/install/releases/index.md b/docs/install/releases/index.md index d89a7d4f190d8..e3d41a67f63e1 100644 --- a/docs/install/releases/index.md +++ b/docs/install/releases/index.md @@ -80,13 +80,13 @@ pages. | Release name | Release Date | Status | Latest Release | |--------------------------------------------------|-------------------|--------------------------|------------------------------------------------------------------| | [2.29](https://coder.com/changelog/coder-2-29) | December 02, 2025 | Extended Support Release | [v2.29.19](https://github.com/coder/coder/releases/tag/v2.29.19) | -| [2.30](https://coder.com/changelog/coder-2-30) | February 03, 2026 | Not Supported | [v2.30.9](https://github.com/coder/coder/releases/tag/v2.30.9) | | [2.31](https://coder.com/changelog/coder-2-31) | February 23, 2026 | Not Supported | [v2.31.14](https://github.com/coder/coder/releases/tag/v2.31.14) | | [2.32](https://coder.com/changelog/coder-2-32) | April 14, 2026 | Not Supported | [v2.32.10](https://github.com/coder/coder/releases/tag/v2.32.10) | -| [2.33](https://coder.com/changelog/coder-2-33) | May 05, 2026 | Security Support | [v2.33.11](https://github.com/coder/coder/releases/tag/v2.33.11) | -| [2.34](https://coder.com/changelog/coder-2-34) | June 02, 2026 | Stable (ESR) | [v2.34.6](https://github.com/coder/coder/releases/tag/v2.34.6) | -| [2.35](https://coder.com/changelog/coder-2-35-1) | July 07, 2026 | Mainline | [v2.35.2](https://github.com/coder/coder/releases/tag/v2.35.2) | -| 2.36 | | Not Released | N/A | +| [2.33](https://coder.com/changelog/coder-2-33) | May 05, 2026 | Not Supported | [v2.33.11](https://github.com/coder/coder/releases/tag/v2.33.11) | +| [2.34](https://coder.com/changelog/coder-2-34) | June 02, 2026 | Security Support | [v2.34.8](https://github.com/coder/coder/releases/tag/v2.34.8) | +| [2.35](https://coder.com/changelog/coder-2-35-1) | July 07, 2026 | Stable | [v2.35.4](https://github.com/coder/coder/releases/tag/v2.35.4) | +| [2.36](https://coder.com/changelog/coder-2-36) | August 04, 2026 | Mainline | [v2.36.0](https://github.com/coder/coder/releases/tag/v2.36.0) | +| 2.37 | | Not Released | N/A | > [!TIP] From eca838de839f7f5394136b2d62dc3f2652cdfed2 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:50:38 -0400 Subject: [PATCH 02/12] docs: fix P2/P3 typos and syntax errors from drift sweep (#28101) (#28156) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28101 Original PR: #28101 — docs: fix P2/P3 typos and syntax errors from drift sweep Merge commit: 1d189cc204f935cac6ce16de162fd7b897652452 Requested by: @nickvigilante Co-authored-by: Nick Vigilante --- docs/about/contributing/modules.md | 2 +- docs/admin/networking/port-forwarding.md | 2 +- docs/admin/templates/extending-templates/variables.md | 2 +- docs/admin/users/idp-sync.md | 4 ++-- docs/ai-coder/tasks-migration.md | 6 +++--- docs/user-guides/workspace-access/index.md | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/about/contributing/modules.md b/docs/about/contributing/modules.md index 4c91a04da126b..d04616d02ed13 100644 --- a/docs/about/contributing/modules.md +++ b/docs/about/contributing/modules.md @@ -342,7 +342,7 @@ Use the version bump script to update versions: ```sh git add . - git commit -m "feat(git-clone):add git-clone module" + git commit -m "feat(git-clone): add git-clone module" ``` 4. **Open a pull request**: diff --git a/docs/admin/networking/port-forwarding.md b/docs/admin/networking/port-forwarding.md index a20439b1e46b9..5c7d5204d7166 100644 --- a/docs/admin/networking/port-forwarding.md +++ b/docs/admin/networking/port-forwarding.md @@ -78,7 +78,7 @@ where each segment of hostnames must not exceed 63 characters. If your app name, agent name, workspace name and username exceed 63 characters in the hostname, port forwarding via the dashboard will not work. -### From an coder_app resource +### From a coder_app resource One way to port forward is to configure a `coder_app` resource in the workspace's template. This approach shows a visual application icon in the diff --git a/docs/admin/templates/extending-templates/variables.md b/docs/admin/templates/extending-templates/variables.md index 4fa89d40aebbf..13a9793279ed3 100644 --- a/docs/admin/templates/extending-templates/variables.md +++ b/docs/admin/templates/extending-templates/variables.md @@ -51,7 +51,7 @@ and predictability. If you encounter a situation where you need to override template settings for variables, you can employ a straightforward solution: -1. Create a `terraform.tfvars` file in in the template directory: +1. Create a `terraform.tfvars` file in the template directory: ```tf coder_image = newimage:tag diff --git a/docs/admin/users/idp-sync.md b/docs/admin/users/idp-sync.md index a5fe3da316983..5f653ad95f60b 100644 --- a/docs/admin/users/idp-sync.md +++ b/docs/admin/users/idp-sync.md @@ -241,7 +241,7 @@ role sync at the organization level. 1. Confirm you have the [Coder CLI](../../install/index.md) installed and are logged in with a user who is an Owner or has an Organization Admin role. -1. To fetch the current group sync settings for an organization, run the +1. To fetch the current role sync settings for an organization, run the following: ```sh @@ -388,7 +388,7 @@ settings, a user's memberships will update when they log out and log back in. "cbdcf774-4123-4118-8cd9-b3f502c84dfb" ], "sales": [ - "d79144d9-b30a-555a-9af8-7dac83b2q4ec", + "d79144d9-b30a-555a-9af8-7dac83b2q4ec" ] }, "organization_assign_default": true diff --git a/docs/ai-coder/tasks-migration.md b/docs/ai-coder/tasks-migration.md index 7fe2a01e266da..e279317fe255d 100644 --- a/docs/ai-coder/tasks-migration.md +++ b/docs/ai-coder/tasks-migration.md @@ -76,7 +76,7 @@ Below is a minimal illustrative example of a Coder Tasks template pre-2.28.0. terraform { required_providers { coder = { - source = "coder/coder + source = "coder/coder" } } } @@ -132,8 +132,8 @@ Example (**not** a full template): terraform { required_providers { coder = { - source = "coder/coder - version = ">= 2.13.0 + source = "coder/coder" + version = ">= 2.13.0" } } } diff --git a/docs/user-guides/workspace-access/index.md b/docs/user-guides/workspace-access/index.md index 722094ce2f684..b7dcd27081edf 100644 --- a/docs/user-guides/workspace-access/index.md +++ b/docs/user-guides/workspace-access/index.md @@ -26,7 +26,7 @@ customization options, keyboard shortcuts, and troubleshooting guides. ## SSH -### Through with the CLI +### Through the CLI Coder will use the optimal path for an SSH connection (determined by your deployment's [networking configuration](../../admin/infrastructure/index.md)) From 81f0dce9097869dd1e150d134e86f4e87bd28bb6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:52:56 -0400 Subject: [PATCH 03/12] docs: correct broken CLI commands and flags from drift sweep (#28098) (#28165) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28098 Original PR: #28098 — docs: correct broken CLI commands and flags from drift sweep Merge commit: 58de9ab8f87e7fede0a2d83afa2644bd56a16422 Requested by: @nickvigilante Co-authored-by: Nick Vigilante --- docs/admin/infrastructure/scale-utility.md | 2 +- docs/admin/integrations/dx-data-cloud.md | 5 +++-- docs/admin/users/index.md | 5 +++-- docs/ai-coder/github-to-tasks.md | 5 +---- docs/user-guides/workspace-access/index.md | 2 +- 5 files changed, 9 insertions(+), 10 deletions(-) diff --git a/docs/admin/infrastructure/scale-utility.md b/docs/admin/infrastructure/scale-utility.md index 5da9b0f0b1384..075b199f1ab39 100644 --- a/docs/admin/infrastructure/scale-utility.md +++ b/docs/admin/infrastructure/scale-utility.md @@ -130,7 +130,7 @@ wish to clean up all workspaces, you can run the following command: ```sh coder exp scaletest cleanup \ --cleanup-job-timeout 2h \ - --cleanup-timeout 15min + --cleanup-timeout 15m ``` This will delete all workspaces and users with the prefix `scaletest-`. diff --git a/docs/admin/integrations/dx-data-cloud.md b/docs/admin/integrations/dx-data-cloud.md index ee4f486a6ca41..0f602ea556822 100644 --- a/docs/admin/integrations/dx-data-cloud.md +++ b/docs/admin/integrations/dx-data-cloud.md @@ -30,10 +30,11 @@ If your organization already uses the Coder-DX integration, you can find a list ### CLI -Use `users list` to export the list of users to a CSV file: +Use `users list` with `jq` to export the list of users to a CSV file: ```sh -coder users list > users.csv +coder users list --output json | \ + jq -r '["username","email","created_at","status"], (.[] | [.username, .email, .created_at, .status]) | @csv' > users.csv ``` Visit the [users list](../../reference/cli/users_list.md) documentation for more options. diff --git a/docs/admin/users/index.md b/docs/admin/users/index.md index 0a38fa8710abc..a5bd35a0586a6 100644 --- a/docs/admin/users/index.md +++ b/docs/admin/users/index.md @@ -230,10 +230,11 @@ You can use the Coder CLI or API to retrieve your list of users. ### CLI -Use `users list` to export the list of users to a CSV file: +Use `users list` with `jq` to export the list of users to a CSV file: ```sh -coder users list > users.csv +coder users list --output json | \ + jq -r '["username","email","created_at","status"], (.[] | [.username, .email, .created_at, .status]) | @csv' > users.csv ``` Visit the [users list](../../reference/cli/users_list.md) documentation for more options. diff --git a/docs/ai-coder/github-to-tasks.md b/docs/ai-coder/github-to-tasks.md index f75dbe850305a..a3462596b3f39 100644 --- a/docs/ai-coder/github-to-tasks.md +++ b/docs/ai-coder/github-to-tasks.md @@ -99,9 +99,6 @@ You must also set `coder-template-name` as part of this. The GHA example has thi ```sh # List all templates in your organization coder templates list - -# List templates in a specific organization -coder templates list --org your-org-name ``` You can also choose to modify the other [input parameters](https://github.com/coder/create-task-action?tab=readme-ov-file#inputs) to better fit your desired workflow. @@ -229,7 +226,7 @@ Generate a new token with these permissions at `https:///deploym **Solution:** -1. Verify the template name using: `coder templates list --org your-org-name` +1. Verify the template name using: `coder templates list` 1. Update the `coder-template-name` input in your workflow file to match exactly, or input secret or variable saved in GitHub 1. Ensure the template exists in the organization specified by `coder-organization` diff --git a/docs/user-guides/workspace-access/index.md b/docs/user-guides/workspace-access/index.md index b7dcd27081edf..e6a437035a741 100644 --- a/docs/user-guides/workspace-access/index.md +++ b/docs/user-guides/workspace-access/index.md @@ -228,7 +228,7 @@ services or preview environments. You can also [share ports](./port-forwarding.md#sharing-ports) with other users, or [port-forward](./port-forwarding.md#the-coder-port-forward-command) through -the CLI with `coder port forward`. Read more in the +the CLI with `coder port-forward`. Read more in the [docs on workspace ports](./port-forwarding.md). ![Open Ports window](../../images/networking/listeningports.png) From 2d47088b30a5190feeca985b39a1c5770c98a8c0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:54:36 -0400 Subject: [PATCH 04/12] docs: fix prometheus metric name and slack webhook backtick (#28085) (#28161) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28085 Original PR: #28085 — docs: fix prometheus metric name and slack webhook backtick Merge commit: 3145cc8386f9b3a6d2e43a78556a1717e2869f79 Requested by: @nickvigilante Co-authored-by: Nick Vigilante --- docs/admin/integrations/prometheus.md | 2 +- docs/admin/monitoring/notifications/slack.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/admin/integrations/prometheus.md b/docs/admin/integrations/prometheus.md index 9364583cbe808..6c409ef04b2e7 100644 --- a/docs/admin/integrations/prometheus.md +++ b/docs/admin/integrations/prometheus.md @@ -358,7 +358,7 @@ The following metrics support native histograms: * `coderd_workspace_creation_duration_seconds` * `coderd_prebuilt_workspace_claim_duration_seconds` -* `coderd_template_coderd_template_workspace_build_duration_seconds` +* `coderd_template_workspace_build_duration_seconds` Native histograms are an **experimental** Prometheus feature that removes the need to predefine bucket boundaries and allows higher-resolution buckets that adapt to deployment characteristics. Whether a metric is exposed as classic or native depends entirely on the Prometheus server configuration (see [Prometheus docs](https://prometheus.io/docs/specs/native_histograms/) for details): diff --git a/docs/admin/monitoring/notifications/slack.md b/docs/admin/monitoring/notifications/slack.md index 7509c1fbad2d9..4d6330b66f8a7 100644 --- a/docs/admin/monitoring/notifications/slack.md +++ b/docs/admin/monitoring/notifications/slack.md @@ -196,7 +196,7 @@ To enable webhook integration in Coder, define the POST webhook endpoint matching the deployed Slack bot: ```sh -export CODER_NOTIFICATIONS_WEBHOOK_ENDPOINT=http://localhost:6000/v1/webhook` +export CODER_NOTIFICATIONS_WEBHOOK_ENDPOINT=http://localhost:6000/v1/webhook ``` Finally, go to the **Notification Settings** in Coder and switch the notifier to From 73d95e9f4ea22b85fcd3107168eaf1f4da223e7d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 14 Aug 2026 12:55:51 -0400 Subject: [PATCH 05/12] docs: remove invalid `--yes` flag from `coder template version promote` (#28084) (#28158) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28084 Original PR: #28084 — docs: remove invalid `--yes` flag from `coder template version promote` Merge commit: f3fd4c4a77f2bd15624310c49d0bcd1e6fad8ed6 Requested by: @nickvigilante Co-authored-by: Nick Vigilante --- docs/tutorials/testing-templates.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tutorials/testing-templates.md b/docs/tutorials/testing-templates.md index 00e5b29e94e37..b18214b1ba02a 100644 --- a/docs/tutorials/testing-templates.md +++ b/docs/tutorials/testing-templates.md @@ -131,5 +131,5 @@ jobs: - name: Promote template version if: success() run: | - coder template version promote --template=$TEMPLATE_NAME --template-version=${{ steps.name.outputs.version_name }} --yes + coder template version promote --template=$TEMPLATE_NAME --template-version=${{ steps.name.outputs.version_name }} ``` From e499c18437f6b0ad3f6a6b58f2e2b7ede02de632 Mon Sep 17 00:00:00 2001 From: Cian Johnston Date: Mon, 17 Aug 2026 08:22:24 +0100 Subject: [PATCH 06/12] chore: ship prices for all AI Governance proivder types (#28102) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Includes the two below commits: - **chore: update model prices to include more providers (#27837)** - **chore: add more known model prices to ChatModelAdminPanel (#27839)** Note that this also requires db88ec3 (#27836) and 5efa7ab (#27923) ℹ️ Note for reviewers: this PR may look horrible, but most of it is just JSON. You don't have to review that. --------- Co-authored-by: Susana Ferreira --- .gitattributes | 4 + coderd/aibridge/prices/data/prices.json | 6718 ++++++++++++++++- scripts/aibridgepricesgen/curation.json | 209 +- scripts/aibridgepricesgen/main.go | 11 +- scripts/aibridgepricesgen/overrides.jq | 15 + .../knownModels/index.test.ts | 2 +- .../knownModels/knownModelsGenerated.json | 595 +- 7 files changed, 7398 insertions(+), 156 deletions(-) diff --git a/.gitattributes b/.gitattributes index 39e1717ed68e2..58a1bceb877b8 100644 --- a/.gitattributes +++ b/.gitattributes @@ -36,3 +36,7 @@ site/e2e/provisionerGenerated.ts linguist-generated=true site/src/api/countriesGenerated.tsx linguist-generated=true site/src/api/rbacresourcesGenerated.tsx linguist-generated=true site/src/api/typesGenerated.ts linguist-generated=true + +# AI Gateway prices +coderd/aibridge/prices/data/prices.json linguist-generated=true +site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json linguist-generated=true diff --git a/coderd/aibridge/prices/data/prices.json b/coderd/aibridge/prices/data/prices.json index d6aabb9abfbe6..7570561b6185e 100644 --- a/coderd/aibridge/prices/data/prices.json +++ b/coderd/aibridge/prices/data/prices.json @@ -128,23 +128,199 @@ "cache_write_price": 2500000 }, { - "provider": "openai", - "model": "gpt-3.5-turbo", + "provider": "azure", + "model": "claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "azure", + "model": "claude-haiku-4-5", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "azure", + "model": "claude-opus-4-1", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "azure", + "model": "claude-opus-4-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "azure", + "model": "claude-opus-4-6", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "azure", + "model": "claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "azure", + "model": "claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "azure", + "model": "claude-sonnet-4-5", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "azure", + "model": "claude-sonnet-4-6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "azure", + "model": "claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "azure", + "model": "codestral-2501", + "input_price": 300000, + "output_price": 900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "codex-mini", + "input_price": 1500000, + "output_price": 6000000, + "cache_read_price": 375000, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "cohere-command-a", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "cohere-embed-v-4-0", + "input_price": 120000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "cohere-embed-v3-english", + "input_price": 100000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "cohere-embed-v3-multilingual", + "input_price": 100000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "deepseek-r1", + "input_price": 1350000, + "output_price": 5400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "deepseek-v3.2", + "input_price": 580000, + "output_price": 1680000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "deepseek-v3.2-speciale", + "input_price": 580000, + "output_price": 1680000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "deepseek-v4-flash", + "input_price": 190000, + "output_price": 510000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "deepseek-v4-pro", + "input_price": 1740000, + "output_price": 3480000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "gpt-3.5-turbo-0125", "input_price": 500000, "output_price": 1500000, - "cache_read_price": 0, + "cache_read_price": null, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-4", - "input_price": 30000000, - "output_price": 60000000, + "provider": "azure", + "model": "gpt-3.5-turbo-1106", + "input_price": 1000000, + "output_price": 2000000, "cache_read_price": null, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", + "model": "gpt-3.5-turbo-instruct", + "input_price": 1500000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", "model": "gpt-4-turbo", "input_price": 10000000, "output_price": 30000000, @@ -152,7 +328,15 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", + "model": "gpt-4-turbo-vision", + "input_price": 10000000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", "model": "gpt-4.1", "input_price": 2000000, "output_price": 8000000, @@ -160,7 +344,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-4.1-mini", "input_price": 400000, "output_price": 1600000, @@ -168,7 +352,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-4.1-nano", "input_price": 100000, "output_price": 400000, @@ -176,7 +360,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-4o", "input_price": 2500000, "output_price": 10000000, @@ -184,31 +368,7 @@ "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-4o-2024-05-13", - "input_price": 5000000, - "output_price": 15000000, - "cache_read_price": null, - "cache_write_price": null - }, - { - "provider": "openai", - "model": "gpt-4o-2024-08-06", - "input_price": 2500000, - "output_price": 10000000, - "cache_read_price": 1250000, - "cache_write_price": null - }, - { - "provider": "openai", - "model": "gpt-4o-2024-11-20", - "input_price": 2500000, - "output_price": 10000000, - "cache_read_price": 1250000, - "cache_write_price": null - }, - { - "provider": "openai", + "provider": "azure", "model": "gpt-4o-mini", "input_price": 150000, "output_price": 600000, @@ -216,31 +376,39 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5", "input_price": 1250000, "output_price": 10000000, - "cache_read_price": 125000, + "cache_read_price": 130000, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", + "model": "gpt-5-codex", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "azure", "model": "gpt-5-mini", "input_price": 250000, "output_price": 2000000, - "cache_read_price": 25000, + "cache_read_price": 30000, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5-nano", "input_price": 50000, "output_price": 400000, - "cache_read_price": 5000, + "cache_read_price": 10000, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5-pro", "input_price": 15000000, "output_price": 120000000, @@ -248,7 +416,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.1", "input_price": 1250000, "output_price": 10000000, @@ -256,55 +424,55 @@ "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.2", - "input_price": 1750000, - "output_price": 14000000, - "cache_read_price": 175000, + "provider": "azure", + "model": "gpt-5.1-codex", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.2-chat-latest", - "input_price": 1750000, - "output_price": 14000000, - "cache_read_price": 175000, + "provider": "azure", + "model": "gpt-5.1-codex-max", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.2-pro", - "input_price": 21000000, - "output_price": 168000000, - "cache_read_price": null, + "provider": "azure", + "model": "gpt-5.1-codex-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 25000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.3-chat-latest", + "provider": "azure", + "model": "gpt-5.2", "input_price": 1750000, "output_price": 14000000, - "cache_read_price": 175000, + "cache_read_price": 125000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.3-codex", + "provider": "azure", + "model": "gpt-5.2-codex", "input_price": 1750000, "output_price": 14000000, "cache_read_price": 175000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.3-codex-spark", + "provider": "azure", + "model": "gpt-5.3-codex", "input_price": 1750000, "output_price": 14000000, "cache_read_price": 175000, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.4", "input_price": 2500000, "output_price": 15000000, @@ -312,7 +480,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.4-mini", "input_price": 750000, "output_price": 4500000, @@ -320,7 +488,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.4-nano", "input_price": 200000, "output_price": 1250000, @@ -328,7 +496,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.4-pro", "input_price": 30000000, "output_price": 180000000, @@ -336,7 +504,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.5", "input_price": 5000000, "output_price": 30000000, @@ -344,23 +512,7 @@ "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-5.5-pro", - "input_price": 30000000, - "output_price": 180000000, - "cache_read_price": null, - "cache_write_price": null - }, - { - "provider": "openai", - "model": "gpt-5.6", - "input_price": 5000000, - "output_price": 30000000, - "cache_read_price": 500000, - "cache_write_price": 6250000 - }, - { - "provider": "openai", + "provider": "azure", "model": "gpt-5.6-luna", "input_price": 1000000, "output_price": 6000000, @@ -368,15 +520,15 @@ "cache_write_price": 1250000 }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.6-sol", "input_price": 5000000, "output_price": 30000000, "cache_read_price": 500000, - "cache_write_price": 6250000 + "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "gpt-5.6-terra", "input_price": 2500000, "output_price": 15000000, @@ -384,39 +536,151 @@ "cache_write_price": 3125000 }, { - "provider": "openai", - "model": "gpt-image-2", + "provider": "azure", + "model": "gpt-chat-latest", "input_price": 5000000, "output_price": 30000000, - "cache_read_price": 1250000, + "cache_read_price": 500000, "cache_write_price": null }, { - "provider": "openai", - "model": "gpt-realtime-2.1", - "input_price": 4000000, - "output_price": 24000000, - "cache_read_price": 400000, + "provider": "azure", + "model": "gpt-image-1", + "input_price": 5000000, + "output_price": 40000000, + "cache_read_price": 1250000, "cache_write_price": null }, { - "provider": "openai", - "model": "o1", - "input_price": 15000000, - "output_price": 60000000, - "cache_read_price": 7500000, + "provider": "azure", + "model": "gpt-image-1.5", + "input_price": 5000000, + "output_price": 32000000, + "cache_read_price": 1250000, "cache_write_price": null }, { - "provider": "openai", - "model": "o1-pro", - "input_price": 150000000, - "output_price": 600000000, + "provider": "azure", + "model": "gpt-image-2", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "grok-4-1-fast-non-reasoning", + "input_price": 200000, + "output_price": 500000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "grok-4-1-fast-reasoning", + "input_price": 200000, + "output_price": 500000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "grok-4-20-non-reasoning", + "input_price": 2000000, + "output_price": 6000000, "cache_read_price": null, "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", + "model": "grok-4-20-reasoning", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "kimi-k2.5", + "input_price": 600000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "kimi-k2.6", + "input_price": 950000, + "output_price": 4000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "llama-3.3-70b-instruct", + "input_price": 710000, + "output_price": 710000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "llama-4-maverick-17b-128e-instruct-fp8", + "input_price": 250000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "llama-4-scout-17b-16e-instruct", + "input_price": 200000, + "output_price": 780000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "ministral-3b", + "input_price": 40000, + "output_price": 40000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "mistral-medium-2505", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "mistral-small-2503", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "model-router", + "input_price": 140000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "o1", + "input_price": 15000000, + "output_price": 60000000, + "cache_read_price": 7500000, + "cache_write_price": null + }, + { + "provider": "azure", "model": "o3", "input_price": 2000000, "output_price": 8000000, @@ -424,7 +688,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "o3-mini", "input_price": 1100000, "output_price": 4400000, @@ -432,15 +696,7 @@ "cache_write_price": null }, { - "provider": "openai", - "model": "o3-pro", - "input_price": 20000000, - "output_price": 80000000, - "cache_read_price": null, - "cache_write_price": null - }, - { - "provider": "openai", + "provider": "azure", "model": "o4-mini", "input_price": 1100000, "output_price": 4400000, @@ -448,7 +704,55 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", + "model": "phi-4", + "input_price": 125000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "phi-4-mini", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "phi-4-mini-reasoning", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "phi-4-multimodal", + "input_price": 80000, + "output_price": 320000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "phi-4-reasoning", + "input_price": 125000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", + "model": "phi-4-reasoning-plus", + "input_price": 125000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "azure", "model": "text-embedding-3-large", "input_price": 130000, "output_price": 0, @@ -456,7 +760,7 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "text-embedding-3-small", "input_price": 20000, "output_price": 0, @@ -464,11 +768,6179 @@ "cache_write_price": null }, { - "provider": "openai", + "provider": "azure", "model": "text-embedding-ada-002", "input_price": 100000, "output_price": 0, "cache_read_price": null, "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "amazon.nova-2-lite-v1:0", + "input_price": 330000, + "output_price": 2750000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "amazon.nova-lite-v1:0", + "input_price": 60000, + "output_price": 240000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "amazon.nova-micro-v1:0", + "input_price": 35000, + "output_price": 140000, + "cache_read_price": 8750, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "amazon.nova-pro-v1:0", + "input_price": 800000, + "output_price": 3200000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "anthropic.claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-4-1-20250805-v1:0", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-4-5-20251101-v1:0", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-4-6-v1", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-4-7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-sonnet-4-6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "anthropic.claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-opus-4-6-v1", + "input_price": 16500000, + "output_price": 82500000, + "cache_read_price": 1650000, + "cache_write_price": 20625000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-sonnet-4-6", + "input_price": 3300000, + "output_price": 16500000, + "cache_read_price": 330000, + "cache_write_price": 4125000 + }, + { + "provider": "bedrock", + "model": "au.anthropic.claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "bedrock", + "model": "deepseek.r1-v1:0", + "input_price": 1350000, + "output_price": 5400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "deepseek.v3-v1:0", + "input_price": 580000, + "output_price": 1680000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "deepseek.v3.2", + "input_price": 620000, + "output_price": 1850000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-fable-5", + "input_price": 11000000, + "output_price": 55000000, + "cache_read_price": 1100000, + "cache_write_price": 13750000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1100000, + "output_price": 5500000, + "cache_read_price": 110000, + "cache_write_price": 1375000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-opus-4-5-20251101-v1:0", + "input_price": 5500000, + "output_price": 27500000, + "cache_read_price": 550000, + "cache_write_price": 6875000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-opus-4-6-v1", + "input_price": 5500000, + "output_price": 27500000, + "cache_read_price": 550000, + "cache_write_price": 6875000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-opus-4-7", + "input_price": 5500000, + "output_price": 27500000, + "cache_read_price": 550000, + "cache_write_price": 6875000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-opus-4-8", + "input_price": 5500000, + "output_price": 27500000, + "cache_read_price": 550000, + "cache_write_price": 6875000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-opus-5", + "input_price": 5500000, + "output_price": 27500000, + "cache_read_price": 550000, + "cache_write_price": 6875000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3300000, + "output_price": 16500000, + "cache_read_price": 330000, + "cache_write_price": 4125000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-sonnet-4-6", + "input_price": 3300000, + "output_price": 16500000, + "cache_read_price": 330000, + "cache_write_price": 4125000 + }, + { + "provider": "bedrock", + "model": "eu.anthropic.claude-sonnet-5", + "input_price": 2200000, + "output_price": 11000000, + "cache_read_price": 220000, + "cache_write_price": 2750000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-opus-4-5-20251101-v1:0", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-opus-4-6-v1", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-opus-4-7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-sonnet-4-6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "global.anthropic.claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "bedrock", + "model": "google.gemma-3-12b-it", + "input_price": 50000, + "output_price": 100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "google.gemma-3-27b-it", + "input_price": 120000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "google.gemma-3-4b-it", + "input_price": 40000, + "output_price": 80000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-opus-4-7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-sonnet-4-6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "jp.anthropic.claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "bedrock", + "model": "meta.llama3-1-70b-instruct-v1:0", + "input_price": 720000, + "output_price": 720000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "meta.llama3-1-8b-instruct-v1:0", + "input_price": 220000, + "output_price": 220000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "meta.llama3-3-70b-instruct-v1:0", + "input_price": 720000, + "output_price": 720000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "meta.llama4-maverick-17b-instruct-v1:0", + "input_price": 240000, + "output_price": 970000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "meta.llama4-scout-17b-instruct-v1:0", + "input_price": 170000, + "output_price": 660000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "minimax.minimax-m2", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "minimax.minimax-m2.1", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "minimax.minimax-m2.5", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.devstral-2-123b", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.magistral-small-2509", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.ministral-3-14b-instruct", + "input_price": 200000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.ministral-3-3b-instruct", + "input_price": 100000, + "output_price": 100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.ministral-3-8b-instruct", + "input_price": 150000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.mistral-large-3-675b-instruct", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.pixtral-large-2502-v1:0", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.voxtral-mini-3b-2507", + "input_price": 40000, + "output_price": 40000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "mistral.voxtral-small-24b-2507", + "input_price": 150000, + "output_price": 350000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "moonshot.kimi-k2-thinking", + "input_price": 600000, + "output_price": 2500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "moonshotai.kimi-k2.5", + "input_price": 600000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "nvidia.nemotron-nano-12b-v2", + "input_price": 200000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "nvidia.nemotron-nano-3-30b", + "input_price": 60000, + "output_price": 240000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "nvidia.nemotron-nano-9b-v2", + "input_price": 60000, + "output_price": 230000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "nvidia.nemotron-super-3-120b", + "input_price": 150000, + "output_price": 650000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-5.4", + "input_price": 2750000, + "output_price": 16500000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-5.5", + "input_price": 5500000, + "output_price": 33000000, + "cache_read_price": 550000, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-5.6-luna", + "input_price": 220000, + "output_price": 1320000, + "cache_read_price": 22000, + "cache_write_price": 275000 + }, + { + "provider": "bedrock", + "model": "openai.gpt-5.6-sol", + "input_price": 5500000, + "output_price": 33000000, + "cache_read_price": 550000, + "cache_write_price": 6880000 + }, + { + "provider": "bedrock", + "model": "openai.gpt-5.6-terra", + "input_price": 2200000, + "output_price": 13200000, + "cache_read_price": 220000, + "cache_write_price": 2750000 + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-120b", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-120b-1:0", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-20b", + "input_price": 70000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-20b-1:0", + "input_price": 70000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-safeguard-120b", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "openai.gpt-oss-safeguard-20b", + "input_price": 70000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-235b-a22b-2507-v1:0", + "input_price": 220000, + "output_price": 880000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-32b-v1:0", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-coder-30b-a3b-v1:0", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-coder-480b-a35b-v1:0", + "input_price": 220000, + "output_price": 1800000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-coder-next", + "input_price": 220000, + "output_price": 1800000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-next-80b-a3b", + "input_price": 140000, + "output_price": 1400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "qwen.qwen3-vl-235b-a22b", + "input_price": 300000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-haiku-4-5-20251001-v1:0", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-4-1-20250805-v1:0", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-4-5-20251101-v1:0", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-4-6-v1", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-4-7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-4-8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-sonnet-4-5-20250929-v1:0", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-sonnet-4-6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "bedrock", + "model": "us.anthropic.claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "bedrock", + "model": "us.deepseek.r1-v1:0", + "input_price": 1350000, + "output_price": 5400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "us.meta.llama4-maverick-17b-instruct-v1:0", + "input_price": 240000, + "output_price": 970000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "us.meta.llama4-scout-17b-instruct-v1:0", + "input_price": 170000, + "output_price": 660000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "writer.palmyra-x4-v1:0", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "writer.palmyra-x5-v1:0", + "input_price": 600000, + "output_price": 6000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "xai.grok-4.3", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "zai.glm-4.7", + "input_price": 600000, + "output_price": 2200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "zai.glm-4.7-flash", + "input_price": 70000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "bedrock", + "model": "zai.glm-5", + "input_price": 1000000, + "output_price": 3200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "copilot", + "model": "claude-haiku-4.5", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "copilot", + "model": "claude-opus-4.5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "claude-opus-4.6", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "claude-opus-4.7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "claude-opus-4.8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "claude-sonnet-4", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "copilot", + "model": "claude-sonnet-4.5", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "copilot", + "model": "claude-sonnet-4.6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "copilot", + "model": "claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "copilot", + "model": "gemini-3.1-pro-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gemini-3.5-flash", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gemini-3.6-flash", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-4.1", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.2", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.2-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.3-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.4", + "input_price": 2500000, + "output_price": 15000000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.4-mini", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.4-nano", + "input_price": 200000, + "output_price": 1250000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.5", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.6-luna", + "input_price": 200000, + "output_price": 1200000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "gpt-5.6-sol", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "copilot", + "model": "gpt-5.6-terra", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "grok-4.5", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "kimi-k2.7-code", + "input_price": 950000, + "output_price": 4000000, + "cache_read_price": 190000, + "cache_write_price": null + }, + { + "provider": "copilot", + "model": "mai-code-1-flash-picker", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "deep-research-max-preview-04-2026", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "deep-research-preview-04-2026", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.0-flash", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.0-flash-lite", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-computer-use-preview-10-2025", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-flash", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-flash-image", + "input_price": 300000, + "output_price": 30000000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-flash-lite", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-flash-preview-tts", + "input_price": 500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-pro", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-2.5-pro-preview-tts", + "input_price": 1000000, + "output_price": 20000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3-flash-preview", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3-pro-image", + "input_price": 2000000, + "output_price": 120000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3-pro-image-preview", + "input_price": 2000000, + "output_price": 120000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3-pro-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-image", + "input_price": 500000, + "output_price": 60000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-image-preview", + "input_price": 500000, + "output_price": 60000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-lite", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-lite-image", + "input_price": 250000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-lite-preview", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-live-preview", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-flash-tts-preview", + "input_price": 1000000, + "output_price": 20000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-pro-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.1-pro-preview-customtools", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.5-flash", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.5-flash-lite", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.5-live-translate-preview", + "input_price": 3500000, + "output_price": 21000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-3.6-flash", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-embedding-001", + "input_price": 150000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-embedding-2", + "input_price": 200000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-flash-latest", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-flash-lite-latest", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-omni-flash-preview", + "input_price": 1500000, + "output_price": 17500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "gemini-robotics-er-1.6-preview", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "lyria-3-clip-preview", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "google", + "model": "lyria-3-pro-preview", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-3.5-turbo", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": 0, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4", + "input_price": 30000000, + "output_price": 60000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4-turbo", + "input_price": 10000000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4.1", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4.1-mini", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4.1-nano", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4o", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4o-2024-05-13", + "input_price": 5000000, + "output_price": 15000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4o-2024-08-06", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4o-2024-11-20", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-4o-mini", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5-nano", + "input_price": 50000, + "output_price": 400000, + "cache_read_price": 5000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5-pro", + "input_price": 15000000, + "output_price": 120000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.1", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.2", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.2-chat-latest", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.2-pro", + "input_price": 21000000, + "output_price": 168000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.3-chat-latest", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.3-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.3-codex-spark", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.4", + "input_price": 2500000, + "output_price": 15000000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.4-mini", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.4-nano", + "input_price": 200000, + "output_price": 1250000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.4-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.5", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.5-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-5.6", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openai", + "model": "gpt-5.6-luna", + "input_price": 200000, + "output_price": 1200000, + "cache_read_price": 20000, + "cache_write_price": 250000 + }, + { + "provider": "openai", + "model": "gpt-5.6-sol", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openai", + "model": "gpt-5.6-terra", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "openai", + "model": "gpt-image-2", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "gpt-realtime-2.1", + "input_price": 4000000, + "output_price": 24000000, + "cache_read_price": 400000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o1", + "input_price": 15000000, + "output_price": 60000000, + "cache_read_price": 7500000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o1-pro", + "input_price": 150000000, + "output_price": 600000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o3", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o3-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 550000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o3-pro", + "input_price": 20000000, + "output_price": 80000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "o4-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "text-embedding-3-large", + "input_price": 130000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "text-embedding-3-small", + "input_price": 20000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openai", + "model": "text-embedding-ada-002", + "input_price": 100000, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "ai21/jamba-large-1.7", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "aion-labs/aion-2.0", + "input_price": 800000, + "output_price": 1600000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "aion-labs/aion-3.0", + "input_price": 3000000, + "output_price": 6000000, + "cache_read_price": 750000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "aion-labs/aion-3.0-mini", + "input_price": 700000, + "output_price": 1400000, + "cache_read_price": 180000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "aion-labs/aion-rp-llama-3.1-8b", + "input_price": 800000, + "output_price": 1600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "allenai/olmo-3-32b-think", + "input_price": 150000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "amazon/nova-2-lite-v1", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "amazon/nova-lite-v1", + "input_price": 60000, + "output_price": 240000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "amazon/nova-micro-v1", + "input_price": 35000, + "output_price": 140000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "amazon/nova-premier-v1", + "input_price": 2500000, + "output_price": 12500000, + "cache_read_price": 625000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "amazon/nova-pro-v1", + "input_price": 800000, + "output_price": 3200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "anthracite-org/magnum-v4-72b", + "input_price": 3000000, + "output_price": 5000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "anthropic/claude-3-haiku", + "input_price": 250000, + "output_price": 1250000, + "cache_read_price": 30000, + "cache_write_price": 300000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-haiku-4.5", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.1", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.6", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.7-fast", + "input_price": 30000000, + "output_price": 150000000, + "cache_read_price": 3000000, + "cache_write_price": 37500000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-4.8-fast", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-opus-5-fast", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-sonnet-4", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-sonnet-4.5", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-sonnet-4.6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "openrouter", + "model": "anthropic/claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "openrouter", + "model": "arcee-ai/trinity-large-thinking", + "input_price": 220000, + "output_price": 850000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "arcee-ai/virtuoso-large", + "input_price": 750000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "baidu/ernie-4.5-vl-424b-a47b", + "input_price": 420000, + "output_price": 1250000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "bytedance-seed/seed-1.6", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "bytedance-seed/seed-1.6-flash", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "bytedance-seed/seed-2.0-lite", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "bytedance-seed/seed-2.0-mini", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "bytedance/ui-tars-1.5-7b", + "input_price": 100000, + "output_price": 200000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cognitivecomputations/dolphin-mistral-24b-venice-edition", + "input_price": 200000, + "output_price": 900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cohere/command-a", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cohere/command-r-08-2024", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cohere/command-r-plus-08-2024", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cohere/command-r7b-12-2024", + "input_price": 37500, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "cohere/north-mini-code:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepcogito/cogito-v2.1-671b", + "input_price": 1250000, + "output_price": 1250000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-chat", + "input_price": 257400, + "output_price": 1028700, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-chat-v3-0324", + "input_price": 270000, + "output_price": 1120000, + "cache_read_price": 135000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-chat-v3.1", + "input_price": 250000, + "output_price": 950000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-r1", + "input_price": 700000, + "output_price": 2500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-r1-0528", + "input_price": 500000, + "output_price": 2150000, + "cache_read_price": 350000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-r1-distill-llama-70b", + "input_price": 800000, + "output_price": 800000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v3.1-terminus", + "input_price": 270000, + "output_price": 1000000, + "cache_read_price": 135000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v3.2", + "input_price": 269000, + "output_price": 400000, + "cache_read_price": 134500, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v3.2-exp", + "input_price": 270000, + "output_price": 410000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v4-flash", + "input_price": 140000, + "output_price": 280000, + "cache_read_price": 28000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v4-flash-0731", + "input_price": 90000, + "output_price": 180000, + "cache_read_price": 18000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "deepseek/deepseek-v4-pro", + "input_price": 435000, + "output_price": 870000, + "cache_read_price": 3625, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-flash", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-flash-image", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-flash-lite", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 10000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-pro", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-pro-preview", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-2.5-pro-preview-05-06", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-3-flash-preview", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 50000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-3-pro-image", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-3-pro-image-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-flash-image", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-flash-image-preview", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-flash-lite", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 25000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-flash-lite-image", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-flash-lite-preview", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 25000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-pro-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.1-pro-preview-customtools", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.5-flash", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": 150000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.5-flash-lite", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemini-3.6-flash", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 150000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "google/gemma-2-27b-it", + "input_price": 650000, + "output_price": 650000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-3-12b-it", + "input_price": 50000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-3-27b-it", + "input_price": 80000, + "output_price": 450000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-3-4b-it", + "input_price": 50000, + "output_price": 100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-3n-e4b-it", + "input_price": 60000, + "output_price": 120000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-4-26b-a4b-it", + "input_price": 70000, + "output_price": 340000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-4-26b-a4b-it:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-4-31b-it", + "input_price": 100000, + "output_price": 340000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/gemma-4-31b-it:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/lyria-3-clip-preview", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "google/lyria-3-pro-preview", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "gryphe/mythomax-l2-13b", + "input_price": 80000, + "output_price": 110000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "ibm-granite/granite-4.0-h-micro", + "input_price": 17000, + "output_price": 112000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "ibm-granite/granite-4.1-8b", + "input_price": 50000, + "output_price": 100000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "inception/mercury-2", + "input_price": 250000, + "output_price": 750000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "inclusionai/ling-2.6-1t", + "input_price": 75000, + "output_price": 625000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "inclusionai/ling-2.6-flash", + "input_price": 10000, + "output_price": 30000, + "cache_read_price": 2000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "inclusionai/ling-3.0-flash:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "inclusionai/ring-2.6-1t", + "input_price": 75000, + "output_price": 625000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "kwaipilot/kat-coder-air-v2.5", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "kwaipilot/kat-coder-pro-v2", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "kwaipilot/kat-coder-pro-v2.5", + "input_price": 740000, + "output_price": 2960000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mancer/weaver", + "input_price": 500000, + "output_price": 750000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meituan/longcat-2.0", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 6000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-3.1-70b-instruct", + "input_price": 400000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-3.1-8b-instruct", + "input_price": 50000, + "output_price": 80000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-3.2-1b-instruct", + "input_price": 27000, + "output_price": 201000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-3.2-3b-instruct", + "input_price": 50000, + "output_price": 330000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-3.3-70b-instruct", + "input_price": 100000, + "output_price": 320000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-4-maverick", + "input_price": 200000, + "output_price": 800000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-4-scout", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta-llama/llama-guard-4-12b", + "input_price": 180000, + "output_price": 180000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "meta/muse-spark-1.1", + "input_price": 1250000, + "output_price": 4250000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "microsoft/phi-4", + "input_price": 70000, + "output_price": 140000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "microsoft/wizardlm-2-8x22b", + "input_price": 620000, + "output_price": 620000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-01", + "input_price": 200000, + "output_price": 1100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m1", + "input_price": 550000, + "output_price": 2200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m2", + "input_price": 255000, + "output_price": 1020000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m2-her", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m2.1", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m2.5", + "input_price": 150000, + "output_price": 900000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m2.7", + "input_price": 250000, + "output_price": 1000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "minimax/minimax-m3", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/codestral-2508", + "input_price": 300000, + "output_price": 900000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/ministral-14b-2512", + "input_price": 200000, + "output_price": 200000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/ministral-3b-2512", + "input_price": 100000, + "output_price": 100000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/ministral-8b-2512", + "input_price": 150000, + "output_price": 150000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-large", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-large-2407", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-large-2512", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-medium-3", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-medium-3-5", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-medium-3.1", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-nemo", + "input_price": 19000, + "output_price": 30000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-saba", + "input_price": 200000, + "output_price": 600000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-small-24b-instruct-2501", + "input_price": 50000, + "output_price": 80000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-small-2603", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-small-3.1-24b-instruct", + "input_price": 351000, + "output_price": 555000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mistral-small-3.2-24b-instruct", + "input_price": 75000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/mixtral-8x22b-instruct", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "mistralai/voxtral-small-24b-2507", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2", + "input_price": 570000, + "output_price": 2300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2-0905", + "input_price": 600000, + "output_price": 2500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2-thinking", + "input_price": 600000, + "output_price": 2500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2.5", + "input_price": 570000, + "output_price": 2850000, + "cache_read_price": 95000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2.6", + "input_price": 589000, + "output_price": 2480000, + "cache_read_price": 99200, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k2.7-code", + "input_price": 730000, + "output_price": 3500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "moonshotai/kimi-k3", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "morph/morph-v3-fast", + "input_price": 800000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "morph/morph-v3-large", + "input_price": 900000, + "output_price": 1900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nex-agi/nex-n2-mini", + "input_price": 25000, + "output_price": 100000, + "cache_read_price": 2500, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nex-agi/nex-n2-pro", + "input_price": 250000, + "output_price": 1000000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nousresearch/hermes-3-llama-3.1-405b", + "input_price": 1000000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nousresearch/hermes-3-llama-3.1-70b", + "input_price": 700000, + "output_price": 700000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nousresearch/hermes-4-405b", + "input_price": 1000000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nousresearch/hermes-4-70b", + "input_price": 130000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-nano-30b-a3b", + "input_price": 50000, + "output_price": 200000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-nano-30b-a3b:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-nano-omni-30b-a3b-reasoning:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-super-120b-a12b", + "input_price": 85000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-super-120b-a12b:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-ultra-550b-a55b", + "input_price": 600000, + "output_price": 3600000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3-ultra-550b-a55b:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-3.5-content-safety:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-nano-12b-v2-vl:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "nvidia/nemotron-nano-9b-v2:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-3.5-turbo", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-3.5-turbo-0613", + "input_price": 1000000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-3.5-turbo-16k", + "input_price": 3000000, + "output_price": 4000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-3.5-turbo-instruct", + "input_price": 1500000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4", + "input_price": 30000000, + "output_price": 60000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4-turbo", + "input_price": 10000000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4-turbo-preview", + "input_price": 10000000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4.1", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4.1-mini", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4.1-nano", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o-2024-05-13", + "input_price": 5000000, + "output_price": 15000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o-2024-08-06", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o-2024-11-20", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o-mini", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-4o-mini-2024-07-18", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5-image", + "input_price": 10000000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5-image-mini", + "input_price": 2500000, + "output_price": 2000000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5-nano", + "input_price": 50000, + "output_price": 400000, + "cache_read_price": 5000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5-pro", + "input_price": 15000000, + "output_price": 120000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.1", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.1-codex", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.1-codex-max", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.1-codex-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.2", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.2-chat", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.2-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.2-pro", + "input_price": 21000000, + "output_price": 168000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.3-chat", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.3-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.4", + "input_price": 2500000, + "output_price": 15000000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.4-image-2", + "input_price": 8000000, + "output_price": 15000000, + "cache_read_price": 2000000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.4-mini", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.4-nano", + "input_price": 200000, + "output_price": 1250000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.4-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.5", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.5-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-luna", + "input_price": 100000, + "output_price": 600000, + "cache_read_price": 10000, + "cache_write_price": 125000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-luna-pro", + "input_price": 100000, + "output_price": 600000, + "cache_read_price": 10000, + "cache_write_price": 125000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-sol", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-sol-pro", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-terra", + "input_price": 1000000, + "output_price": 6000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-5.6-terra-pro", + "input_price": 1000000, + "output_price": 6000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "openrouter", + "model": "openai/gpt-audio", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-audio-mini", + "input_price": 600000, + "output_price": 2400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-chat-latest", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-oss-120b", + "input_price": 37000, + "output_price": 170000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-oss-20b", + "input_price": 30000, + "output_price": 130000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-oss-20b:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/gpt-oss-safeguard-20b", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": 37500, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o1", + "input_price": 15000000, + "output_price": 60000000, + "cache_read_price": 7500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o1-pro", + "input_price": 150000000, + "output_price": 600000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o3", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o3-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 550000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o3-mini-high", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 550000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o3-pro", + "input_price": 20000000, + "output_price": 80000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o4-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openai/o4-mini-high", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "openrouter/free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perceptron/perceptron-mk1", + "input_price": 150000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perplexity/sonar", + "input_price": 1000000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perplexity/sonar-deep-research", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perplexity/sonar-pro", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perplexity/sonar-pro-search", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "perplexity/sonar-reasoning-pro", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "poolside/laguna-s-2.1", + "input_price": 90000, + "output_price": 180000, + "cache_read_price": 9000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "poolside/laguna-s-2.1:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "poolside/laguna-xs-2.1", + "input_price": 60000, + "output_price": 120000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "poolside/laguna-xs-2.1:free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen-2.5-72b-instruct", + "input_price": 360000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen-2.5-7b-instruct", + "input_price": 100000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen-2.5-coder-32b-instruct", + "input_price": 660000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen-plus", + "input_price": 260000, + "output_price": 780000, + "cache_read_price": 52000, + "cache_write_price": 325000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen-plus-2025-07-28", + "input_price": 260000, + "output_price": 780000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen-plus-2025-07-28:thinking", + "input_price": 400000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": 500000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen2.5-vl-72b-instruct", + "input_price": 250000, + "output_price": 750000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-14b", + "input_price": 227500, + "output_price": 910000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-235b-a22b", + "input_price": 455000, + "output_price": 1820000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-235b-a22b-2507", + "input_price": 149500, + "output_price": 598000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-235b-a22b-thinking-2507", + "input_price": 230000, + "output_price": 2300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-30b-a3b", + "input_price": 120000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-30b-a3b-instruct-2507", + "input_price": 48150, + "output_price": 193050, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-30b-a3b-thinking-2507", + "input_price": 200000, + "output_price": 2400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-32b", + "input_price": 80000, + "output_price": 280000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-8b", + "input_price": 117000, + "output_price": 455000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-coder", + "input_price": 300000, + "output_price": 1000000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-coder-30b-a3b-instruct", + "input_price": 70000, + "output_price": 270000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-coder-flash", + "input_price": 195000, + "output_price": 975000, + "cache_read_price": 39000, + "cache_write_price": 243750 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-coder-next", + "input_price": 120000, + "output_price": 800000, + "cache_read_price": 70000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-coder-plus", + "input_price": 650000, + "output_price": 3250000, + "cache_read_price": 130000, + "cache_write_price": 812500 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-max", + "input_price": 780000, + "output_price": 3900000, + "cache_read_price": 156000, + "cache_write_price": 975000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-max-thinking", + "input_price": 780000, + "output_price": 3900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-next-80b-a3b-instruct", + "input_price": 90000, + "output_price": 1100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-next-80b-a3b-thinking", + "input_price": 150000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-235b-a22b-instruct", + "input_price": 210000, + "output_price": 1900000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-235b-a22b-thinking", + "input_price": 980000, + "output_price": 3950000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-30b-a3b-instruct", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-30b-a3b-thinking", + "input_price": 200000, + "output_price": 2400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-32b-instruct", + "input_price": 104000, + "output_price": 416000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-8b-instruct", + "input_price": 117000, + "output_price": 455000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3-vl-8b-thinking", + "input_price": 180000, + "output_price": 2100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-122b-a10b", + "input_price": 260000, + "output_price": 2080000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-27b", + "input_price": 195000, + "output_price": 1560000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-35b-a3b", + "input_price": 140000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-397b-a17b", + "input_price": 390000, + "output_price": 2340000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-9b", + "input_price": 100000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-flash-02-23", + "input_price": 65000, + "output_price": 260000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-plus-02-15", + "input_price": 260000, + "output_price": 1560000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.5-plus-20260420", + "input_price": 300000, + "output_price": 1800000, + "cache_read_price": null, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.6-27b", + "input_price": 289000, + "output_price": 2400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.6-35b-a3b", + "input_price": 140000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.6-flash", + "input_price": 187500, + "output_price": 1125000, + "cache_read_price": null, + "cache_write_price": 234375 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.6-max-preview", + "input_price": 1027000, + "output_price": 6162000, + "cache_read_price": null, + "cache_write_price": 1283750 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.6-plus", + "input_price": 325000, + "output_price": 1950000, + "cache_read_price": null, + "cache_write_price": 406250 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.7-flash", + "input_price": 30000, + "output_price": 130000, + "cache_read_price": 6000, + "cache_write_price": 38000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.7-max", + "input_price": 1475000, + "output_price": 4425000, + "cache_read_price": 295000, + "cache_write_price": 1843750 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.7-plus", + "input_price": 320000, + "output_price": 1280000, + "cache_read_price": 64000, + "cache_write_price": 400000 + }, + { + "provider": "openrouter", + "model": "qwen/qwen3.8-max", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 250000, + "cache_write_price": 2500000 + }, + { + "provider": "openrouter", + "model": "rekaai/reka-edge", + "input_price": 100000, + "output_price": 100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "rekaai/reka-flash-3", + "input_price": 100000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "relace/relace-apply-3", + "input_price": 850000, + "output_price": 1250000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "relace/relace-search", + "input_price": 1000000, + "output_price": 3000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "sakana/fugu-ultra", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "sao10k/l3-lunaris-8b", + "input_price": 40000, + "output_price": 50000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "sao10k/l3.1-euryale-70b", + "input_price": 850000, + "output_price": 850000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "sao10k/l3.3-euryale-70b", + "input_price": 650000, + "output_price": 750000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "stepfun/step-3.5-flash", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "stepfun/step-3.7-flash", + "input_price": 200000, + "output_price": 1150000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "tencent/hunyuan-a13b-instruct", + "input_price": 140000, + "output_price": 570000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "tencent/hy3", + "input_price": 132000, + "output_price": 528000, + "cache_read_price": 33000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "tencent/hy3-preview", + "input_price": 63000, + "output_price": 210000, + "cache_read_price": 21000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thedrummer/cydonia-24b-v4.1", + "input_price": 300000, + "output_price": 500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thedrummer/rocinante-12b", + "input_price": 250000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thedrummer/skyfall-36b-v2", + "input_price": 550000, + "output_price": 800000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thedrummer/unslopnemo-12b", + "input_price": 400000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thinkingmachines/inkling", + "input_price": 1000000, + "output_price": 4050000, + "cache_read_price": 170000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "thinkingmachines/inkling-small", + "input_price": 500000, + "output_price": 1200000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "undi95/remm-slerp-l2-13b", + "input_price": 450000, + "output_price": 650000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "upstage/solar-pro-3", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "writer/palmyra-x5", + "input_price": 600000, + "output_price": 6000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "x-ai/grok-4.20", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "x-ai/grok-4.20-multi-agent", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "x-ai/grok-4.3", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "x-ai/grok-4.5", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "x-ai/grok-build-0.1", + "input_price": 1000000, + "output_price": 2000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "xiaomi/mimo-v2.5", + "input_price": 140000, + "output_price": 280000, + "cache_read_price": 2800, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "xiaomi/mimo-v2.5-pro", + "input_price": 435000, + "output_price": 870000, + "cache_read_price": 3600, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.5", + "input_price": 600000, + "output_price": 2200000, + "cache_read_price": 110000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.5-air", + "input_price": 130000, + "output_price": 850000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.5v", + "input_price": 600000, + "output_price": 1800000, + "cache_read_price": 110000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.6", + "input_price": 500000, + "output_price": 2000000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.6v", + "input_price": 300000, + "output_price": 900000, + "cache_read_price": 55000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.7", + "input_price": 400000, + "output_price": 1750000, + "cache_read_price": 80000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-4.7-flash", + "input_price": 60000, + "output_price": 400000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-5", + "input_price": 950000, + "output_price": 2550000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-5-turbo", + "input_price": 1200000, + "output_price": 4000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-5.1", + "input_price": 966000, + "output_price": 3036000, + "cache_read_price": 179400, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-5.2", + "input_price": 760000, + "output_price": 2420000, + "cache_read_price": 140000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "z-ai/glm-5v-turbo", + "input_price": 1200000, + "output_price": 4000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "~anthropic/claude-fable-latest", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "openrouter", + "model": "~anthropic/claude-haiku-latest", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "openrouter", + "model": "~anthropic/claude-opus-latest", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "~anthropic/claude-sonnet-latest", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "openrouter", + "model": "~deepseek/deepseek-v4-flash-latest", + "input_price": 90000, + "output_price": 180000, + "cache_read_price": 18000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "~google/gemini-flash-latest", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 150000, + "cache_write_price": 83333 + }, + { + "provider": "openrouter", + "model": "~google/gemini-pro-latest", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 375000 + }, + { + "provider": "openrouter", + "model": "~moonshotai/kimi-latest", + "input_price": 2900000, + "output_price": 14000000, + "cache_read_price": 290000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "~openai/gpt-latest", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "openrouter", + "model": "~openai/gpt-mini-latest", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "openrouter", + "model": "~x-ai/grok-latest", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen-3-14b", + "input_price": 120000, + "output_price": 240000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen-3-235b", + "input_price": 220000, + "output_price": 880000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen-3-30b", + "input_price": 120000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen-3-32b", + "input_price": 160000, + "output_price": 640000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen-3.6-max-preview", + "input_price": 1300000, + "output_price": 7800000, + "cache_read_price": 260000, + "cache_write_price": 1625000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-235b-a22b-thinking", + "input_price": 400000, + "output_price": 4000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-coder", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-coder-30b-a3b", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-coder-next", + "input_price": 500000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-coder-plus", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-max", + "input_price": 1200000, + "output_price": 6000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-max-preview", + "input_price": 1200000, + "output_price": 6000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-max-thinking", + "input_price": 1200000, + "output_price": 6000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-next-80b-a3b-instruct", + "input_price": 150000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-next-80b-a3b-thinking", + "input_price": 150000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-vl-235b-a22b-instruct", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-vl-instruct", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3-vl-thinking", + "input_price": 400000, + "output_price": 4000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.5-flash", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 1000, + "cache_write_price": 125000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.5-plus", + "input_price": 400000, + "output_price": 2400000, + "cache_read_price": 40000, + "cache_write_price": 500000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.6-27b", + "input_price": 600000, + "output_price": 3600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.6-plus", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 100000, + "cache_write_price": 625000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.7-flash", + "input_price": 30000, + "output_price": 130000, + "cache_read_price": 6000, + "cache_write_price": 38000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.7-max", + "input_price": 2500000, + "output_price": 7500000, + "cache_read_price": 500000, + "cache_write_price": 3125000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.7-plus", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": 80000, + "cache_write_price": 500000 + }, + { + "provider": "vercel", + "model": "alibaba/qwen3.8-max", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 250000, + "cache_write_price": 2500000 + }, + { + "provider": "vercel", + "model": "amazon/nova-2-lite", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "amazon/nova-lite", + "input_price": 60000, + "output_price": 240000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "amazon/nova-micro", + "input_price": 35000, + "output_price": 140000, + "cache_read_price": 8750, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "amazon/nova-pro", + "input_price": 800000, + "output_price": 3200000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "anthropic/claude-3-haiku", + "input_price": 250000, + "output_price": 1250000, + "cache_read_price": 30000, + "cache_write_price": 300000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-fable-5", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-haiku-4.5", + "input_price": 1000000, + "output_price": 5000000, + "cache_read_price": 100000, + "cache_write_price": 1250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.1", + "input_price": 15000000, + "output_price": 75000000, + "cache_read_price": 1500000, + "cache_write_price": 18750000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.6", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.7", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.8", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-4.8-fast", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-5", + "input_price": 5000000, + "output_price": 25000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-opus-5-fast", + "input_price": 10000000, + "output_price": 50000000, + "cache_read_price": 1000000, + "cache_write_price": 12500000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-sonnet-4", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-sonnet-4.5", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-sonnet-4.6", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": 3750000 + }, + { + "provider": "vercel", + "model": "anthropic/claude-sonnet-5", + "input_price": 2000000, + "output_price": 10000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "vercel", + "model": "arcee-ai/trinity-large-thinking", + "input_price": 250000, + "output_price": 900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "arcee-ai/trinity-mini", + "input_price": 45000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "bytedance/seed-1.6", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "bytedance/seed-1.8", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "cohere/command-a", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-r1", + "input_price": 1350000, + "output_price": 5400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v3", + "input_price": 270000, + "output_price": 1120000, + "cache_read_price": 135000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v3.1", + "input_price": 250000, + "output_price": 950000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v3.1-terminus", + "input_price": 270000, + "output_price": 1000000, + "cache_read_price": 135000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v3.2", + "input_price": 280000, + "output_price": 420000, + "cache_read_price": 28000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v3.2-thinking", + "input_price": 620000, + "output_price": 1850000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v4-flash", + "input_price": 200000, + "output_price": 400000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v4-flash-0731", + "input_price": 130000, + "output_price": 260000, + "cache_read_price": 28000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "deepseek/deepseek-v4-pro", + "input_price": 435000, + "output_price": 870000, + "cache_read_price": 3600, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-2.5-flash", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-2.5-flash-image", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-2.5-flash-lite", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-2.5-pro", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3-flash", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3-pro-image", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.1-flash-image", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.1-flash-image-preview", + "input_price": 500000, + "output_price": 3000000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.1-flash-lite", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.1-flash-lite-image", + "input_price": 250000, + "output_price": 1500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.1-pro-preview", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.5-flash", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.5-flash-lite", + "input_price": 300000, + "output_price": 2500000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-3.6-flash", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemini-omni-flash-preview", + "input_price": 1500000, + "output_price": 9000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemma-4-26b-a4b-it", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 15000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "google/gemma-4-31b-it", + "input_price": 140000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "inception/mercury-2", + "input_price": 250000, + "output_price": 750000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "inception/mercury-coder-small", + "input_price": 250000, + "output_price": 1000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "inclusionai/ling-3.0-flash-free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "interfaze/interfaze-beta", + "input_price": 1500000, + "output_price": 3500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "kwaipilot/kat-coder-air-v2.5", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "kwaipilot/kat-coder-pro-v1", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "kwaipilot/kat-coder-pro-v2", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "kwaipilot/kat-coder-pro-v2.5", + "input_price": 740000, + "output_price": 2960000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/llama-3.1-70b", + "input_price": 720000, + "output_price": 720000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/llama-3.1-8b", + "input_price": 220000, + "output_price": 220000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/llama-3.3-70b", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/llama-4-maverick", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/llama-4-scout", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "meta/muse-spark-1.1", + "input_price": 1250000, + "output_price": 4250000, + "cache_read_price": 150000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 30000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.1", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 30000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.1-lightning", + "input_price": 300000, + "output_price": 2400000, + "cache_read_price": 30000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.5", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 30000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.5-highspeed", + "input_price": 600000, + "output_price": 2400000, + "cache_read_price": 30000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.7", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m2.7-highspeed", + "input_price": 600000, + "output_price": 2400000, + "cache_read_price": 60000, + "cache_write_price": 375000 + }, + { + "provider": "vercel", + "model": "minimax/minimax-m3", + "input_price": 300000, + "output_price": 1200000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/codestral", + "input_price": 300000, + "output_price": 900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/devstral-2", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/devstral-small-2", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/magistral-medium", + "input_price": 2000000, + "output_price": 5000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/magistral-small", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/ministral-14b", + "input_price": 200000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/ministral-3b", + "input_price": 40000, + "output_price": 40000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/ministral-8b", + "input_price": 100000, + "output_price": 100000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/mistral-large-3", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/mistral-medium", + "input_price": 400000, + "output_price": 2000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/mistral-medium-3.5", + "input_price": 1500000, + "output_price": 7500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/mistral-nemo", + "input_price": 150000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/mistral-small", + "input_price": 100000, + "output_price": 300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "mistral/pixtral-12b", + "input_price": 150000, + "output_price": 150000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2", + "input_price": 570000, + "output_price": 2300000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2-thinking", + "input_price": 470000, + "output_price": 2000000, + "cache_read_price": 141000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2.5", + "input_price": 600000, + "output_price": 3000000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2.6", + "input_price": 950000, + "output_price": 4000000, + "cache_read_price": 160000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2.7-code", + "input_price": 950000, + "output_price": 4000000, + "cache_read_price": 190000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k2.7-code-highspeed", + "input_price": 1900000, + "output_price": 8000000, + "cache_read_price": 380000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k3", + "input_price": 3000000, + "output_price": 15000000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "moonshotai/kimi-k3-fast", + "input_price": 4500000, + "output_price": 22500000, + "cache_read_price": 450000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "morph/morph-v3-fast", + "input_price": 800000, + "output_price": 1200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "morph/morph-v3-large", + "input_price": 900000, + "output_price": 1900000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "nvidia/nemotron-3-nano-30b-a3b", + "input_price": 50000, + "output_price": 240000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "nvidia/nemotron-3-super-120b-a12b", + "input_price": 150000, + "output_price": 650000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "nvidia/nemotron-3-ultra-550b-a55b", + "input_price": 600000, + "output_price": 2400000, + "cache_read_price": 120000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "nvidia/nemotron-nano-12b-v2-vl", + "input_price": 200000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "nvidia/nemotron-nano-9b-v2", + "input_price": 60000, + "output_price": 230000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-3.5-turbo", + "input_price": 500000, + "output_price": 1500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4-turbo", + "input_price": 10000000, + "output_price": 30000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4.1", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4.1-mini", + "input_price": 400000, + "output_price": 1600000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4.1-nano", + "input_price": 100000, + "output_price": 400000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4o", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4o-mini", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4o-mini-search-preview", + "input_price": 150000, + "output_price": 600000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4o-mini-transcribe", + "input_price": 1250000, + "output_price": 5000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-4o-transcribe", + "input_price": 2500000, + "output_price": 10000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5-codex", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 25000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5-nano", + "input_price": 50000, + "output_price": 400000, + "cache_read_price": 5000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5-pro", + "input_price": 15000000, + "output_price": 120000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.1-codex", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.1-codex-max", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.1-codex-mini", + "input_price": 250000, + "output_price": 2000000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.1-instant", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 130000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.1-thinking", + "input_price": 1250000, + "output_price": 10000000, + "cache_read_price": 125000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.2", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.2-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.2-pro", + "input_price": 21000000, + "output_price": 168000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.3-chat", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.3-codex", + "input_price": 1750000, + "output_price": 14000000, + "cache_read_price": 175000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.4", + "input_price": 2500000, + "output_price": 15000000, + "cache_read_price": 250000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.4-mini", + "input_price": 750000, + "output_price": 4500000, + "cache_read_price": 75000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.4-nano", + "input_price": 200000, + "output_price": 1250000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.4-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.5", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.5-pro", + "input_price": 30000000, + "output_price": 180000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-5.6-luna", + "input_price": 200000, + "output_price": 1200000, + "cache_read_price": 20000, + "cache_write_price": 250000 + }, + { + "provider": "vercel", + "model": "openai/gpt-5.6-sol", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": 6250000 + }, + { + "provider": "vercel", + "model": "openai/gpt-5.6-terra", + "input_price": 2000000, + "output_price": 12000000, + "cache_read_price": 200000, + "cache_write_price": 2500000 + }, + { + "provider": "vercel", + "model": "openai/gpt-image-1", + "input_price": 5000000, + "output_price": 40000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-image-1-mini", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-image-1.5", + "input_price": 5000000, + "output_price": 32000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-image-2", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 1250000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-oss-120b", + "input_price": 100000, + "output_price": 500000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-oss-20b", + "input_price": 50000, + "output_price": 200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-oss-safeguard-20b", + "input_price": 75000, + "output_price": 300000, + "cache_read_price": 37000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-realtime-1.5", + "input_price": 4000000, + "output_price": 16000000, + "cache_read_price": 400000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-realtime-2", + "input_price": 4000000, + "output_price": 24000000, + "cache_read_price": 400000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-realtime-2.1", + "input_price": 4000000, + "output_price": 24000000, + "cache_read_price": 400000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/gpt-realtime-mini", + "input_price": 600000, + "output_price": 2400000, + "cache_read_price": 60000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o1", + "input_price": 15000000, + "output_price": 60000000, + "cache_read_price": 7500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o3", + "input_price": 2000000, + "output_price": 8000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o3-deep-research", + "input_price": 10000000, + "output_price": 40000000, + "cache_read_price": 2500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o3-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 550000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o3-pro", + "input_price": 20000000, + "output_price": 80000000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "openai/o4-mini", + "input_price": 1100000, + "output_price": 4400000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "poolside/laguna-s-2.1", + "input_price": 100000, + "output_price": 200000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "poolside/laguna-s-2.1-free", + "input_price": 0, + "output_price": 0, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "sakana/fugu-ultra", + "input_price": 5000000, + "output_price": 30000000, + "cache_read_price": 500000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "stepfun/step-3.5-flash", + "input_price": 90000, + "output_price": 300000, + "cache_read_price": 20000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "stepfun/step-3.7-flash", + "input_price": 200000, + "output_price": 1150000, + "cache_read_price": 40000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "tencent/hy3", + "input_price": 140000, + "output_price": 580000, + "cache_read_price": 35000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "thinkingmachines/inkling", + "input_price": 1000000, + "output_price": 4050000, + "cache_read_price": 170000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "thinkingmachines/inkling-small", + "input_price": 500000, + "output_price": 1200000, + "cache_read_price": 100000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.1-fast-non-reasoning", + "input_price": 200000, + "output_price": 500000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.1-fast-reasoning", + "input_price": 200000, + "output_price": 500000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-multi-agent", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-multi-agent-beta", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-non-reasoning", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-non-reasoning-beta", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 400000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-reasoning", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.20-reasoning-beta", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.3", + "input_price": 1250000, + "output_price": 2500000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-4.5", + "input_price": 2000000, + "output_price": 6000000, + "cache_read_price": 300000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xai/grok-build-0.1", + "input_price": 1000000, + "output_price": 2000000, + "cache_read_price": 200000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xiaomi/mimo-v2.5", + "input_price": 140000, + "output_price": 280000, + "cache_read_price": 2800, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "xiaomi/mimo-v2.5-pro", + "input_price": 435000, + "output_price": 870000, + "cache_read_price": 3600, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.5", + "input_price": 600000, + "output_price": 2200000, + "cache_read_price": 110000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.5-air", + "input_price": 200000, + "output_price": 1100000, + "cache_read_price": 30000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.5v", + "input_price": 600000, + "output_price": 1800000, + "cache_read_price": 110000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.6", + "input_price": 600000, + "output_price": 2200000, + "cache_read_price": 110000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.6v", + "input_price": 300000, + "output_price": 900000, + "cache_read_price": 50000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.7", + "input_price": 600000, + "output_price": 2200000, + "cache_read_price": 120000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.7-flash", + "input_price": 70000, + "output_price": 400000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-4.7-flashx", + "input_price": 60000, + "output_price": 400000, + "cache_read_price": 10000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5", + "input_price": 1000000, + "output_price": 3200000, + "cache_read_price": null, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5-turbo", + "input_price": 1200000, + "output_price": 4000000, + "cache_read_price": 240000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5.1", + "input_price": 1400000, + "output_price": 4400000, + "cache_read_price": 260000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5.2", + "input_price": 1100000, + "output_price": 3851000, + "cache_read_price": 275000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5.2-fast", + "input_price": 2100000, + "output_price": 6600000, + "cache_read_price": 210000, + "cache_write_price": null + }, + { + "provider": "vercel", + "model": "zai/glm-5v-turbo", + "input_price": 1200000, + "output_price": 4000000, + "cache_read_price": 240000, + "cache_write_price": null } ] diff --git a/scripts/aibridgepricesgen/curation.json b/scripts/aibridgepricesgen/curation.json index b528c8d0d3326..be22f21e86a3a 100644 --- a/scripts/aibridgepricesgen/curation.json +++ b/scripts/aibridgepricesgen/curation.json @@ -1,4 +1,115 @@ { + "anthropic": [ + { + "modelIdentifier": "claude-fable-5", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-mythos-5", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-opus-4-8", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-opus-4-7", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-opus-4-6", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-sonnet-5", + "reasoningEffort": "high" + }, + { + "modelIdentifier": "claude-sonnet-4-6", + "reasoningEffort": "medium" + }, + { + "modelIdentifier": "claude-haiku-4-5", + "aliases": ["claude-haiku-4-5-20251001"], + "displayName": "Claude Haiku 4.5", + "thinkingBudgetTokens": 8192 + }, + { + "modelIdentifier": "claude-sonnet-4-5", + "aliases": ["claude-sonnet-4-5-20250929"], + "displayName": "Claude Sonnet 4.5", + "thinkingBudgetTokens": 8192 + } + ], + "azure": [ + { + "modelIdentifier": "claude-fable-5" + }, + { + "modelIdentifier": "claude-opus-5" + }, + { + "modelIdentifier": "claude-sonnet-5" + }, + { + "modelIdentifier": "claude-opus-4-8" + }, + { + "modelIdentifier": "claude-sonnet-4-6" + }, + { + "modelIdentifier": "claude-haiku-4-5" + }, + { + "modelIdentifier": "gpt-5.6-sol" + }, + { + "modelIdentifier": "gpt-5.6-terra" + }, + { + "modelIdentifier": "gpt-5.6-luna" + }, + { + "modelIdentifier": "gpt-5.5" + }, + { + "modelIdentifier": "deepseek-v4-flash" + } + ], + "bedrock": [ + { + "modelIdentifier": "anthropic.claude-haiku-4-5-20251001-v1:0", + "aliases": ["global.anthropic.claude-haiku-4-5-20251001-v1:0"] + }, + { + "modelIdentifier": "anthropic.claude-sonnet-5", + "aliases": ["global.anthropic.claude-sonnet-5"] + }, + { + "modelIdentifier": "anthropic.claude-opus-5", + "aliases": ["global.anthropic.claude-opus-5"] + }, + { + "modelIdentifier": "anthropic.claude-fable-5", + "aliases": ["global.anthropic.claude-fable-5"] + }, + { + "modelIdentifier": "anthropic.claude-sonnet-4-6", + "aliases": ["global.anthropic.claude-sonnet-4-6"] + }, + { + "modelIdentifier": "anthropic.claude-opus-4-8", + "aliases": ["global.anthropic.claude-opus-4-8"] + } + ], + "google": [ + { + "modelIdentifier": "gemini-flash-latest" + }, + { + "modelIdentifier": "gemini-3.6-flash" + } + ], "openai": [ { "modelIdentifier": "gpt-5.6-sol", @@ -36,46 +147,98 @@ "reasoningEffort": "medium" } ], - "anthropic": [ + "openrouter": [ { - "modelIdentifier": "claude-fable-5", - "reasoningEffort": "high" + "modelIdentifier": "openai/gpt-5.6-sol" }, { - "modelIdentifier": "claude-mythos-5", - "reasoningEffort": "high" + "modelIdentifier": "openai/gpt-5.6-terra" }, { - "modelIdentifier": "claude-opus-4-8", - "reasoningEffort": "high" + "modelIdentifier": "openai/gpt-5.6-luna" }, { - "modelIdentifier": "claude-opus-4-7", - "reasoningEffort": "high" + "modelIdentifier": "openai/gpt-oss-120b" }, { - "modelIdentifier": "claude-opus-4-6", - "reasoningEffort": "high" + "modelIdentifier": "moonshotai/kimi-k3" }, { - "modelIdentifier": "claude-sonnet-5", - "reasoningEffort": "high" + "modelIdentifier": "z-ai/glm-5.2" }, { - "modelIdentifier": "claude-sonnet-4-6", - "reasoningEffort": "medium" + "modelIdentifier": "anthropic/claude-fable-5" }, { - "modelIdentifier": "claude-haiku-4-5", - "aliases": ["claude-haiku-4-5-20251001"], - "displayName": "Claude Haiku 4.5", - "thinkingBudgetTokens": 8192 + "modelIdentifier": "anthropic/claude-opus-5" }, { - "modelIdentifier": "claude-sonnet-4-5", - "aliases": ["claude-sonnet-4-5-20250929"], - "displayName": "Claude Sonnet 4.5", - "thinkingBudgetTokens": 8192 + "modelIdentifier": "anthropic/claude-sonnet-5" + }, + { + "modelIdentifier": "anthropic/claude-opus-4.8" + }, + { + "modelIdentifier": "anthropic/claude-sonnet-4.6" + }, + { + "modelIdentifier": "anthropic/claude-haiku-4.5" + }, + { + "modelIdentifier": "deepseek/deepseek-v4-flash" + }, + { + "modelIdentifier": "deepseek/deepseek-v4-pro" + }, + { + "modelIdentifier": "qwen/qwen3.8-max" + }, + { + "modelIdentifier": "qwen/qwen3-coder-next" + } + ], + "vercel": [ + { + "modelIdentifier": "openai/gpt-5.6-sol" + }, + { + "modelIdentifier": "openai/gpt-5.6-terra" + }, + { + "modelIdentifier": "openai/gpt-5.6-luna" + }, + { + "modelIdentifier": "openai/gpt-oss-120b" + }, + { + "modelIdentifier": "moonshotai/kimi-k3" + }, + { + "modelIdentifier": "zai/glm-5.2" + }, + { + "modelIdentifier": "anthropic/claude-fable-5" + }, + { + "modelIdentifier": "anthropic/claude-opus-5" + }, + { + "modelIdentifier": "anthropic/claude-sonnet-5" + }, + { + "modelIdentifier": "anthropic/claude-opus-4.8" + }, + { + "modelIdentifier": "anthropic/claude-sonnet-4.6" + }, + { + "modelIdentifier": "anthropic/claude-haiku-4.5" + }, + { + "modelIdentifier": "deepseek/deepseek-v4-flash" + }, + { + "modelIdentifier": "deepseek/deepseek-v4-pro" } ] } diff --git a/scripts/aibridgepricesgen/main.go b/scripts/aibridgepricesgen/main.go index 19778c357f3e9..51ad5bb8bda91 100644 --- a/scripts/aibridgepricesgen/main.go +++ b/scripts/aibridgepricesgen/main.go @@ -27,7 +27,16 @@ import ( // supportedProviders lists the providers we ship prices for. Adding a // provider here is enough to include it on the next regeneration. -var supportedProviders = []string{"anthropic", "openai"} +var supportedProviders = []string{ + "anthropic", + "azure", + "bedrock", + "copilot", + "google", + "openai", + "openrouter", + "vercel", +} // upstreamProvider is the subset of a models.dev per-provider entry we read. type upstreamProvider struct { diff --git a/scripts/aibridgepricesgen/overrides.jq b/scripts/aibridgepricesgen/overrides.jq index cc1b424a1f80d..12c3985654e41 100644 --- a/scripts/aibridgepricesgen/overrides.jq +++ b/scripts/aibridgepricesgen/overrides.jq @@ -30,3 +30,18 @@ end | .name = "Claude Mythos 5" ) end + +# Mapping of provider names on models.dev to our own names +# Ref. table definition for ai_provider_type +# amazon-bedrock -> bedrock +# github-copilot -> copilot +| if (has("amazon-bedrock") | not) then + error("overrides.jq: amazon-bedrock not present upstream; drop or update the rename") +else + .bedrock = ."amazon-bedrock" | del(."amazon-bedrock") +end +| if (has("github-copilot") | not) then + error("overrides.jq: github-copilot not present upstream; drop or update the rename") +else + .copilot = ."github-copilot" | del(."github-copilot") +end diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/index.test.ts b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/index.test.ts index 53c9fa3a3b9ef..80a7ecc47a81a 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/index.test.ts +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/index.test.ts @@ -50,7 +50,7 @@ describe("formatContextBadge", () => { describe("getKnownModelsForProvider", () => { it("returns unsupported provider as an empty list", () => { - expect(getKnownModelsForProvider("azure")).toEqual([]); + expect(getKnownModelsForProvider("openai-compat")).toEqual([]); }); it("returns empty provider as an empty list", () => { diff --git a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json index bfcb8c0a23aff..5ae75467a2813 100644 --- a/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json +++ b/site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json @@ -118,6 +118,234 @@ "cacheWriteCost": 3.75 } ], + "azure": [ + { + "provider": "azure", + "modelIdentifier": "claude-fable-5", + "displayName": "Claude Fable 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 10, + "outputCost": 50, + "cacheReadCost": 1, + "cacheWriteCost": 12.5 + }, + { + "provider": "azure", + "modelIdentifier": "claude-opus-5", + "displayName": "Claude Opus 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "azure", + "modelIdentifier": "claude-sonnet-5", + "displayName": "Claude Sonnet 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 2, + "outputCost": 10, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 + }, + { + "provider": "azure", + "modelIdentifier": "claude-opus-4-8", + "displayName": "Claude Opus 4.8", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "azure", + "modelIdentifier": "claude-sonnet-4-6", + "displayName": "Claude Sonnet 4.6", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 64000, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3, + "cacheWriteCost": 3.75 + }, + { + "provider": "azure", + "modelIdentifier": "claude-haiku-4-5", + "displayName": "Claude Haiku 4.5", + "aliases": [], + "contextLimit": 200000, + "maxOutputTokens": 64000, + "inputCost": 1, + "outputCost": 5, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "azure", + "modelIdentifier": "gpt-5.6-sol", + "displayName": "GPT-5.6 Sol", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 30, + "cacheReadCost": 0.5 + }, + { + "provider": "azure", + "modelIdentifier": "gpt-5.6-terra", + "displayName": "GPT-5.6 Terra", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 2.5, + "outputCost": 15, + "cacheReadCost": 0.25, + "cacheWriteCost": 3.125 + }, + { + "provider": "azure", + "modelIdentifier": "gpt-5.6-luna", + "displayName": "GPT-5.6 Luna", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 1, + "outputCost": 6, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "azure", + "modelIdentifier": "gpt-5.5", + "displayName": "GPT-5.5", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 30, + "cacheReadCost": 0.5 + }, + { + "provider": "azure", + "modelIdentifier": "deepseek-v4-flash", + "displayName": "DeepSeek-V4-Flash", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 384000, + "inputCost": 0.19, + "outputCost": 0.51 + } + ], + "bedrock": [ + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-haiku-4-5-20251001-v1:0", + "displayName": "Claude Haiku 4.5", + "aliases": ["global.anthropic.claude-haiku-4-5-20251001-v1:0"], + "contextLimit": 200000, + "maxOutputTokens": 64000, + "inputCost": 1, + "outputCost": 5, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-sonnet-5", + "displayName": "Claude Sonnet 5", + "aliases": ["global.anthropic.claude-sonnet-5"], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 2, + "outputCost": 10, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 + }, + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-opus-5", + "displayName": "Claude Opus 5", + "aliases": ["global.anthropic.claude-opus-5"], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-fable-5", + "displayName": "Claude Fable 5", + "aliases": ["global.anthropic.claude-fable-5"], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 10, + "outputCost": 50, + "cacheReadCost": 1, + "cacheWriteCost": 12.5 + }, + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-sonnet-4-6", + "displayName": "Claude Sonnet 4.6", + "aliases": ["global.anthropic.claude-sonnet-4-6"], + "contextLimit": 1000000, + "maxOutputTokens": 64000, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3, + "cacheWriteCost": 3.75 + }, + { + "provider": "bedrock", + "modelIdentifier": "anthropic.claude-opus-4-8", + "displayName": "Claude Opus 4.8", + "aliases": ["global.anthropic.claude-opus-4-8"], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + } + ], + "google": [ + { + "provider": "google", + "modelIdentifier": "gemini-flash-latest", + "displayName": "Gemini Flash Latest", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 65536, + "inputCost": 1.5, + "outputCost": 9, + "cacheReadCost": 0.15 + }, + { + "provider": "google", + "modelIdentifier": "gemini-3.6-flash", + "displayName": "Gemini 3.6 Flash", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 65536, + "inputCost": 1.5, + "outputCost": 7.5, + "cacheReadCost": 0.15 + } + ], "openai": [ { "provider": "openai", @@ -140,10 +368,10 @@ "contextLimit": 1050000, "maxOutputTokens": 128000, "reasoningEffort": "medium", - "inputCost": 2.5, - "outputCost": 15, - "cacheReadCost": 0.25, - "cacheWriteCost": 3.125 + "inputCost": 2, + "outputCost": 12, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 }, { "provider": "openai", @@ -153,10 +381,10 @@ "contextLimit": 1050000, "maxOutputTokens": 128000, "reasoningEffort": "medium", - "inputCost": 1, - "outputCost": 6, - "cacheReadCost": 0.1, - "cacheWriteCost": 1.25 + "inputCost": 0.2, + "outputCost": 1.2, + "cacheReadCost": 0.02, + "cacheWriteCost": 0.25 }, { "provider": "openai", @@ -227,5 +455,356 @@ "outputCost": 14, "cacheReadCost": 0.175 } + ], + "openrouter": [ + { + "provider": "openrouter", + "modelIdentifier": "openai/gpt-5.6-sol", + "displayName": "GPT-5.6 Sol", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 30, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "openrouter", + "modelIdentifier": "openai/gpt-5.6-terra", + "displayName": "GPT-5.6 Terra", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 1, + "outputCost": 6, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "openrouter", + "modelIdentifier": "openai/gpt-5.6-luna", + "displayName": "GPT-5.6 Luna", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 0.1, + "outputCost": 0.6, + "cacheReadCost": 0.01, + "cacheWriteCost": 0.125 + }, + { + "provider": "openrouter", + "modelIdentifier": "openai/gpt-oss-120b", + "displayName": "GPT OSS 120B", + "aliases": [], + "contextLimit": 131072, + "maxOutputTokens": 131072, + "inputCost": 0.037, + "outputCost": 0.17 + }, + { + "provider": "openrouter", + "modelIdentifier": "moonshotai/kimi-k3", + "displayName": "Kimi K3", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 1048576, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3 + }, + { + "provider": "openrouter", + "modelIdentifier": "z-ai/glm-5.2", + "displayName": "GLM-5.2", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 262144, + "inputCost": 0.76, + "outputCost": 2.42, + "cacheReadCost": 0.14 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-fable-5", + "displayName": "Claude Fable 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 10, + "outputCost": 50, + "cacheReadCost": 1, + "cacheWriteCost": 12.5 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-opus-5", + "displayName": "Claude Opus 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-sonnet-5", + "displayName": "Claude Sonnet 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 2, + "outputCost": 10, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-opus-4.8", + "displayName": "Claude Opus 4.8", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-sonnet-4.6", + "displayName": "Claude Sonnet 4.6", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3, + "cacheWriteCost": 3.75 + }, + { + "provider": "openrouter", + "modelIdentifier": "anthropic/claude-haiku-4.5", + "displayName": "Claude Haiku 4.5 (latest)", + "aliases": [], + "contextLimit": 200000, + "maxOutputTokens": 64000, + "inputCost": 1, + "outputCost": 5, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "openrouter", + "modelIdentifier": "deepseek/deepseek-v4-flash", + "displayName": "DeepSeek V4 Flash", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 393216, + "inputCost": 0.14, + "outputCost": 0.28, + "cacheReadCost": 0.028 + }, + { + "provider": "openrouter", + "modelIdentifier": "deepseek/deepseek-v4-pro", + "displayName": "DeepSeek V4 Pro", + "aliases": [], + "contextLimit": 1048576, + "maxOutputTokens": 384000, + "inputCost": 0.435, + "outputCost": 0.87, + "cacheReadCost": 0.003625 + }, + { + "provider": "openrouter", + "modelIdentifier": "qwen/qwen3.8-max", + "displayName": "Qwen3.8 Max", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 131072, + "inputCost": 2, + "outputCost": 6, + "cacheReadCost": 0.25, + "cacheWriteCost": 2.5 + }, + { + "provider": "openrouter", + "modelIdentifier": "qwen/qwen3-coder-next", + "displayName": "Qwen3 Coder Next", + "aliases": [], + "contextLimit": 262144, + "maxOutputTokens": 262144, + "inputCost": 0.12, + "outputCost": 0.8, + "cacheReadCost": 0.07 + } + ], + "vercel": [ + { + "provider": "vercel", + "modelIdentifier": "openai/gpt-5.6-sol", + "displayName": "GPT 5.6 Sol", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 30, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "vercel", + "modelIdentifier": "openai/gpt-5.6-terra", + "displayName": "GPT 5.6 Terra", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 2, + "outputCost": 12, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 + }, + { + "provider": "vercel", + "modelIdentifier": "openai/gpt-5.6-luna", + "displayName": "GPT 5.6 Luna", + "aliases": [], + "contextLimit": 1050000, + "maxOutputTokens": 128000, + "inputCost": 0.2, + "outputCost": 1.2, + "cacheReadCost": 0.02, + "cacheWriteCost": 0.25 + }, + { + "provider": "vercel", + "modelIdentifier": "openai/gpt-oss-120b", + "displayName": "GPT OSS 120B", + "aliases": [], + "contextLimit": 131072, + "maxOutputTokens": 131072, + "inputCost": 0.1, + "outputCost": 0.5 + }, + { + "provider": "vercel", + "modelIdentifier": "moonshotai/kimi-k3", + "displayName": "Kimi K3", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 131072, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3 + }, + { + "provider": "vercel", + "modelIdentifier": "zai/glm-5.2", + "displayName": "GLM 5.2", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 1.1, + "outputCost": 3.851, + "cacheReadCost": 0.275 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-fable-5", + "displayName": "Claude Fable 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 10, + "outputCost": 50, + "cacheReadCost": 1, + "cacheWriteCost": 12.5 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-opus-5", + "displayName": "Claude Opus 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-sonnet-5", + "displayName": "Claude Sonnet 5", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 2, + "outputCost": 10, + "cacheReadCost": 0.2, + "cacheWriteCost": 2.5 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-opus-4.8", + "displayName": "Claude Opus 4.8", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 5, + "outputCost": 25, + "cacheReadCost": 0.5, + "cacheWriteCost": 6.25 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-sonnet-4.6", + "displayName": "Claude Sonnet 4.6", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 128000, + "inputCost": 3, + "outputCost": 15, + "cacheReadCost": 0.3, + "cacheWriteCost": 3.75 + }, + { + "provider": "vercel", + "modelIdentifier": "anthropic/claude-haiku-4.5", + "displayName": "Claude Haiku 4.5", + "aliases": [], + "contextLimit": 200000, + "maxOutputTokens": 64000, + "inputCost": 1, + "outputCost": 5, + "cacheReadCost": 0.1, + "cacheWriteCost": 1.25 + }, + { + "provider": "vercel", + "modelIdentifier": "deepseek/deepseek-v4-flash", + "displayName": "DeepSeek V4 Flash", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 384000, + "inputCost": 0.2, + "outputCost": 0.4, + "cacheReadCost": 0.04 + }, + { + "provider": "vercel", + "modelIdentifier": "deepseek/deepseek-v4-pro", + "displayName": "DeepSeek V4 Pro", + "aliases": [], + "contextLimit": 1000000, + "maxOutputTokens": 384000, + "inputCost": 0.435, + "outputCost": 0.87, + "cacheReadCost": 0.0036 + } ] } From 2934aa079ad54ded6f8d0c3c568db20915dd8bbb Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 17 Aug 2026 08:23:15 +0100 Subject: [PATCH 07/12] fix: price AI usage by configured provider type (#27836) (#28104) Backport of https://github.com/coder/coder/pull/27836 Original PR: #27836 - fix: price AI usage by configured provider type Merge commit: db88ec3f6ad564c7029e22a2d515b7a031c17ebc Requested by: @ssncferreira --- coderd/aibridgedserver/aibridgedserver.go | 1 + .../aibridgedserver/aibridgedserver_test.go | 338 +++++++++++++++++- coderd/aibridgedserver/cost.go | 33 +- coderd/aibridgedserver/metrics.go | 5 +- docs/admin/integrations/prometheus.md | 2 +- scripts/metricsdocgen/metrics | 2 +- 6 files changed, 357 insertions(+), 24 deletions(-) diff --git a/coderd/aibridgedserver/aibridgedserver.go b/coderd/aibridgedserver/aibridgedserver.go index c9906bd35aa9c..038286555f756 100644 --- a/coderd/aibridgedserver/aibridgedserver.go +++ b/coderd/aibridgedserver/aibridgedserver.go @@ -78,6 +78,7 @@ type store interface { // Cost-attribution queries, used to snapshot price and effective group on // each token usage record. GetAIBridgeInterceptionByID(ctx context.Context, id uuid.UUID) (database.AIBridgeInterception, error) + GetAIProviderByName(ctx context.Context, name string) (database.AIProvider, error) GetAIModelPriceByProviderModel(ctx context.Context, arg database.GetAIModelPriceByProviderModelParams) (database.AIModelPrice, error) GetUserAIBudgetOverride(ctx context.Context, userID uuid.UUID) (database.UserAIBudgetOverride, error) GetHighestGroupAIBudgetByUser(ctx context.Context, userID uuid.UUID) (database.GetHighestGroupAIBudgetByUserRow, error) diff --git a/coderd/aibridgedserver/aibridgedserver_test.go b/coderd/aibridgedserver/aibridgedserver_test.go index f5641a0531cc6..8fa4776fb92b7 100644 --- a/coderd/aibridgedserver/aibridgedserver_test.go +++ b/coderd/aibridgedserver/aibridgedserver_test.go @@ -1684,6 +1684,17 @@ func TestRecordTokenUsage(t *testing.T) { now = time.Date(2026, 6, 25, 14, 30, 0, 0, time.UTC) ) + // Budget resolution falls through to the Everyone group, for cases that vary + // only provider resolution. + expectBudgetLookups := func(db *dbmock.MockStore, intc database.AIBridgeInterception) { + db.EXPECT().GetAIBridgeInterceptionByID(gomock.Any(), intc.ID).Return(intc, nil) + db.EXPECT().GetUserAIBudgetOverride(gomock.Any(), intc.InitiatorID). + Return(database.UserAIBudgetOverride{}, sql.ErrNoRows) + db.EXPECT().GetHighestGroupAIBudgetByUser(gomock.Any(), intc.InitiatorID). + Return(database.GetHighestGroupAIBudgetByUserRow{}, sql.ErrNoRows) + db.EXPECT().GetUserEveryoneFallbackGroup(gomock.Any(), intc.InitiatorID).Return(uuid.New(), nil) + } + testRecordMethod(t, func(srv *aibridgedserver.Server, ctx context.Context, req *proto.RecordTokenUsageRequest) (*proto.RecordTokenUsageResponse, error) { return srv.RecordTokenUsage(ctx, req) @@ -2131,6 +2142,123 @@ func TestRecordTokenUsage(t *testing.T) { db.EXPECT().IncrementUserAIDailySpend(gomock.Any(), gomock.Any()).Times(0) }, }, + { + // An azure provider has the openai upstream wire format but bills at + // its own rates. + name: "openai wire format priced as azure", + request: &proto.RecordTokenUsageRequest{ + InterceptionId: uuid.NewString(), + MsgId: "msg_123", + InputTokens: 100, + CreatedAt: timestamppb.Now(), + }, + setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordTokenUsageRequest) { + interceptionID, err := uuid.Parse(req.GetInterceptionId()) + assert.NoError(t, err, "parse interception UUID") + + intc := newTestInterception(interceptionID) + intc.Provider = "openai" + intc.ProviderName = "azure-prod" + intc.Model = "gpt-5-mini" + expectBudgetLookups(db, intc) + + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName). + Return(database.AIProvider{Name: intc.ProviderName, Type: database.AIProviderTypeAzure}, nil) + db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), database.GetAIModelPriceByProviderModelParams{ + Provider: string(database.AIProviderTypeAzure), + Model: intc.Model, + }).Return(database.AIModelPrice{ + Provider: string(database.AIProviderTypeAzure), + Model: intc.Model, + InputPrice: sql.NullInt64{Int64: 3_000_000, Valid: true}, + }, nil) + + db.EXPECT().InTx(gomock.Any(), nil).DoAndReturn( + func(fn func(database.Store) error, _ *database.TxOptions) error { return fn(db) }, + ) + db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), gomock.Cond(func(p database.InsertAIBridgeTokenUsageParams) bool { + return assert.Equal(t, sql.NullInt64{Int64: 3_000_000, Valid: true}, p.InputPriceMicros, "input price") && + assert.Equal(t, sql.NullInt64{Int64: 300, Valid: true}, p.CostMicros, "cost") + })).Return(database.AIBridgeTokenUsage{ID: uuid.New(), InterceptionID: interceptionID}, nil) + db.EXPECT().IncrementUserAIDailySpend(gomock.Any(), gomock.Any()). + Return(database.AIUserDailySpend{}, nil) + }, + }, + { + // A bedrock provider has the anthropic upstream wire format but bills + // at its own rates. + name: "anthropic wire format priced as bedrock", + request: &proto.RecordTokenUsageRequest{ + InterceptionId: uuid.NewString(), + MsgId: "msg_123", + InputTokens: 100, + CreatedAt: timestamppb.Now(), + }, + setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordTokenUsageRequest) { + interceptionID, err := uuid.Parse(req.GetInterceptionId()) + assert.NoError(t, err, "parse interception UUID") + + intc := newTestInterception(interceptionID) + intc.ProviderName = "bedrock-eu" + expectBudgetLookups(db, intc) + + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName). + Return(database.AIProvider{Name: intc.ProviderName, Type: database.AIProviderTypeBedrock}, nil) + db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), database.GetAIModelPriceByProviderModelParams{ + Provider: string(database.AIProviderTypeBedrock), + Model: intc.Model, + }).Return(database.AIModelPrice{ + Provider: string(database.AIProviderTypeBedrock), + Model: intc.Model, + InputPrice: sql.NullInt64{Int64: 3_000_000, Valid: true}, + }, nil) + + db.EXPECT().InTx(gomock.Any(), nil).DoAndReturn( + func(fn func(database.Store) error, _ *database.TxOptions) error { return fn(db) }, + ) + db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), gomock.Cond(func(p database.InsertAIBridgeTokenUsageParams) bool { + return assert.Equal(t, sql.NullInt64{Int64: 3_000_000, Valid: true}, p.InputPriceMicros, "input price") && + assert.Equal(t, sql.NullInt64{Int64: 300, Valid: true}, p.CostMicros, "cost") + })).Return(database.AIBridgeTokenUsage{ID: uuid.New(), InterceptionID: interceptionID}, nil) + db.EXPECT().IncrementUserAIDailySpend(gomock.Any(), gomock.Any()). + Return(database.AIUserDailySpend{}, nil) + }, + }, + { + name: "unresolved provider is unpriced", + request: &proto.RecordTokenUsageRequest{ + InterceptionId: uuid.NewString(), + MsgId: "msg_123", + InputTokens: 100, + CreatedAt: timestamppb.Now(), + }, + setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordTokenUsageRequest) { + interceptionID, err := uuid.Parse(req.GetInterceptionId()) + assert.NoError(t, err, "parse interception UUID") + + intc := newTestInterception(interceptionID) + expectBudgetLookups(db, intc) + + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName). + Return(database.AIProvider{}, sql.ErrNoRows) + // Without a provider there is nothing to key the price on. + db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()).Times(0) + + db.EXPECT().InTx(gomock.Any(), nil).DoAndReturn( + func(fn func(database.Store) error, _ *database.TxOptions) error { return fn(db) }, + ) + db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), gomock.Cond(func(p database.InsertAIBridgeTokenUsageParams) bool { + return assert.False(t, p.InputPriceMicros.Valid, "input price null") && + assert.False(t, p.CostMicros.Valid, "cost null") + })).Return(database.AIBridgeTokenUsage{ID: uuid.New(), InterceptionID: interceptionID}, nil) + db.EXPECT().IncrementUserAIDailySpend(gomock.Any(), gomock.Any()).Times(0) + }, + // The metric names the provider that failed to resolve. + assertMetrics: func(t *testing.T, reg *prometheus.Registry) { + require.Equal(t, 1, promhelp.CounterValue(t, reg, "cost_control_unpriced_token_usage_records_total", + prometheus.Labels{"provider": "anthropic-eu", "model": "claude-sonnet-4-6"})) + }, + }, { name: "invalid interception ID", request: &proto.RecordTokenUsageRequest{ @@ -2162,6 +2290,27 @@ func TestRecordTokenUsage(t *testing.T) { }, expectedErr: "get interception", }, + { + name: "provider lookup error", + request: &proto.RecordTokenUsageRequest{ + InterceptionId: uuid.NewString(), + MsgId: "msg_123", + InputTokens: 100, + CreatedAt: timestamppb.Now(), + }, + setupMocks: func(t *testing.T, db *dbmock.MockStore, req *proto.RecordTokenUsageRequest) { + interceptionID, err := uuid.Parse(req.GetInterceptionId()) + assert.NoError(t, err, "parse interception UUID") + + // An unexpected provider lookup error (not sql.ErrNoRows) fails + // the record. + intc := newTestInterception(interceptionID) + expectBudgetLookups(db, intc) + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName). + Return(database.AIProvider{}, sql.ErrConnDone) + }, + expectedErr: "get configured provider", + }, { name: "price lookup error", request: &proto.RecordTokenUsageRequest{ @@ -2185,6 +2334,8 @@ func TestRecordTokenUsage(t *testing.T) { Return(database.GetHighestGroupAIBudgetByUserRow{}, sql.ErrNoRows) db.EXPECT().GetUserEveryoneFallbackGroup(gomock.Any(), intc.InitiatorID). Return(uuid.New(), nil) + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName). + Return(database.AIProvider{Name: intc.ProviderName, Type: database.AIProviderTypeAnthropic}, nil) db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()). Return(database.AIModelPrice{}, sql.ErrConnDone) }, @@ -2287,10 +2438,18 @@ func TestRecordTokenUsageAuthorized(t *testing.T) { require.NoError(t, err) require.NoError(t, rawDB.UpsertAIModelPrices(ctx, priceSeed), "seed model prices") + // The interception's provider name resolves to this provider, whose type keys + // the price lookup. + aiProvider := dbgen.AIProvider(t, rawDB, database.AIProvider{ + Name: "anthropic-eu", + Type: database.AIProviderTypeAnthropic, + }) + intc := dbgen.AIBridgeInterception(t, rawDB, database.InsertAIBridgeInterceptionParams{ - InitiatorID: user.ID, - Provider: provider, - Model: model, + InitiatorID: user.ID, + Provider: provider, + ProviderName: aiProvider.Name, + Model: model, }, nil) // Use fixed dates to keep the test deterministic. @@ -2348,6 +2507,152 @@ func TestRecordTokenUsageAuthorized(t *testing.T) { require.Equal(t, wantCost, spend.SpendMicros, "spend micros") } +// TestRecordTokenUsageProviderResolution covers provider resolution against a real +// database through dbauthz, where the live-row filter and name reuse apply. +func TestRecordTokenUsageProviderResolution(t *testing.T) { + t.Parallel() + + const claudeModel, gptModel = "claude-sonnet-4-6", "gpt-5-mini" + const anthropicInputPrice, bedrockInputPrice, openaiInputPrice, azureInputPrice int64 = 2_000_000, 3_000_000, 4_000_000, 5_000_000 + + setupCtx := testutil.Context(t, testutil.WaitLong) + logger := testutil.Logger(t) + + rawDB, _ := dbtestutil.NewDB(t) + authzDB := dbauthz.New(rawDB, rbac.NewStrictAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer()) + + user := dbgen.User(t, rawDB, database.User{}) + + // Prices differ per provider type so the asserted cost identifies which type resolved. + priceSeed, err := json.Marshal([]map[string]any{ + {"provider": string(database.AIProviderTypeAnthropic), "model": claudeModel, "input_price": anthropicInputPrice}, + {"provider": string(database.AIProviderTypeBedrock), "model": claudeModel, "input_price": bedrockInputPrice}, + {"provider": string(database.AIProviderTypeOpenai), "model": gptModel, "input_price": openaiInputPrice}, + {"provider": string(database.AIProviderTypeAzure), "model": gptModel, "input_price": azureInputPrice}, + }) + require.NoError(t, err) + require.NoError(t, rawDB.UpsertAIModelPrices(setupCtx, priceSeed), "seed model prices") + + srv, err := aibridgedserver.NewServer(setupCtx, aibridgedserver.Options{ + Store: authzDB, + AISeatTracker: agplaiseats.Noop{}, + AccessURL: "/", + GatewayCfg: codersdk.AIBridgeConfig{}, + Experiments: requiredExperiments, + Logger: logger, + Clock: quartz.NewReal(), + }) + require.NoError(t, err) + + cases := []struct { + name string + // wireProvider is the upstream wire format recorded on the interception. + wireProvider string + // providerName is the provider instance name recorded on the interception. + providerName string + // providerType is the configured provider type of the live provider. + providerType database.AIProviderType + model string + // setupProvider creates and deletes the case's providers. + setupProvider func(t *testing.T, ctx context.Context, providerName string, providerType database.AIProviderType) + wantInputPrice sql.NullInt64 + wantCost sql.NullInt64 + }{ + { + // The common configuration, where the configured provider type matches + // the upstream wire format. + name: "provider named after its type", + wireProvider: "anthropic", + providerName: "anthropic", + providerType: database.AIProviderTypeAnthropic, + model: claudeModel, + // One live anthropic provider. + setupProvider: func(t *testing.T, _ context.Context, providerName string, providerType database.AIProviderType) { + dbgen.AIProvider(t, rawDB, database.AIProvider{Name: providerName, Type: providerType}) + }, + wantInputPrice: sql.NullInt64{Int64: anthropicInputPrice, Valid: true}, + // 100 input tokens at the anthropic input price: $0.0002. + wantCost: sql.NullInt64{Int64: 200, Valid: true}, + }, + { + name: "priced by configured provider type", + wireProvider: "anthropic", + providerName: "bedrock-eu", + providerType: database.AIProviderTypeBedrock, + model: claudeModel, + // One live bedrock provider. + setupProvider: func(t *testing.T, _ context.Context, providerName string, providerType database.AIProviderType) { + dbgen.AIProvider(t, rawDB, database.AIProvider{Name: providerName, Type: providerType}) + }, + wantInputPrice: sql.NullInt64{Int64: bedrockInputPrice, Valid: true}, + // 100 input tokens at the bedrock input price: $0.0003. + wantCost: sql.NullInt64{Int64: 300, Valid: true}, + }, + { + name: "deleted provider is unpriced", + wireProvider: "anthropic", + providerName: "bedrock-deleted", + providerType: database.AIProviderTypeBedrock, + model: claudeModel, + // One bedrock provider, deleted before the usage is recorded. + setupProvider: func(t *testing.T, ctx context.Context, providerName string, providerType database.AIProviderType) { + provider := dbgen.AIProvider(t, rawDB, database.AIProvider{Name: providerName, Type: providerType}) + require.NoError(t, rawDB.DeleteAIProviderByID(ctx, provider.ID), "delete provider") + }, + wantInputPrice: sql.NullInt64{Valid: false}, + wantCost: sql.NullInt64{Valid: false}, + }, + { + // Names are unique only among live providers, so a deleted name can be + // reused by a provider of a different configured provider type. + name: "reused name resolves to the live provider", + wireProvider: "openai", + providerName: "reused-name", + providerType: database.AIProviderTypeAzure, + model: gptModel, + // A deleted openai provider and a live azure provider sharing the name. + setupProvider: func(t *testing.T, ctx context.Context, providerName string, providerType database.AIProviderType) { + deleted := dbgen.AIProvider(t, rawDB, database.AIProvider{Name: providerName, Type: database.AIProviderTypeOpenai}) + require.NoError(t, rawDB.DeleteAIProviderByID(ctx, deleted.ID), "delete provider") + dbgen.AIProvider(t, rawDB, database.AIProvider{Name: providerName, Type: providerType}) + }, + wantInputPrice: sql.NullInt64{Int64: azureInputPrice, Valid: true}, + // 100 input tokens at the azure input price: $0.0005. + wantCost: sql.NullInt64{Int64: 500, Valid: true}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx := testutil.Context(t, testutil.WaitLong) + tc.setupProvider(t, ctx, tc.providerName, tc.providerType) + + intc := dbgen.AIBridgeInterception(t, rawDB, database.InsertAIBridgeInterceptionParams{ + InitiatorID: user.ID, + Provider: tc.wireProvider, + ProviderName: tc.providerName, + Model: tc.model, + }, nil) + + _, err := srv.RecordTokenUsage(ctx, &proto.RecordTokenUsageRequest{ + InterceptionId: intc.ID.String(), + MsgId: "msg_e2e", + InputTokens: 100, + CreatedAt: timestamppb.Now(), + }) + require.NoError(t, err, "record token usage") + + tokenUsages, err := rawDB.GetAIBridgeTokenUsagesByInterceptionID(ctx, intc.ID) + require.NoError(t, err) + require.Len(t, tokenUsages, 1) + require.Equal(t, tc.wantInputPrice, tokenUsages[0].InputPriceMicros, "input price") + require.Equal(t, tc.wantCost, tokenUsages[0].CostMicros, "cost") + }) + } +} + // TestRecordTokenUsageBudgetNotifications verifies that recording token usage // enqueues the right budget notifications: the warning template when spend // crosses the warning threshold, the limit-reached template at 100%, both when @@ -2900,21 +3205,25 @@ func TestRecordTokenUsageBudgetAdminNotification(t *testing.T) { } // newTestInterception returns an interception with a fixed initiator, provider, -// and model for cost-attribution test setup. +// and model for cost-attribution test setup. The provider name intentionally +// differs from the upstream wire format. func newTestInterception(id uuid.UUID) database.AIBridgeInterception { return database.AIBridgeInterception{ - ID: id, - InitiatorID: uuid.New(), - Provider: "anthropic", - Model: "claude-sonnet-4-6", + ID: id, + InitiatorID: uuid.New(), + Provider: "anthropic", + ProviderName: "anthropic-eu", + Model: "claude-sonnet-4-6", } } // expectTokenUsageCostLookups mocks the store lookups made by resolveTokenUsageCost -// (budget resolution and the price lookup). A nil override, group, everyoneGroupID, or -// price makes that lookup return sql.ErrNoRows. Budget resolution mirrors production code: -// a non-nil override wins and skips the group lookup, and the Everyone fallback is consulted -// only when both override and group are nil. +// (budget resolution, provider resolution, and the price lookup). A nil override, group, +// everyoneGroupID, or price makes that lookup return sql.ErrNoRows. Budget resolution +// mirrors production code: a non-nil override wins and skips the group lookup, and the +// Everyone fallback is consulted only when both override and group are nil. The provider +// name resolves to a provider whose configured provider type equals the interception's +// upstream wire format. func expectTokenUsageCostLookups( db *dbmock.MockStore, intc database.AIBridgeInterception, @@ -2945,6 +3254,11 @@ func expectTokenUsageCostLookups( } } + db.EXPECT().GetAIProviderByName(gomock.Any(), intc.ProviderName).Return(database.AIProvider{ + Name: intc.ProviderName, + Type: database.AIProviderType(intc.Provider), + }, nil) + if price != nil { db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), database.GetAIModelPriceByProviderModelParams{ Provider: intc.Provider, diff --git a/coderd/aibridgedserver/cost.go b/coderd/aibridgedserver/cost.go index ab1c13472aef4..ba56412a4554e 100644 --- a/coderd/aibridgedserver/cost.go +++ b/coderd/aibridgedserver/cost.go @@ -34,10 +34,11 @@ type tokenUsageCost struct { } // resolveTokenUsageCost resolves the effective group and per-token prices for an -// interception and computes its cost. Two independent conditions yield a NULL +// interception and computes its cost. Three independent conditions yield a NULL // column rather than an error: an unresolved effective group (the user has no -// org membership), and a model absent from the price table leaves prices and -// cost NULL (a NULL cost unambiguously means "model not priced"). +// org membership), an interception whose provider name matches no configured +// provider, and a model absent from the price table. The latter two leave prices +// and cost NULL (a NULL cost unambiguously means "model not priced"). // Any other error is returned. func (s *Server) resolveTokenUsageCost(ctx context.Context, intc database.AIBridgeInterception, in *proto.RecordTokenUsageRequest) (tokenUsageCost, error) { var result tokenUsageCost @@ -63,22 +64,40 @@ func (s *Server) resolveTokenUsageCost(ctx context.Context, intc database.AIBrid } } + // The interception records one of three upstream wire formats. Prices are + // keyed on the configured provider type, the provider actually serving the + // request, resolved by provider name. Names are unique among live providers. + provider, err := s.store.GetAIProviderByName(ctx, intc.ProviderName) + switch { + case errors.Is(err, sql.ErrNoRows): + // Only reachable if the provider was deleted mid-request. + s.logger.Info(ctx, "no configured provider found for interception, recording token usage with NULL cost", + slog.F("provider_name", intc.ProviderName), slog.F("model", intc.Model)) + if s.metrics != nil { + s.metrics.UnpricedTokenUsageRecords.WithLabelValues(intc.ProviderName, intc.Model).Inc() + } + return result, nil + case err != nil: + return tokenUsageCost{}, xerrors.Errorf("get configured provider %q: %w", intc.ProviderName, err) + } + configuredType := string(provider.Type) + // Snapshot the price for this (provider, model) and compute cost. price, err := s.store.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{ - Provider: intc.Provider, + Provider: configuredType, Model: intc.Model, }) switch { case errors.Is(err, sql.ErrNoRows): // Model not in the price table: record tokens but leave cost NULL. s.logger.Info(ctx, "no price found for model, recording token usage with NULL cost", - slog.F("provider", intc.Provider), slog.F("model", intc.Model)) + slog.F("provider", configuredType), slog.F("model", intc.Model)) if s.metrics != nil { - s.metrics.UnpricedTokenUsageRecords.WithLabelValues(intc.Provider, intc.Model).Inc() + s.metrics.UnpricedTokenUsageRecords.WithLabelValues(configuredType, intc.Model).Inc() } return result, nil case err != nil: - return tokenUsageCost{}, xerrors.Errorf("look up model price for %s/%s: %w", intc.Provider, intc.Model, err) + return tokenUsageCost{}, xerrors.Errorf("look up model price for %s/%s: %w", configuredType, intc.Model, err) } result.inputPriceMicros = price.InputPrice diff --git a/coderd/aibridgedserver/metrics.go b/coderd/aibridgedserver/metrics.go index dfd3cfdf8bcfb..b88b8789a6f6b 100644 --- a/coderd/aibridgedserver/metrics.go +++ b/coderd/aibridgedserver/metrics.go @@ -50,12 +50,11 @@ func NewMetrics(reg prometheus.Registerer) *Metrics { Name: "blocked_users", Help: "The number of users currently over their AI budget.", }, []string{"group_id"}), - // Pessimistic cardinality: 3 providers, 5 models = up to 15. + // Pessimistic cardinality: one series per configured provider and model. UnpricedTokenUsageRecords: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ Subsystem: "cost_control", Name: "unpriced_token_usage_records_total", - Help: "The number of recorded AI token-usage records for which no model price was found " + - "(provider: anthropic, openai, copilot).", + Help: "The number of recorded AI token-usage records for which no (provider, model) price was found.", }, []string{"provider", "model"}), // Pessimistic cardinality: 3 outcomes, 8 buckets + 3 extra series // (count, sum, +Inf) = up to 33. diff --git a/docs/admin/integrations/prometheus.md b/docs/admin/integrations/prometheus.md index 6c409ef04b2e7..408e87882b257 100644 --- a/docs/admin/integrations/prometheus.md +++ b/docs/admin/integrations/prometheus.md @@ -125,7 +125,7 @@ The `coder_ai_gateway_cost_control_*` metrics are exported only by `coderd`. | `coder_ai_gateway_cost_control_blocked_requests_total` | counter | The number of AI requests blocked because the initiator's budget was exceeded. | `group_id` | | `coder_ai_gateway_cost_control_blocked_users` | gauge | The number of users currently over their AI budget. | `group_id` | | `coder_ai_gateway_cost_control_enforcement_duration_seconds` | histogram | The duration of AI budget enforcement checks, in seconds (outcome: allowed, blocked, error). | `outcome` | -| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | The number of recorded AI token-usage records for which no model price was found (provider: anthropic, openai, copilot). | `model` `provider` | +| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | The number of recorded AI token-usage records for which no (provider, model) price was found. | `model` `provider` | | `coder_ai_gateway_injected_tool_invocations_total` | counter | The number of times an injected MCP tool was invoked by AI Gateway. | `model` `name` `provider` `server` | | `coder_ai_gateway_interceptions_duration_seconds` | histogram | The total duration of intercepted requests, in seconds. The majority of this time will be the upstream processing of the request. AI Gateway has no control over upstream processing time, so it's just an illustrative metric. | `model` `provider` | | `coder_ai_gateway_interceptions_inflight` | gauge | The number of intercepted requests which are being processed. | `model` `provider` `route` | diff --git a/scripts/metricsdocgen/metrics b/scripts/metricsdocgen/metrics index 6464f6ff94b43..755068744c8ff 100644 --- a/scripts/metricsdocgen/metrics +++ b/scripts/metricsdocgen/metrics @@ -166,7 +166,7 @@ coder_ai_gateway_cost_control_enforcement_duration_seconds_bucket{outcome="allow coder_ai_gateway_cost_control_enforcement_duration_seconds_bucket{outcome="allowed",le="+Inf"} 0 coder_ai_gateway_cost_control_enforcement_duration_seconds_sum{outcome="allowed"} 0 coder_ai_gateway_cost_control_enforcement_duration_seconds_count{outcome="allowed"} 0 -# HELP coder_ai_gateway_cost_control_unpriced_token_usage_records_total The number of recorded AI token-usage records for which no model price was found (provider: anthropic, openai, copilot). +# HELP coder_ai_gateway_cost_control_unpriced_token_usage_records_total The number of recorded AI token-usage records for which no (provider, model) price was found. # TYPE coder_ai_gateway_cost_control_unpriced_token_usage_records_total counter coder_ai_gateway_cost_control_unpriced_token_usage_records_total{model="gpt-5-nano",provider="openai"} 0 # HELP coder_ai_gateway_injected_tool_invocations_total The number of times an injected MCP tool was invoked by AI Gateway. From d295caf5eaf14c562ff3c1534f225bf84761bae8 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 17 Aug 2026 08:23:48 +0100 Subject: [PATCH 08/12] fix: only write AI model prices that changed (#27923) (#28105) Backport of https://github.com/coder/coder/pull/27923 Original PR: #27923 - only write AI model prices that changed Merge commit: 5efa7abe7d8f0cbf0c853a234c972357fce1a752 Requested by: @ssncferreira --- coderd/aibridge/prices/prices_test.go | 107 +++++++++++++++++++++- coderd/database/querier.go | 3 + coderd/database/queries.sql.go | 14 +++ coderd/database/queries/aicostcontrol.sql | 16 +++- 4 files changed, 137 insertions(+), 3 deletions(-) diff --git a/coderd/aibridge/prices/prices_test.go b/coderd/aibridge/prices/prices_test.go index 1ce642e20840d..ae30b7e449e84 100644 --- a/coderd/aibridge/prices/prices_test.go +++ b/coderd/aibridge/prices/prices_test.go @@ -1,6 +1,7 @@ package prices_test import ( + "fmt" "testing" "github.com/prometheus/client_golang/prometheus" @@ -58,6 +59,7 @@ func TestSeedFromBytes(t *testing.T) { require.Equal(t, int64(25_000_000), opus.OutputPrice.Int64) require.Equal(t, int64(500_000), opus.CacheReadPrice.Int64) require.Equal(t, int64(6_250_000), opus.CacheWritePrice.Int64) + require.Equal(t, opus.CreatedAt, opus.UpdatedAt) // Spot-check a row where the seed has a NULL price (OpenAI does not // publish a cache_write_price). The column should land as SQL NULL. @@ -90,11 +92,11 @@ func TestSeedFromBytes(t *testing.T) { }) require.NoError(t, err) - // Prices must be identical across runs and CreatedAt must be - // preserved (only updated_at moves on a no-op upsert). + // A re-seed that changes nothing must not touch the row at all. require.Equal(t, first.InputPrice, second.InputPrice) require.Equal(t, first.OutputPrice, second.OutputPrice) require.Equal(t, first.CreatedAt, second.CreatedAt) + require.Equal(t, first.UpdatedAt, second.UpdatedAt) }) t.Run("OverwritesExistingPrices", func(t *testing.T) { @@ -114,6 +116,10 @@ func TestSeedFromBytes(t *testing.T) { "cache_read_price": 3, "cache_write_price": 4 }]`))) + before, err := db.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{ + Provider: "openai", Model: "gpt-4o", + }) + require.NoError(t, err) require.NoError(t, prices.SeedFromBytes(ctx, db, []byte(testSeedJSON))) @@ -126,6 +132,8 @@ func TestSeedFromBytes(t *testing.T) { require.Equal(t, int64(1_250_000), got.CacheReadPrice.Int64) require.False(t, got.CacheWritePrice.Valid) require.Zero(t, got.CacheWritePrice.Int64) + require.Equal(t, before.CreatedAt, got.CreatedAt) + require.True(t, got.UpdatedAt.After(before.UpdatedAt)) }) t.Run("LeavesOrphanRowsUntouched", func(t *testing.T) { @@ -174,6 +182,101 @@ func TestSeedFromBytes(t *testing.T) { require.True(t, got.InputPrice.Valid) require.Equal(t, int64(2_500_000), got.InputPrice.Int64) }) + + // Every price column counts toward the comparison, and a NULL on either + // side counts as a difference. + t.Run("UpdatedAtTracksPriceChanges", func(t *testing.T) { + t.Parallel() + + key := database.GetAIModelPriceByProviderModelParams{Provider: "openai", Model: "gpt-4o"} + seed := func(priceFields string) []byte { + return fmt.Appendf(nil, `[{"provider": %q, "model": %q, %s}]`, key.Provider, key.Model, priceFields) + } + + tests := []struct { + name string + initial, updated string + }{ + { + name: "InputPriceChanged", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 111, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "InputPriceSetFromNull", + initial: `"input_price": null, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "InputPriceClearedToNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": null, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "OutputPriceChanged", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 222, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "OutputPriceSetFromNull", + initial: `"input_price": 100, "output_price": null, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "OutputPriceClearedToNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": null, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "CacheReadPriceChanged", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 333, "cache_write_price": 400`, + }, + { + name: "CacheReadPriceSetFromNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": null, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "CacheReadPriceClearedToNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": null, "cache_write_price": 400`, + }, + { + name: "CacheWritePriceChanged", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 444`, + }, + { + name: "CacheWritePriceSetFromNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": null`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + }, + { + name: "CacheWritePriceClearedToNull", + initial: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": 400`, + updated: `"input_price": 100, "output_price": 200, "cache_read_price": 300, "cache_write_price": null`, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + ctx := testutil.Context(t, testutil.WaitShort) + db, _ := dbtestutil.NewDB(t) + + require.NoError(t, prices.SeedFromBytes(ctx, db, seed(tt.initial))) + before, err := db.GetAIModelPriceByProviderModel(ctx, key) + require.NoError(t, err) + + require.NoError(t, prices.SeedFromBytes(ctx, db, seed(tt.updated))) + after, err := db.GetAIModelPriceByProviderModel(ctx, key) + require.NoError(t, err) + + require.True(t, after.UpdatedAt.After(before.UpdatedAt), "updated_at should advance when a price changes") + }) + } + }) } // TestSeed exercises the real embedded prices.json so we catch a corrupted, diff --git a/coderd/database/querier.go b/coderd/database/querier.go index bcb6b722fd2ea..527168ba4a4f4 100644 --- a/coderd/database/querier.go +++ b/coderd/database/querier.go @@ -1635,6 +1635,9 @@ type sqlcQuerier interface { // Upsert a batch of (provider, model) rows from a JSON array. Each element // must have provider, model, and the four price fields; null prices are // written as SQL NULL. + // A conflicting row is only rewritten when a price differs, so updated_at + // records when a price last changed. Prices are nullable and a NULL on + // either side counts as a difference. UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error // Returns true if a new rows was inserted, false otherwise. UpsertAISeatState(ctx context.Context, arg UpsertAISeatStateParams) (bool, error) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 72f3812d4a317..07ccd2b813a9a 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -3406,11 +3406,25 @@ ON CONFLICT (provider, model) DO UPDATE SET cache_read_price = EXCLUDED.cache_read_price, cache_write_price = EXCLUDED.cache_write_price, updated_at = NOW() +WHERE ( + ai_model_prices.input_price, + ai_model_prices.output_price, + ai_model_prices.cache_read_price, + ai_model_prices.cache_write_price +) IS DISTINCT FROM ( + EXCLUDED.input_price, + EXCLUDED.output_price, + EXCLUDED.cache_read_price, + EXCLUDED.cache_write_price +) ` // Upsert a batch of (provider, model) rows from a JSON array. Each element // must have provider, model, and the four price fields; null prices are // written as SQL NULL. +// A conflicting row is only rewritten when a price differs, so updated_at +// records when a price last changed. Prices are nullable and a NULL on +// either side counts as a difference. func (q *sqlQuerier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error { _, err := q.db.ExecContext(ctx, upsertAIModelPrices, seed) return err diff --git a/coderd/database/queries/aicostcontrol.sql b/coderd/database/queries/aicostcontrol.sql index 43c5970fe2dca..5552e8c7af4a6 100644 --- a/coderd/database/queries/aicostcontrol.sql +++ b/coderd/database/queries/aicostcontrol.sql @@ -2,6 +2,9 @@ -- Upsert a batch of (provider, model) rows from a JSON array. Each element -- must have provider, model, and the four price fields; null prices are -- written as SQL NULL. +-- A conflicting row is only rewritten when a price differs, so updated_at +-- records when a price last changed. Prices are nullable and a NULL on +-- either side counts as a difference. INSERT INTO ai_model_prices ( provider, model, input_price, output_price, cache_read_price, cache_write_price ) @@ -18,7 +21,18 @@ ON CONFLICT (provider, model) DO UPDATE SET output_price = EXCLUDED.output_price, cache_read_price = EXCLUDED.cache_read_price, cache_write_price = EXCLUDED.cache_write_price, - updated_at = NOW(); + updated_at = NOW() +WHERE ( + ai_model_prices.input_price, + ai_model_prices.output_price, + ai_model_prices.cache_read_price, + ai_model_prices.cache_write_price +) IS DISTINCT FROM ( + EXCLUDED.input_price, + EXCLUDED.output_price, + EXCLUDED.cache_read_price, + EXCLUDED.cache_write_price +); -- name: GetAIModelPriceByProviderModel :one SELECT * From 83f642383e43bfa61190575fc7d22c0a7c0be169 Mon Sep 17 00:00:00 2001 From: Susana Ferreira Date: Mon, 17 Aug 2026 14:39:27 +0100 Subject: [PATCH 09/12] fix: label unpriced token usage metric by provider name and type (#28210) (#28212) Backport of https://github.com/coder/coder/pull/28210 Original PR: #28210 - fix: label unpriced token usage metric by provider name and type Merge commit: 95328f1ead6bdf275664678a92033a771c8a8db1 Requested by: @ssncferreira --- coderd/aibridgedserver/aibridgedserver_test.go | 8 ++++---- coderd/aibridgedserver/cost.go | 10 +++++++--- coderd/aibridgedserver/metrics.go | 5 +++-- docs/admin/integrations/prometheus.md | 2 +- docs/ai-coder/ai-gateway/cost-controls.md | 7 ++++--- docs/ai-coder/ai-gateway/monitoring.md | 12 ++++++------ scripts/metricsdocgen/metrics | 4 ++-- 7 files changed, 27 insertions(+), 21 deletions(-) diff --git a/coderd/aibridgedserver/aibridgedserver_test.go b/coderd/aibridgedserver/aibridgedserver_test.go index 8fa4776fb92b7..9b63d66b64d31 100644 --- a/coderd/aibridgedserver/aibridgedserver_test.go +++ b/coderd/aibridgedserver/aibridgedserver_test.go @@ -1762,7 +1762,7 @@ func TestRecordTokenUsage(t *testing.T) { // A priced model does not increment unpriced_token_usage_records_total. assertMetrics: func(t *testing.T, reg *prometheus.Registry) { require.Nil(t, promhelp.MetricValue(t, reg, "cost_control_unpriced_token_usage_records_total", - prometheus.Labels{"provider": "anthropic", "model": "claude-sonnet-4-6"})) + prometheus.Labels{"provider": "anthropic-eu", "provider_type": "anthropic", "model": "claude-sonnet-4-6"})) }, }, { @@ -1909,7 +1909,7 @@ func TestRecordTokenUsage(t *testing.T) { // A missing price row increments unpriced_token_usage_records_total. assertMetrics: func(t *testing.T, reg *prometheus.Registry) { require.Equal(t, 1, promhelp.CounterValue(t, reg, "cost_control_unpriced_token_usage_records_total", - prometheus.Labels{"provider": "anthropic", "model": "claude-sonnet-4-6"})) + prometheus.Labels{"provider": "anthropic-eu", "provider_type": "anthropic", "model": "claude-sonnet-4-6"})) }, }, { @@ -2089,7 +2089,7 @@ func TestRecordTokenUsage(t *testing.T) { // A missing price row increments unpriced_token_usage_records_total. assertMetrics: func(t *testing.T, reg *prometheus.Registry) { require.Equal(t, 1, promhelp.CounterValue(t, reg, "cost_control_unpriced_token_usage_records_total", - prometheus.Labels{"provider": "anthropic", "model": "claude-sonnet-4-6"})) + prometheus.Labels{"provider": "anthropic-eu", "provider_type": "anthropic", "model": "claude-sonnet-4-6"})) }, }, { @@ -2256,7 +2256,7 @@ func TestRecordTokenUsage(t *testing.T) { // The metric names the provider that failed to resolve. assertMetrics: func(t *testing.T, reg *prometheus.Registry) { require.Equal(t, 1, promhelp.CounterValue(t, reg, "cost_control_unpriced_token_usage_records_total", - prometheus.Labels{"provider": "anthropic-eu", "model": "claude-sonnet-4-6"})) + prometheus.Labels{"provider": "anthropic-eu", "provider_type": "unknown", "model": "claude-sonnet-4-6"})) }, }, { diff --git a/coderd/aibridgedserver/cost.go b/coderd/aibridgedserver/cost.go index ba56412a4554e..0470e66b09a12 100644 --- a/coderd/aibridgedserver/cost.go +++ b/coderd/aibridgedserver/cost.go @@ -19,6 +19,10 @@ import ( // tokens. const tokensPerMillion = 1_000_000 +// unknownProviderType labels a metric whose provider did not resolve to a +// configured type. +const unknownProviderType = "unknown" + // tokenUsageCost holds the cost-attribution columns snapshotted onto a token // usage record. A field left unset (Valid == false) is recorded as SQL NULL; a // price or cost of 0 is recorded as 0, which is distinct from NULL. @@ -71,10 +75,10 @@ func (s *Server) resolveTokenUsageCost(ctx context.Context, intc database.AIBrid switch { case errors.Is(err, sql.ErrNoRows): // Only reachable if the provider was deleted mid-request. - s.logger.Info(ctx, "no configured provider found for interception, recording token usage with NULL cost", + s.logger.Warn(ctx, "no configured provider found for interception, recording token usage with NULL cost", slog.F("provider_name", intc.ProviderName), slog.F("model", intc.Model)) if s.metrics != nil { - s.metrics.UnpricedTokenUsageRecords.WithLabelValues(intc.ProviderName, intc.Model).Inc() + s.metrics.UnpricedTokenUsageRecords.WithLabelValues(intc.ProviderName, unknownProviderType, intc.Model).Inc() } return result, nil case err != nil: @@ -93,7 +97,7 @@ func (s *Server) resolveTokenUsageCost(ctx context.Context, intc database.AIBrid s.logger.Info(ctx, "no price found for model, recording token usage with NULL cost", slog.F("provider", configuredType), slog.F("model", intc.Model)) if s.metrics != nil { - s.metrics.UnpricedTokenUsageRecords.WithLabelValues(configuredType, intc.Model).Inc() + s.metrics.UnpricedTokenUsageRecords.WithLabelValues(intc.ProviderName, configuredType, intc.Model).Inc() } return result, nil case err != nil: diff --git a/coderd/aibridgedserver/metrics.go b/coderd/aibridgedserver/metrics.go index b88b8789a6f6b..c613b2f2f3e91 100644 --- a/coderd/aibridgedserver/metrics.go +++ b/coderd/aibridgedserver/metrics.go @@ -54,8 +54,9 @@ func NewMetrics(reg prometheus.Registerer) *Metrics { UnpricedTokenUsageRecords: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ Subsystem: "cost_control", Name: "unpriced_token_usage_records_total", - Help: "The number of recorded AI token-usage records for which no (provider, model) price was found.", - }, []string{"provider", "model"}), + Help: "The number of recorded AI token-usage records for which no (provider_type, model) price was found. " + + "provider is the provider instance name, and provider_type is its configured type.", + }, []string{"provider", "provider_type", "model"}), // Pessimistic cardinality: 3 outcomes, 8 buckets + 3 extra series // (count, sum, +Inf) = up to 33. EnforcementDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ diff --git a/docs/admin/integrations/prometheus.md b/docs/admin/integrations/prometheus.md index 408e87882b257..b67cfb7e5adc0 100644 --- a/docs/admin/integrations/prometheus.md +++ b/docs/admin/integrations/prometheus.md @@ -125,7 +125,7 @@ The `coder_ai_gateway_cost_control_*` metrics are exported only by `coderd`. | `coder_ai_gateway_cost_control_blocked_requests_total` | counter | The number of AI requests blocked because the initiator's budget was exceeded. | `group_id` | | `coder_ai_gateway_cost_control_blocked_users` | gauge | The number of users currently over their AI budget. | `group_id` | | `coder_ai_gateway_cost_control_enforcement_duration_seconds` | histogram | The duration of AI budget enforcement checks, in seconds (outcome: allowed, blocked, error). | `outcome` | -| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | The number of recorded AI token-usage records for which no (provider, model) price was found. | `model` `provider` | +| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | The number of recorded AI token-usage records for which no (provider_type, model) price was found. provider is the provider instance name, and provider_type is its configured type. | `model` `provider_type` `provider` | | `coder_ai_gateway_injected_tool_invocations_total` | counter | The number of times an injected MCP tool was invoked by AI Gateway. | `model` `name` `provider` `server` | | `coder_ai_gateway_interceptions_duration_seconds` | histogram | The total duration of intercepted requests, in seconds. The majority of this time will be the upstream processing of the request. AI Gateway has no control over upstream processing time, so it's just an illustrative metric. | `model` `provider` | | `coder_ai_gateway_interceptions_inflight` | gauge | The number of intercepted requests which are being processed. | `model` `provider` `route` | diff --git a/docs/ai-coder/ai-gateway/cost-controls.md b/docs/ai-coder/ai-gateway/cost-controls.md index 6b8b81be11652..0b563cc3b26b8 100644 --- a/docs/ai-coder/ai-gateway/cost-controls.md +++ b/docs/ai-coder/ai-gateway/cost-controls.md @@ -210,9 +210,10 @@ Replace `` with your Coder minor version, for example `2.36`. > effectively unlimited. Monitor `coder_ai_gateway_cost_control_unpriced_token_usage_records_total`, -labeled by `provider` and `model`, to detect unpriced usage. Any non-zero value -means spend is under-counted. Because the price book ships with the release, a -newly launched model can remain unpriced until you upgrade Coder. +labeled by `provider`, `provider_type`, and `model`, to detect unpriced usage. +The `(provider_type, model)` tuple identifies the missing price. Any non-zero +value means spend is under-counted. Because the price book ships with the +release, a newly launched model can remain unpriced until you upgrade Coder. ## Monitor spend diff --git a/docs/ai-coder/ai-gateway/monitoring.md b/docs/ai-coder/ai-gateway/monitoring.md index df648ef8ec9f8..b56a6d8fbec40 100644 --- a/docs/ai-coder/ai-gateway/monitoring.md +++ b/docs/ai-coder/ai-gateway/monitoring.md @@ -55,12 +55,12 @@ Budget enforcement runs in `coderd`. Cost control metrics are exported only from the `coderd` Prometheus listener. Standalone replicas do not export them. -| Metric | Type | Labels | Purpose | -|--------------------------------------------------------------------|-----------|---------------------|------------------------------------------------------------------------------------------| -| `coder_ai_gateway_cost_control_blocked_requests_total` | counter | `group_id` | AI requests blocked because the initiator's budget was exceeded. | -| `coder_ai_gateway_cost_control_blocked_users` | gauge | `group_id` | Users currently over their AI budget. | -| `coder_ai_gateway_cost_control_enforcement_duration_seconds` | histogram | `outcome` | Duration of AI budget enforcement checks. `outcome` is `allowed`, `blocked`, or `error`. | -| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | `model`, `provider` | Recorded token-usage records for which no model price was found. | +| Metric | Type | Labels | Purpose | +|--------------------------------------------------------------------|-----------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `coder_ai_gateway_cost_control_blocked_requests_total` | counter | `group_id` | AI requests blocked because the initiator's budget was exceeded. | +| `coder_ai_gateway_cost_control_blocked_users` | gauge | `group_id` | Users currently over their AI budget. | +| `coder_ai_gateway_cost_control_enforcement_duration_seconds` | histogram | `outcome` | Duration of AI budget enforcement checks. `outcome` is `allowed`, `blocked`, or `error`. | +| `coder_ai_gateway_cost_control_unpriced_token_usage_records_total` | counter | `model`, `provider`, `provider_type` | Recorded token-usage records for which no model price was found. `provider` is the provider instance name, and `provider_type` is the configured type the price is keyed on, or `unknown` when the provider could not be resolved. | ### AI Gateway Proxy metrics diff --git a/scripts/metricsdocgen/metrics b/scripts/metricsdocgen/metrics index 755068744c8ff..ccce769144d45 100644 --- a/scripts/metricsdocgen/metrics +++ b/scripts/metricsdocgen/metrics @@ -166,9 +166,9 @@ coder_ai_gateway_cost_control_enforcement_duration_seconds_bucket{outcome="allow coder_ai_gateway_cost_control_enforcement_duration_seconds_bucket{outcome="allowed",le="+Inf"} 0 coder_ai_gateway_cost_control_enforcement_duration_seconds_sum{outcome="allowed"} 0 coder_ai_gateway_cost_control_enforcement_duration_seconds_count{outcome="allowed"} 0 -# HELP coder_ai_gateway_cost_control_unpriced_token_usage_records_total The number of recorded AI token-usage records for which no (provider, model) price was found. +# HELP coder_ai_gateway_cost_control_unpriced_token_usage_records_total The number of recorded AI token-usage records for which no (provider_type, model) price was found. provider is the provider instance name, and provider_type is its configured type. # TYPE coder_ai_gateway_cost_control_unpriced_token_usage_records_total counter -coder_ai_gateway_cost_control_unpriced_token_usage_records_total{model="gpt-5-nano",provider="openai"} 0 +coder_ai_gateway_cost_control_unpriced_token_usage_records_total{model="gpt-5-nano",provider="openai",provider_type="openai"} 0 # HELP coder_ai_gateway_injected_tool_invocations_total The number of times an injected MCP tool was invoked by AI Gateway. # TYPE coder_ai_gateway_injected_tool_invocations_total counter coder_ai_gateway_injected_tool_invocations_total{model="gpt-5-nano",name="coder_list_templates",provider="openai",server="https://xxx.pit-1.try.coder.app/api/experimental/mcp/http"} 1 From 10e6321619114db490b380a703514cab62f38246 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:35:54 +0200 Subject: [PATCH 10/12] fix(site): include owner context in Agents org picker permission check (#28076) (#28132) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28076 Original PR: #28076 — fix(site): include owner context in Agents org picker permission check Merge commit: d509e1e6a03ff7d88996cb637acc0ef5c131e950 Requested by: @ibetitsmike Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> --- site/src/api/queries/organizations.ts | 8 ++- .../components/AgentCreateForm.stories.tsx | 57 +++++++++++++++++-- .../AgentsPage/components/AgentCreateForm.tsx | 2 +- 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/site/src/api/queries/organizations.ts b/site/src/api/queries/organizations.ts index 1dcaac36596f6..6caf0d6f958bf 100644 --- a/site/src/api/queries/organizations.ts +++ b/site/src/api/queries/organizations.ts @@ -294,6 +294,12 @@ export const provisionerJobs = ( }; }; +export const permittedOrganizationsKey = (check: AuthorizationCheck) => [ + "organizations", + "permitted", + check, +]; + /** * Fetch organizations the current user is permitted to use for a given * action. Fetches all organizations, runs a per-org authorization @@ -301,7 +307,7 @@ export const provisionerJobs = ( */ export const permittedOrganizations = (check: AuthorizationCheck) => { return { - queryKey: ["organizations", "permitted", check], + queryKey: permittedOrganizationsKey(check), queryFn: async (): Promise => { const orgs = await API.getOrganizations(); const checks = Object.fromEntries( diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index e38c344494d62..46fb4784803af 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -9,6 +9,7 @@ import { within, } from "storybook/test"; import { API } from "#/api/api"; +import { permittedOrganizationsKey } from "#/api/queries/organizations"; import type * as TypesGen from "#/api/typesGenerated"; import { ConfirmDialog } from "#/components/Dialogs/ConfirmDialog/ConfirmDialog"; import { MockChatModelConfig } from "#/testHelpers/chatModels"; @@ -24,12 +25,10 @@ import { } from "../utils/reasoningEffort"; import { AgentCreateForm } from "./AgentCreateForm"; -// Query key used by permittedOrganizations() in the form. -const permittedOrgsKey = [ - "organizations", - "permitted", - { object: { resource_type: "chat" }, action: "create" }, -]; +const permittedOrgsKey = permittedOrganizationsKey({ + object: { resource_type: "chat", owner_id: "me" }, + action: "create", +}); const modelConfigID = "model-config-1"; const claudeModelConfigID = "model-config-claude"; @@ -1061,3 +1060,49 @@ export const PermittedOrgsResolvesToSubset: Story = { expect(options.organizationId).toBe(MockOrganization2.id); }, }; + +/** + * Member-scoped roles like agents-access grant chat:create only on + * chats the user owns, so the per-org check must carry owner context + * for the picker to render. + */ +export const MemberScopedPermissionsShowOrgPicker: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => + Object.fromEntries( + Object.entries(checks).map(([id, check]) => [ + id, + check.object.owner_id === "me", + ]), + ), + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const picker = await canvas.findByRole( + "button", + { name: /^Organization:/ }, + { timeout: 3000 }, + ); + await userEvent.click(picker); + await screen.findByRole("option", { + name: MockDefaultOrganization.display_name, + }); + await userEvent.click( + screen.getByRole("option", { name: MockOrganization2.display_name }), + ); + expect( + canvas.getByRole("button", { + name: `Organization: ${MockOrganization2.display_name}`, + }), + ).toBeInTheDocument(); + }, +}; diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index 0a494c01dcf16..22165f04b54ee 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -461,7 +461,7 @@ export const AgentCreateForm: FC = ({ const permittedOrgsQuery = useQuery({ ...permittedOrganizations({ - object: { resource_type: "chat" }, + object: { resource_type: "chat", owner_id: "me" }, action: "create", }), enabled: showOrganizations, From 7fe2ec471353a64e3e4314253645169a1b1b445b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:43:09 +0200 Subject: [PATCH 11/12] fix(coderd): send assigned chat model IDs verbatim (#28144) (#28216) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28144 Original PR: #28144 — fix(coderd): send assigned chat model IDs verbatim Merge commit: 93c6faf1de78b9cbe930295d61abbffd8db6971c Requested by: @ibetitsmike Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Co-authored-by: Marcin Tojek --- coderd/exp_chats.go | 2 +- coderd/x/chatd/chatprovider/chatprovider.go | 30 ++------------- .../x/chatd/chatprovider/chatprovider_test.go | 38 ++++++++++++++++--- coderd/x/chatd/model_routing_aibridge.go | 5 +-- 4 files changed, 40 insertions(+), 35 deletions(-) diff --git a/coderd/exp_chats.go b/coderd/exp_chats.go index 35ca899bac4d4..1fd901255ccaa 100644 --- a/coderd/exp_chats.go +++ b/coderd/exp_chats.go @@ -7304,7 +7304,7 @@ func validateChatModelConfigProviderModel(aiProvider database.AIProvider, model return &chatModelConfigProviderModelError{ Response: codersdk.Response{ Message: "OpenRouter-like provider configured as type openai does not support slash-namespaced models.", - Detail: "Change the AI provider type to openrouter or openai-compat. The openai type strips the vendor prefix from slash-namespaced model IDs, routing to the wrong upstream provider.", + Detail: "Change the AI provider type to openrouter or openai-compat. Slash-namespaced model IDs on OpenRouter-like gateways require one of those provider types.", }, } } diff --git a/coderd/x/chatd/chatprovider/chatprovider.go b/coderd/x/chatd/chatprovider/chatprovider.go index ad450d6a5429c..5edae77da573c 100644 --- a/coderd/x/chatd/chatprovider/chatprovider.go +++ b/coderd/x/chatd/chatprovider/chatprovider.go @@ -687,21 +687,6 @@ func orderProviders(providerSet map[string]struct{}) []string { return ordered } -// isGatewayProvider reports whether the provider routes requests to -// multiple upstream model providers using a "/" model -// identifier, where the slash is part of the upstream model ID rather -// than a hint. -func isGatewayProvider(provider string) bool { - switch provider { - case fantasyvercel.Name, - fantasyopenrouter.Name, - fantasyopenaicompat.Name: - return true - default: - return false - } -} - // NormalizeProvider canonicalizes a provider name. func NormalizeProvider(provider string) string { switch strings.ToLower(strings.TrimSpace(provider)) { @@ -732,23 +717,16 @@ func ResolveModelWithProviderHint(modelName, providerHint string) (provider stri return "", "", xerrors.New("model is required") } - // Gateway providers (vercel, openrouter, openai-compat) treat the - // "/" slash as part of the upstream model ID, so - // parseCanonicalModelRef would incorrectly strip the prefix and - // route to the embedded provider name instead. Honor an explicit - // gateway hint before attempting canonical-ref parsing. - if normalized := NormalizeProvider(providerHint); normalized != "" && isGatewayProvider(normalized) { - return normalized, modelName, nil + // A valid provider hint is authoritative, so preserve the model ID + // instead of interpreting its namespace as a different provider. + if provider := NormalizeProvider(providerHint); provider != "" { + return provider, modelName, nil } if provider, modelID, ok := parseCanonicalModelRef(modelName); ok { return provider, modelID, nil } - if provider := NormalizeProvider(providerHint); provider != "" { - return provider, modelName, nil - } - normalized := strings.ToLower(modelName) switch normalized { case "claude-opus-4-6": diff --git a/coderd/x/chatd/chatprovider/chatprovider_test.go b/coderd/x/chatd/chatprovider/chatprovider_test.go index b1b2a37a885f2..4245f139b01b9 100644 --- a/coderd/x/chatd/chatprovider/chatprovider_test.go +++ b/coderd/x/chatd/chatprovider/chatprovider_test.go @@ -1719,11 +1719,11 @@ func TestResolveModelWithProviderHint(t *testing.T) { wantModel: "anthropic/claude-opus-4.6", }, { - name: "OpenAIHintStripsCanonicalPrefix", + name: "OpenAIHintPreservesCanonicalPrefix", modelName: "anthropic/claude-opus-4.6", providerHint: fantasyopenai.Name, - wantProvider: fantasyanthropic.Name, - wantModel: "claude-opus-4.6", + wantProvider: fantasyopenai.Name, + wantModel: "anthropic/claude-opus-4.6", }, { name: "OpenAIHintPreservesUnknownSlashNamespace", @@ -1733,11 +1733,39 @@ func TestResolveModelWithProviderHint(t *testing.T) { wantModel: "meta-llama/llama-3-70b", }, { - name: "AnthropicHintStripsCanonicalPrefix", + name: "AnthropicHintPreservesCanonicalPrefix", modelName: "anthropic/claude-4-5-sonnet", providerHint: fantasyanthropic.Name, wantProvider: fantasyanthropic.Name, - wantModel: "claude-4-5-sonnet", + wantModel: "anthropic/claude-4-5-sonnet", + }, + { + name: "AnthropicHintPreservesBedrockNamespace", + modelName: "bedrock/claude-opus-4-8", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "bedrock/claude-opus-4-8", + }, + { + name: "AnthropicHintPreservesDoubleBedrockNamespace", + modelName: "bedrock/bedrock/claude-opus-4-8", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "bedrock/bedrock/claude-opus-4-8", + }, + { + name: "AnthropicHintPreservesColonCanonicalRef", + modelName: "openai:gpt-5.2", + providerHint: fantasyanthropic.Name, + wantProvider: fantasyanthropic.Name, + wantModel: "openai:gpt-5.2", + }, + { + name: "BedrockHintWithBareModel", + modelName: "claude-opus-4-6", + providerHint: fantasybedrock.Name, + wantProvider: fantasybedrock.Name, + wantModel: "claude-opus-4-6", }, { name: "NoHintUsesCanonicalRef", diff --git a/coderd/x/chatd/model_routing_aibridge.go b/coderd/x/chatd/model_routing_aibridge.go index 2bd724869ae99..a156481a00225 100644 --- a/coderd/x/chatd/model_routing_aibridge.go +++ b/coderd/x/chatd/model_routing_aibridge.go @@ -85,9 +85,8 @@ func (t *aiGatewayRoundTripper) RoundTrip(req *http.Request) (*http.Response, er return t.base.RoundTrip(cloned) } -// ValidateAIGatewayProviderModel rejects slash-namespaced models on -// OpenRouter-like providers typed as openai, where the provider type -// strips the vendor prefix. +// ValidateAIGatewayProviderModel rejects slash-namespaced models when an +// OpenRouter-like gateway is configured with the OpenAI provider type. func ValidateAIGatewayProviderModel(provider database.AIProvider, model string) error { if provider.Type != database.AIProviderTypeOpenai { return nil From 9548f239a8bdfe74d0f62eafc41c97b2d7adca91 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 17 Aug 2026 21:46:36 +0200 Subject: [PATCH 12/12] fix(site): prefer permitted organization for chat creation (#28078) (#28133) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backport of https://github.com/coder/coder/pull/28078 Original PR: #28078 — fix(site): prefer permitted organization for chat creation Merge commit: bca5d72c1c58b0efcf131c401db9481555d1645b Requested by: @ibetitsmike The original automatic cherry-pick had conflicts; `release/2.36` has since moved and the merge commit was re-cherry-picked cleanly (`git cherry-pick -x -m1 bca5d72c`) and pushed by Coder Agents on behalf of @ibetitsmike. Co-authored-by: Michael Suchacz <203725896+ibetitsmike@users.noreply.github.com> Co-authored-by: Marcin Tojek --- .../components/AgentCreateForm.stories.tsx | 562 ++++++++++++++++-- .../AgentsPage/components/AgentCreateForm.tsx | 250 ++++---- .../hooks/useFileAttachments.test.tsx | 216 +++++++ .../AgentsPage/hooks/useFileAttachments.ts | 118 ++-- 4 files changed, 962 insertions(+), 184 deletions(-) create mode 100644 site/src/pages/AgentsPage/hooks/useFileAttachments.test.tsx diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx index 46fb4784803af..cbbe9acd016d1 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.stories.tsx @@ -1,4 +1,7 @@ -import type { Meta, StoryObj } from "@storybook/react-vite"; +import type { Decorator, Meta, StoryObj } from "@storybook/react-vite"; +import { delay } from "msw"; +import { useState } from "react"; +import { QueryClient, QueryClientProvider } from "react-query"; import { expect, fn, @@ -19,11 +22,12 @@ import { MockWorkspace, } from "#/testHelpers/entities"; import { withDashboardProvider } from "#/testHelpers/storybook"; +import { persistedAttachmentsStorageKey } from "../hooks/useFileAttachments"; import { getReasoningEffortForModel, saveReasoningEffortForModel, } from "../utils/reasoningEffort"; -import { AgentCreateForm } from "./AgentCreateForm"; +import { AgentCreateForm, emptyInputStorageKey } from "./AgentCreateForm"; const permittedOrgsKey = permittedOrganizationsKey({ object: { resource_type: "chat", owner_id: "me" }, @@ -131,19 +135,27 @@ type Story = StoryObj; const defaultArgs = meta.args; -const mockPermittedOrganizations = (permissions: Record) => { +const mockPermittedOrganizations = ( + permissions: Record, + delayMs = 0, +) => { spyOn(API, "getOrganizations").mockResolvedValue([ MockDefaultOrganization, MockOrganization2, ]); - spyOn(API, "checkAuthorization").mockResolvedValue(permissions); + spyOn(API, "checkAuthorization").mockImplementation(async () => { + if (delayMs > 0) { + await delay(delayMs); + } + return permissions; + }); }; export const Default: Story = {}; const submitMessage = async (canvasElement: HTMLElement, message: string) => { const canvas = within(canvasElement); - const input = canvas.getByTestId("chat-message-input"); + const input = canvas.getByRole("textbox", { name: "Chat message" }); await userEvent.click(input); await userEvent.keyboard(message); await userEvent.click(canvas.getByRole("button", { name: "Send" })); @@ -852,24 +864,496 @@ export const WithOrganizationPicker: Story = { queries: [ { key: permittedOrgsKey, - data: [MockDefaultOrganization, MockOrganization2], + data: [MockOrganization2, MockDefaultOrganization], }, ], }, play: async ({ canvasElement }) => { const canvas = within(canvasElement); - // Verify the org picker rendered (component didn't crash). - await waitFor(() => { - expect(canvas.getByTestId("compact-org-selector")).toBeInTheDocument(); + const organizationPicker = canvas.getByRole("button", { + name: "Organization: My Organization", }); - // Type into the chat input to trigger re-renders. If the - // permittedOrgs fallback is referentially unstable, this - // causes a render cascade that hits React's update limit. - const input = canvas.getByTestId("chat-message-input"); + await expect(organizationPicker).toBeVisible(); + + const input = canvas.getByRole("textbox", { name: "Chat message" }); await userEvent.click(input); await userEvent.keyboard("hello world"); - // The org picker should still be present after typing. - expect(canvas.getByTestId("compact-org-selector")).toBeInTheDocument(); + await expect( + canvas.getByRole("button", { + name: "Organization: My Organization", + }), + ).toBeVisible(); + }, +}; + +export const RestrictedMultiOrganizationUser: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + // Model agents-access: "me" supplies the owner for member-scoped chat:create. + spyOn(API, "checkAuthorization").mockImplementation(async ({ checks }) => + Object.fromEntries( + Object.entries(checks).map(([id, check]) => [ + id, + check.object.owner_id === "me" && + check.object.organization_id === MockOrganization2.id, + ]), + ), + ); + }, + args: { + ...defaultArgs, + onCreateChat: fn().mockResolvedValue(undefined), + }, + play: async ({ canvasElement, args }) => { + await submitMessage(canvasElement, "test message"); + await waitFor(() => { + expect(args.onCreateChat).toHaveBeenCalledWith( + expect.objectContaining({ + organizationId: MockOrganization2.id, + }), + ); + }); + }, +}; + +export const RestrictedUserKeepsPersistedWorkspace: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + args: { + ...defaultArgs, + onCreateChat: fn().mockResolvedValue(undefined), + workspaceOptions: [ + { + ...MockWorkspace, + id: "ws-permitted-org", + name: "permitted-workspace", + organization_id: MockOrganization2.id, + }, + ], + workspaceCount: 1, + }, + beforeEach: () => { + localStorage.setItem("agents.selected-workspace-id", "ws-permitted-org"); + mockPermittedOrganizations({ + [MockDefaultOrganization.id]: false, + [MockOrganization2.id]: true, + }); + }, + play: async ({ canvasElement, args }) => { + await submitMessage(canvasElement, "test message"); + await waitFor(() => { + expect(args.onCreateChat).toHaveBeenCalledWith( + expect.objectContaining({ + organizationId: MockOrganization2.id, + workspaceId: "ws-permitted-org", + }), + ); + }); + }, +}; + +export const RestrictedUserKeepsPersistedAttachments: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + localStorage.clear(); + localStorage.setItem( + "agents.persisted-attachments", + JSON.stringify([ + { + fileId: "file-permitted-org", + fileName: "notes.txt", + fileType: "text/plain", + lastModified: 1700000000000, + organizationId: MockOrganization2.id, + }, + ]), + ); + mockPermittedOrganizations({ + [MockDefaultOrganization.id]: false, + [MockOrganization2.id]: true, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => { + expect(canvas.getByLabelText("Remove notes.txt")).toBeInTheDocument(); + }); + const stored = localStorage.getItem("agents.persisted-attachments"); + expect(stored).toContain("file-permitted-org"); + }, +}; + +export const OrganizationAuthorizationFailure: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + localStorage.clear(); + localStorage.setItem(emptyInputStorageKey, "draft message"); + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + spyOn(API, "checkAuthorization").mockRejectedValue( + new Error("authorization check failed"), + ); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findAllByText(/authorization check failed/i); + expect(canvas.getByRole("button", { name: "Send" })).toBeDisabled(); + }, +}; + +export const LoadingWorkspacesBlocksSendUntilValidated: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + args: { + ...defaultArgs, + workspaceOptions: [], + isWorkspacesLoading: true, + }, + beforeEach: () => { + localStorage.setItem(emptyInputStorageKey, "draft message"); + localStorage.setItem("agents.selected-workspace-id", "ws-default-org"); + mockPermittedOrganizations({ + [MockDefaultOrganization.id]: true, + [MockOrganization2.id]: true, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + // Wait for permissions to settle before checking workspace validation. + await canvas.findByRole("button", { + name: "Organization: My Organization", + }); + await expect(canvas.getByRole("button", { name: "Send" })).toBeDisabled(); + }, +}; + +export const DelayedOrganizationAuthorization: Story = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + beforeEach: () => { + localStorage.setItem(emptyInputStorageKey, "draft message"); + mockPermittedOrganizations( + { + [MockDefaultOrganization.id]: true, + [MockOrganization2.id]: true, + }, + 1_500, + ); + }, + args: { + ...defaultArgs, + workspaceOptions: [ + { + ...MockWorkspace, + id: "ws-provisional", + name: "provisional-workspace", + organization_id: MockDefaultOrganization.id, + }, + ], + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const sendButton = canvas.getByRole("button", { name: "Send" }); + await expect(sendButton).toBeDisabled(); + await expect( + canvas.getByRole("button", { name: "More options" }), + ).toBeDisabled(); + // dispatchEvent returns false when a handler accepted the drop + // via preventDefault, giving a race-free accepted/ignored signal. + const dropFile = (name: string): boolean => { + const dataTransfer = new DataTransfer(); + dataTransfer.items.add(new File(["hello"], name, { type: "text/plain" })); + return canvas.getByTestId("chat-composer").dispatchEvent( + new DragEvent("drop", { + bubbles: true, + cancelable: true, + dataTransfer, + }), + ); + }; + // Pending authorization leaves attachments without a valid org, so drops + // must be ignored. + expect(dropFile("drop.txt")).toBe(true); + expect(canvas.queryByLabelText("Remove drop.txt")).not.toBeInTheDocument(); + // The pending option list is unfiltered, so the picker must stay hidden. + expect( + canvas.queryByRole("button", { name: /organization/i }), + ).not.toBeInTheDocument(); + await waitFor(() => expect(sendButton).toBeEnabled(), { timeout: 3_000 }); + await canvas.findByRole("button", { name: /organization/i }); + // Positive control: once settled the same drop is accepted and + // attaches, so the pending-state assertions exercised a real path. + expect(dropFile("after.txt")).toBe(false); + await waitFor(() => + expect(canvas.getByLabelText("Remove after.txt")).toBeInTheDocument(), + ); + }, +}; + +// Mutable permissions let play functions change authorization across refetches. +// The story-local QueryClient exposes those refetches; the preview client's +// instance is inaccessible and uses infinite stale time. +const revocablePermissions: Record = {}; +let revocableQueryClient: QueryClient | undefined; + +const withRevocableQueryClient: Decorator = (Story) => { + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { + queries: { + staleTime: Number.POSITIVE_INFINITY, + retry: false, + }, + }, + }), + ); + revocableQueryClient = queryClient; + return ( + + + + ); +}; + +const mockRevocablePermissions = (permissions: Record) => { + for (const key of Object.keys(revocablePermissions)) { + delete revocablePermissions[key]; + } + Object.assign(revocablePermissions, permissions); + spyOn(API, "getOrganizations").mockResolvedValue([ + MockDefaultOrganization, + MockOrganization2, + ]); + spyOn(API, "checkAuthorization").mockImplementation(async () => ({ + ...revocablePermissions, + })); +}; + +const revocableStoryContext = { + parameters: { + showOrganizations: true, + organizations: [MockDefaultOrganization, MockOrganization2], + }, + decorators: [withRevocableQueryClient], +}; + +const allOrganizationsPermitted = { + [MockDefaultOrganization.id]: true, + [MockOrganization2.id]: true, +}; + +export const RevokedSelectionDoesNotResurrect: Story = { + ...revocableStoryContext, + beforeEach: () => { + mockRevocablePermissions(allOrganizationsPermitted); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const trigger = await canvas.findByRole("button", { + name: "Organization: My Organization", + }); + await userEvent.click(trigger); + await userEvent.click( + await screen.findByRole("option", { name: /My Organization 2/ }), + ); + await canvas.findByRole("button", { + name: "Organization: My Organization 2", + }); + + revocablePermissions[MockOrganization2.id] = false; + await revocableQueryClient?.invalidateQueries(); + await waitFor(() => + expect( + canvas.queryByRole("button", { name: /organization/i }), + ).not.toBeInTheDocument(), + ); + + revocablePermissions[MockOrganization2.id] = true; + await revocableQueryClient?.invalidateQueries(); + await canvas.findByRole("button", { + name: "Organization: My Organization", + }); + }, +}; + +export const RevokedOrgChangeClearsStoredWorkspace: Story = { + ...revocableStoryContext, + args: { + ...defaultArgs, + workspaceOptions: [ + { + ...MockWorkspace, + id: "ws-default-org", + name: "default-workspace", + organization_id: MockDefaultOrganization.id, + }, + ], + workspaceCount: 1, + }, + beforeEach: () => { + localStorage.setItem("agents.selected-workspace-id", "ws-default-org"); + mockRevocablePermissions(allOrganizationsPermitted); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await waitFor(() => + expect( + canvas.getByLabelText("Remove workspace default-workspace"), + ).toBeInTheDocument(), + ); + + revocablePermissions[MockDefaultOrganization.id] = false; + await revocableQueryClient?.invalidateQueries(); + await waitFor(() => + expect( + canvas.queryByLabelText("Remove workspace default-workspace"), + ).not.toBeInTheDocument(), + ); + + revocablePermissions[MockDefaultOrganization.id] = true; + await revocableQueryClient?.invalidateQueries(); + await canvas.findByRole("button", { + name: "Organization: My Organization 2", + }); + expect( + canvas.queryByLabelText("Remove workspace default-workspace"), + ).not.toBeInTheDocument(); + expect(localStorage.getItem("agents.selected-workspace-id")).toBeNull(); + }, +}; + +export const EmptyPermittedSetPreservesStoredWorkspace: Story = { + ...revocableStoryContext, + args: { + ...defaultArgs, + workspaceOptions: [ + { + ...MockWorkspace, + id: "ws-org-2", + name: "org2-workspace", + organization_id: MockOrganization2.id, + }, + ], + }, + beforeEach: () => { + localStorage.setItem("agents.selected-workspace-id", "ws-org-2"); + mockRevocablePermissions({ + [MockDefaultOrganization.id]: false, + [MockOrganization2.id]: true, + }); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByLabelText("Remove workspace org2-workspace"); + + revocablePermissions[MockOrganization2.id] = false; + await revocableQueryClient?.invalidateQueries(); + await canvas.findByText(/don't have permission/i); + + revocablePermissions[MockOrganization2.id] = true; + await revocableQueryClient?.invalidateQueries(); + await canvas.findByLabelText("Remove workspace org2-workspace"); + expect(localStorage.getItem("agents.selected-workspace-id")).toBe( + "ws-org-2", + ); + }, +}; + +export const SingleOrgIgnoresStalePermittedCache: Story = { + parameters: { + showOrganizations: false, + organizations: [MockDefaultOrganization], + queries: [ + { + key: permittedOrgsKey, + data: [MockOrganization2], + }, + ], + }, + args: { + ...defaultArgs, + onCreateChat: fn().mockResolvedValue(undefined), + }, + play: async ({ canvasElement, args }) => { + await submitMessage(canvasElement, "test message"); + await waitFor(() => { + expect(args.onCreateChat).toHaveBeenCalledWith( + expect.objectContaining({ + organizationId: MockDefaultOrganization.id, + }), + ); + }); + }, +}; + +export const RevokedPendingOrgClosesConfirmDialog: Story = { + ...revocableStoryContext, + beforeEach: () => { + localStorage.clear(); + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([ + { + fileId: "file-default-org", + fileName: "notes.txt", + fileType: "text/plain", + lastModified: 1700000000000, + organizationId: MockDefaultOrganization.id, + }, + ]), + ); + mockRevocablePermissions(allOrganizationsPermitted); + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const body = within(canvasElement.ownerDocument.body); + await waitFor(() => + expect(canvas.getByLabelText("Remove notes.txt")).toBeInTheDocument(), + ); + await userEvent.click( + await canvas.findByRole("button", { + name: "Organization: My Organization", + }), + ); + await userEvent.click( + await screen.findByRole("option", { name: /My Organization 2/ }), + ); + await body.findByText( + "Changing organization will remove your current attachments.", + ); + + revocablePermissions[MockOrganization2.id] = false; + await revocableQueryClient?.invalidateQueries(); + await waitFor(() => + expect( + body.queryByText( + "Changing organization will remove your current attachments.", + ), + ).not.toBeInTheDocument(), + ); + expect(canvas.getByLabelText("Remove notes.txt")).toBeInTheDocument(); }, }; @@ -965,14 +1449,27 @@ export const PermittedOrgsResolvesToEmpty: Story = { parameters: { showOrganizations: true, organizations: [MockDefaultOrganization, MockOrganization2], - // Deliberately do not pre-seed permittedOrgsKey. Let the - // mocked API calls drive the async permission resolution. }, args: { ...defaultArgs, onCreateChat: fn().mockResolvedValue(undefined), }, beforeEach: () => { + localStorage.clear(); + // Another org's persisted attachment must survive a visit while + // the user has no chat permission anywhere. + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([ + { + fileId: "file-other-org", + fileName: "keep.txt", + fileType: "text/plain", + lastModified: 1000, + organizationId: MockOrganization2.id, + }, + ]), + ); mockPermittedOrganizations({ [MockDefaultOrganization.id]: false, [MockOrganization2.id]: false, @@ -980,36 +1477,17 @@ export const PermittedOrgsResolvesToEmpty: Story = { }, play: async ({ canvasElement, args }) => { const canvas = within(canvasElement); - - // Wait for the permitted orgs query to resolve. The org picker - // should disappear since no org is permitted. await waitFor( () => { - expect( - canvas.queryByTestId("compact-org-selector"), - ).not.toBeInTheDocument(); + expect(canvas.getByText(/don't have permission/i)).toBeInTheDocument(); }, { timeout: 3000 }, ); - - // Type a message and submit the form. - const input = canvas.getByTestId("chat-message-input"); - await userEvent.click(input); - await userEvent.keyboard("test message"); - await userEvent.click(canvas.getByRole("button", { name: "Send" })); - - // Verify onCreateChat was called with a non-empty organizationId. - await waitFor(() => { - expect(args.onCreateChat).toHaveBeenCalled(); - }); - const options = (args.onCreateChat as ReturnType).mock - .calls[0]?.[0] as { organizationId: string } | undefined; - if (!options) { - throw new Error("Expected onCreateChat to receive options"); - } - expect(options.organizationId).not.toBe(""); - // It should fall back to the default org from the dashboard. - expect(options.organizationId).toBe(MockDefaultOrganization.id); + expect(canvas.getByRole("button", { name: "Send" })).toBeDisabled(); + expect(args.onCreateChat).not.toHaveBeenCalled(); + expect( + localStorage.getItem(persistedAttachmentsStorageKey) ?? "", + ).toContain("file-other-org"); }, }; diff --git a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx index 22165f04b54ee..b81dbe083ebd2 100644 --- a/site/src/pages/AgentsPage/components/AgentCreateForm.tsx +++ b/site/src/pages/AgentsPage/components/AgentCreateForm.tsx @@ -1,4 +1,4 @@ -import { type FC, useEffect, useEffectEvent, useRef, useState } from "react"; +import { type FC, useEffect, useRef, useState } from "react"; import { useQuery } from "react-query"; import { toast } from "sonner"; import { isApiError } from "#/api/errors"; @@ -271,42 +271,96 @@ export const AgentCreateForm: FC = ({ : undefined; const initialOrg = organizations.find((o) => o.is_default) ?? organizations[0]; + // effectiveWorkspaceId nulls a stored selection outside the effective org's + // filtered workspace list without deleting it. Preserve the stored value + // because the permitted-organizations query may resolve after mount and + // change the effective org. const [selectedWorkspaceId, setSelectedWorkspaceId] = useState( - () => { - const stored = localStorage.getItem(selectedWorkspaceIdStorageKey); - if (!stored) return null; - - // The stored value is kept optimistically until workspaces - // load. effectiveWorkspaceId (computed after render) drops - // it if it doesn't match the current org's workspaces. - if (workspaceOptions.length === 0) return stored; - - // Validate the stored workspace still exists and belongs - // to the initial org. Without this, a workspace from a - // previously selected org persists across sessions and - // gets submitted even though it's hidden from the picker. - const workspace = workspaceOptions.find((ws) => ws.id === stored); - if (!workspace) { - localStorage.removeItem(selectedWorkspaceIdStorageKey); - return null; - } - if ( - showOrganizations && - initialOrg && - workspace.organization_id !== initialOrg.id - ) { - localStorage.removeItem(selectedWorkspaceIdStorageKey); - return null; - } - return stored; - }, + () => localStorage.getItem(selectedWorkspaceIdStorageKey), ); const [selectedOrg, setSelectedOrg] = useState( - initialOrg ?? null, + null, ); const [pendingOrgChange, setPendingOrgChange] = useState(null); - const organizationId = selectedOrg?.id ?? ""; + const permittedOrgsQuery = useQuery({ + ...permittedOrganizations({ + // agents-access grants chat:create only at member scope. "me" is + // replaced with the caller ID so that permission can match. + object: { resource_type: "chat", owner_id: "me" }, + action: "create", + }), + enabled: showOrganizations, + }); + // Disabled queries retain cached data. When the dashboard hides organization + // selection, its organization list is authoritative so a removed org cannot + // remain selected for submission. + const permittedOrgs = showOrganizations + ? (permittedOrgsQuery.data ?? organizations) + : organizations; + // Treat the dashboard org as provisional until permissions resolve so + // sends and persisted attachments cannot use an unpermitted org. + const orgSelectionSettled = + !showOrganizations || permittedOrgsQuery.data !== undefined; + // Prevent effectiveOrg's dashboard fallback from bypassing an empty + // permitted set. + const noPermittedOrgs = + showOrganizations && permittedOrgsQuery.data?.length === 0; + const selectedOrgIsPermitted = + selectedOrg !== null && + permittedOrgs.some((org) => org.id === selectedOrg.id); + // Clear invalid selections during render so re-permission cannot silently + // restore them and switch attachment state. effectiveOrg already ignores + // the invalid selection in this render. + if (selectedOrg && orgSelectionSettled && !selectedOrgIsPermitted) { + setSelectedOrg(null); + } + // Same rule for a pending change awaiting confirmation: closing the + // dialog prevents confirming into an org that was just revoked. + if ( + pendingOrgChange && + orgSelectionSettled && + !permittedOrgs.some((org) => org.id === pendingOrgChange.id) + ) { + setPendingOrgChange(null); + } + const effectiveOrg = + selectedOrg && selectedOrgIsPermitted + ? selectedOrg + : (permittedOrgs.find((org) => org.is_default) ?? + permittedOrgs[0] ?? + initialOrg ?? + null); + const organizationId = effectiveOrg?.id ?? ""; + // Adopt a permitted fallback so later refetches cannot switch the form to a + // re-permitted default. The permission guard also avoids a render loop. + if ( + orgSelectionSettled && + !selectedOrg && + effectiveOrg && + permittedOrgs.some((org) => org.id === effectiveOrg.id) + ) { + setSelectedOrg(effectiveOrg); + } + // Clear a workspace after a settled org change, before its localStorage value + // is cleared post-commit. An empty permission set has no selectable org, so + // preserve the workspace until its org is re-permitted. + const [lastSettledOrgId, setLastSettledOrgId] = useState(null); + if ( + orgSelectionSettled && + !noPermittedOrgs && + organizationId !== lastSettledOrgId + ) { + setLastSettledOrgId(organizationId); + if (lastSettledOrgId !== null) { + setSelectedWorkspaceId(null); + } + } + useEffect(() => { + if (selectedWorkspaceId === null) { + localStorage.removeItem(selectedWorkspaceIdStorageKey); + } + }, [selectedWorkspaceId]); const [planModeEnabled, setPlanModeEnabled] = useState(false); const hasModelOptions = modelOptions.length > 0; const hasConfiguredModels = hasConfiguredModelsInCatalog(modelCatalog); @@ -377,7 +431,7 @@ export const AgentCreateForm: FC = ({ saveReasoningEffortForModel(selectedModel, value); }; - const isForbidden = !canCreateChat; + const isForbidden = !canCreateChat || noPermittedOrgs; // Filter workspaces by the selected organization. We use // client-side filtering of the full "owner:me" fetch rather @@ -388,8 +442,8 @@ export const AgentCreateForm: FC = ({ // enough to warrant pagination, this should switch to a // server-side organization: query filter. const filteredWorkspaces = - showOrganizations && selectedOrg - ? workspaceOptions.filter((ws) => ws.organization_id === selectedOrg.id) + showOrganizations && effectiveOrg + ? workspaceOptions.filter((ws) => ws.organization_id === effectiveOrg.id) : workspaceOptions; const effectiveWorkspaceId = @@ -398,6 +452,11 @@ export const AgentCreateForm: FC = ({ filteredWorkspaces.some((ws) => ws.id === selectedWorkspaceId)) ? selectedWorkspaceId : null; + // A stored workspace cannot be validated against the effective org + // until the list loads; sending then would silently drop the + // association, so Send stays disabled instead. + const workspaceValidationPending = + selectedWorkspaceId !== null && isWorkspacesLoading; const handleSend = async (message: string, fileIDs?: string[]) => { submitDraft(); @@ -420,6 +479,7 @@ export const AgentCreateForm: FC = ({ }; const { + organizationAdopted, attachments, textContents, uploadStates, @@ -427,10 +487,17 @@ export const AgentCreateForm: FC = ({ handleAttach, handleRemoveAttachment, resetAttachments, - } = useFileAttachments(organizationId || undefined, { - persist: true, - provider: getProviderForModelOption(modelOptions, selectedModel), - }); + } = useFileAttachments( + // Avoid restoring against effectiveOrg's fallback when no org is permitted; + // that would prune attachments persisted for other orgs. + orgSelectionSettled && !noPermittedOrgs + ? organizationId || undefined + : undefined, + { + persist: true, + provider: getProviderForModelOption(modelOptions, selectedModel), + }, + ); const handleSendWithAttachments = async (message: string) => { const fileIds: string[] = []; @@ -459,50 +526,6 @@ export const AgentCreateForm: FC = ({ } }; - const permittedOrgsQuery = useQuery({ - ...permittedOrganizations({ - object: { resource_type: "chat", owner_id: "me" }, - action: "create", - }), - enabled: showOrganizations, - }); - const permittedOrgs = permittedOrgsQuery.data ?? organizations; - - // Reconcile selectedOrg when permission filtering removes it. - // Only pure state setters run during render; side effects - // (localStorage, blob URL cleanup) run in the effect below. - const [prevPermittedOrgs, setPrevPermittedOrgs] = useState(permittedOrgs); - const [orgWasAdjusted, setOrgWasAdjusted] = useState(false); - if (permittedOrgs !== prevPermittedOrgs) { - setPrevPermittedOrgs(permittedOrgs); - if (selectedOrg && !permittedOrgs.some((o) => o.id === selectedOrg.id)) { - // Fall back through: first permitted org, then the - // dashboard default. Never null out selectedOrg. - // organizationId must always be a valid UUID for the - // create-chat request. - const nextOrg = permittedOrgs[0] ?? initialOrg ?? null; - setSelectedOrg(nextOrg); - if (nextOrg?.id !== selectedOrg.id) { - setOrgWasAdjusted(true); - } - } - } - - // Clean up workspace and attachment state after a programmatic - // org change from permission filtering. These calls have side - // effects (localStorage, blob URL revocation) that must not - // run during render. - const onOrgAdjusted = useEffectEvent(() => { - handleWorkspaceChange(null); - resetAttachments(); - }); - useEffect(() => { - if (orgWasAdjusted) { - setOrgWasAdjusted(false); - onOrgAdjusted(); - } - }, [orgWasAdjusted]); - return ( <>
@@ -526,23 +549,27 @@ export const AgentCreateForm: FC = ({ {permittedOrgsQuery.error != null && ( )} - {showOrganizations && permittedOrgs.length > 1 && ( - { - const orgChanged = newOrg.id !== selectedOrg?.id; - if (orgChanged && attachments.length > 0) { - setPendingOrgChange(newOrg); - return; - } - if (orgChanged) { - handleWorkspaceChange(null); - } - setSelectedOrg(newOrg); - }} - /> - )} + {/* The pre-settlement list is the unfiltered dashboard fallback; + selecting from it could destroy existing workspace state. */} + {showOrganizations && + orgSelectionSettled && + permittedOrgs.length > 1 && ( + { + const orgChanged = newOrg.id !== effectiveOrg?.id; + if (orgChanged && attachments.length > 0) { + setPendingOrgChange(newOrg); + return; + } + if (orgChanged) { + handleWorkspaceChange(null); + } + setSelectedOrg(newOrg); + }} + /> + )} = ({ isDisabled={ isCreating || isForbidden || + !orgSelectionSettled || + // Sending before adoption would omit persisted files not yet restored. + !organizationAdopted || + workspaceValidationPending || isPersonalModelOverridesLoading || !hasModelOptions || Boolean(aiGatewayDisabled) @@ -569,7 +600,9 @@ export const AgentCreateForm: FC = ({ planModeEnabled={planModeEnabled} onPlanModeToggle={setPlanModeEnabled} attachments={attachments} - onAttach={handleAttach} + // Files attached before org adoption cannot upload and would be discarded + // when restoration completes. + onAttach={organizationAdopted ? handleAttach : undefined} onRemoveAttachment={handleRemoveAttachment} uploadStates={uploadStates} previewUrls={previewUrls} @@ -583,7 +616,12 @@ export const AgentCreateForm: FC = ({ onMCPAuthComplete={onMCPAuthComplete} workspaceOptions={filteredWorkspaces} selectedWorkspaceId={effectiveWorkspaceId} - onWorkspaceChange={handleWorkspaceChange} + // Do not persist a workspace until its organization is authorized. + onWorkspaceChange={ + orgSelectionSettled && !noPermittedOrgs + ? handleWorkspaceChange + : undefined + } isWorkspaceLoading={isWorkspacesLoading} canConfigureAgentSetup={canConfigureAgentSetup} providerCount={providerCount} @@ -617,10 +655,18 @@ export const AgentCreateForm: FC = ({ hideCancel={false} confirmText="Continue" onConfirm={() => { + if (!pendingOrgChange) { + return; + } + setPendingOrgChange(null); + // Recheck authorization because a refetch may revoke the pending org + // after this render created the closure. + if (!permittedOrgs.some((org) => org.id === pendingOrgChange.id)) { + return; + } resetAttachments(); handleWorkspaceChange(null); setSelectedOrg(pendingOrgChange); - setPendingOrgChange(null); }} onClose={() => setPendingOrgChange(null)} /> diff --git a/site/src/pages/AgentsPage/hooks/useFileAttachments.test.tsx b/site/src/pages/AgentsPage/hooks/useFileAttachments.test.tsx new file mode 100644 index 0000000000000..38d82886b3320 --- /dev/null +++ b/site/src/pages/AgentsPage/hooks/useFileAttachments.test.tsx @@ -0,0 +1,216 @@ +import { act, render, renderHook, waitFor } from "@testing-library/react"; +import { type FC, Suspense } from "react"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { API } from "#/api/api"; +import { createDeferred } from "#/testHelpers/deferred"; +import { + persistedAttachmentsStorageKey, + useFileAttachments, +} from "./useFileAttachments"; + +const persistEntry = (fileId: string, fileName: string, orgId: string) => ({ + fileId, + fileName, + fileType: "text/plain", + lastModified: 1000, + organizationId: orgId, +}); + +const uploadedFileIds = ( + result: ReturnType, +): string[] => + [...result.uploadStates.values()].flatMap((state) => + state.status === "uploaded" && state.fileId ? [state.fileId] : [], + ); + +const renderAttachments = (initialProps: { orgId: string | undefined }) => + renderHook( + ({ orgId }: { orgId: string | undefined }) => + useFileAttachments(orgId, { persist: true }), + { initialProps }, + ); + +const mockDeferredUpload = (): ((value: { id: string }) => void) => { + const deferred = createDeferred<{ id: string }>(); + vi.spyOn(API.experimental, "uploadChatFile").mockReturnValue( + deferred.promise, + ); + return deferred.resolve; +}; + +describe("useFileAttachments org scoping", () => { + beforeEach(() => { + localStorage.clear(); + }); + + it("defers restoration until the org is known", () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-b", "b.txt", "org-b")]), + ); + const { result, rerender } = renderAttachments({ orgId: undefined }); + + expect(result.current.attachments).toHaveLength(0); + expect(localStorage.getItem(persistedAttachmentsStorageKey)).toContain( + "file-b", + ); + + rerender({ orgId: "org-b" }); + expect(uploadedFileIds(result.current)).toStrictEqual(["file-b"]); + }); + + it("drops another org's attachments when the org changes", async () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-a", "a.txt", "org-a")]), + ); + const { result, rerender } = renderAttachments({ orgId: "org-a" }); + expect(uploadedFileIds(result.current)).toStrictEqual(["file-a"]); + rerender({ orgId: "org-b" }); + await waitFor(() => { + expect(uploadedFileIds(result.current)).toStrictEqual([]); + }); + expect( + localStorage.getItem(persistedAttachmentsStorageKey) ?? "", + ).not.toContain("file-a"); + }); + + it("never exposes the previous org's file IDs in any render", async () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-a", "a.txt", "org-a")]), + ); + // Log the hook output of every render, including the + // intermediate commit between the org changing and the + // adoption effect running; that window must expose nothing. + const renderLog: { orgId: string; fileIds: string[] }[] = []; + const Probe: FC<{ orgId: string }> = ({ orgId }) => { + const result = useFileAttachments(orgId, { persist: true }); + renderLog.push({ orgId, fileIds: uploadedFileIds(result) }); + return null; + }; + const { rerender } = render(); + expect(renderLog.at(-1)).toStrictEqual({ + orgId: "org-a", + fileIds: ["file-a"], + }); + + rerender(); + await waitFor(() => { + expect(renderLog.at(-1)?.fileIds).toStrictEqual([]); + }); + const leaked = renderLog.filter( + (entry) => entry.orgId === "org-b" && entry.fileIds.includes("file-a"), + ); + expect(leaked).toStrictEqual([]); + }); + + it("reports adoption only after the post-commit effect", () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-a", "a.txt", "org-a")]), + ); + const log: { adopted: boolean; fileIds: string[] }[] = []; + const Probe: FC<{ orgId: string }> = ({ orgId }) => { + const result = useFileAttachments(orgId, { persist: true }); + log.push({ + adopted: result.organizationAdopted, + fileIds: uploadedFileIds(result), + }); + return null; + }; + render(); + expect(log[0]).toStrictEqual({ adopted: false, fileIds: [] }); + expect(log.at(-1)).toStrictEqual({ adopted: true, fileIds: ["file-a"] }); + }); + + it("discards an upload that completes after another org was adopted", async () => { + const resolveUpload = mockDeferredUpload(); + const { result, rerender } = renderAttachments({ orgId: "org-a" }); + act(() => { + result.current.startUpload(new File(["x"], "x.txt")); + }); + + rerender({ orgId: "org-b" }); + await waitFor(() => { + expect(result.current.organizationAdopted).toBe(true); + }); + await act(async () => { + resolveUpload({ id: "file-x" }); + }); + + expect(uploadedFileIds(result.current)).toStrictEqual([]); + expect( + localStorage.getItem(persistedAttachmentsStorageKey) ?? "", + ).not.toContain("file-x"); + }); + + it("discards an upload that spans an org round trip", async () => { + const resolveUpload = mockDeferredUpload(); + const { result, rerender } = renderAttachments({ orgId: "org-a" }); + act(() => { + result.current.startUpload(new File(["x"], "x.txt")); + }); + + // After A -> B -> A, only the adoption epoch distinguishes the stale upload. + rerender({ orgId: "org-b" }); + await waitFor(() => { + expect(result.current.organizationAdopted).toBe(true); + }); + rerender({ orgId: "org-a" }); + await waitFor(() => { + expect(result.current.organizationAdopted).toBe(true); + }); + await act(async () => { + resolveUpload({ id: "file-x" }); + }); + + expect(uploadedFileIds(result.current)).toStrictEqual([]); + expect( + localStorage.getItem(persistedAttachmentsStorageKey) ?? "", + ).not.toContain("file-x"); + }); + + it("hides attachments when authorization leaves no org", async () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-a", "a.txt", "org-a")]), + ); + const { result, rerender } = renderAttachments({ orgId: "org-a" }); + await waitFor(() => { + expect(uploadedFileIds(result.current)).toStrictEqual(["file-a"]); + }); + + rerender({ orgId: undefined }); + expect(result.current.attachments).toStrictEqual([]); + expect(uploadedFileIds(result.current)).toStrictEqual([]); + expect(localStorage.getItem(persistedAttachmentsStorageKey)).toContain( + "file-a", + ); + + rerender({ orgId: "org-a" }); + await waitFor(() => { + expect(uploadedFileIds(result.current)).toStrictEqual(["file-a"]); + }); + }); + + it("does not prune storage during a render that never commits", () => { + localStorage.setItem( + persistedAttachmentsStorageKey, + JSON.stringify([persistEntry("file-a", "a.txt", "org-a")]), + ); + // Suspending after the hook runs simulates a render React abandons before commit. + const Suspender: FC<{ orgId: string }> = ({ orgId }) => { + useFileAttachments(orgId, { persist: true }); + throw new Promise(() => {}); + }; + render( + + + , + ); + expect(localStorage.getItem(persistedAttachmentsStorageKey)).toContain( + "file-a", + ); + }); +}); diff --git a/site/src/pages/AgentsPage/hooks/useFileAttachments.ts b/site/src/pages/AgentsPage/hooks/useFileAttachments.ts index eb6098c631225..b4b9193fb8808 100644 --- a/site/src/pages/AgentsPage/hooks/useFileAttachments.ts +++ b/site/src/pages/AgentsPage/hooks/useFileAttachments.ts @@ -52,9 +52,7 @@ function restorePersistedAttachments(currentOrgId: string): { uploadStates: Map; previewUrls: Map; } { - // Skip when org ID isn't loaded yet so we don't prune valid - // entries. The initializer runs once, so callers must wait for - // the org ID before mounting. + // An unknown org must not prune entries persisted for the eventual org. if (!currentOrgId) { return { attachments: [], @@ -164,6 +162,11 @@ function clearPersistedAttachments() { } interface UseFileAttachmentsReturn { + /** + * True after the post-commit effect assigns in-memory attachment state to + * the supplied organization. Keep attach and send controls disabled until then. + */ + organizationAdopted: boolean; attachments: File[]; textContents: Map; uploadStates: Map; @@ -192,19 +195,14 @@ export function useFileAttachments( providerRef.current = provider; }, [provider]); - const [restored] = useState(() => - persist - ? restorePersistedAttachments(organizationId ?? "") - : { - attachments: [] as File[], - uploadStates: new Map(), - previewUrls: new Map(), - }, + const [attachments, setAttachments] = useState([]); + const [uploadStates, setUploadStates] = useState( + () => new Map(), ); - - const [attachments, setAttachments] = useState(restored.attachments); - const [uploadStates, setUploadStates] = useState(restored.uploadStates); - const [previewUrls, setPreviewUrls] = useState(restored.previewUrls); + const [previewUrls, setPreviewUrls] = useState(() => new Map()); + // Persisted state remains unowned until post-commit org adoption; restoring + // against a provisional org would prune entries for the eventual org. + const [stateOrgId, setStateOrgId] = useState(null); const [textContents, setTextContents] = useState( () => new Map(), ); @@ -220,6 +218,27 @@ export function useFileAttachments( return () => revokePreviewUrls(); }, []); + // Every adoption invalidates older upload completions, including A-to-B-to-A + // round trips where stateOrgId matches again. Otherwise a stale completion + // could persist and later restore an abandoned upload. + const adoptionEpochRef = useRef(0); + const commitUploadOutcome = useEffectEvent( + ( + file: File, + uploadOrgId: string, + uploadEpoch: number, + state: UploadState, + ) => { + if (persist && adoptionEpochRef.current !== uploadEpoch) { + return; + } + setUploadStates((prev) => new Map(prev).set(file, state)); + if (persist && state.status === "uploaded" && state.fileId) { + addPersistedAttachment(file, state.fileId, uploadOrgId); + } + }, + ); + const startUpload = (file: File) => { if (!organizationId) { setUploadStates((prev) => @@ -231,25 +250,18 @@ export function useFileAttachments( return; } - const shouldPersist = persist && Boolean(organizationId); + const uploadOrgId = organizationId; + const uploadEpoch = adoptionEpochRef.current; const isImage = file.type.startsWith("image/"); setUploadStates((prev) => new Map(prev).set(file, { status: "uploading" })); void (async () => { try { - const result = await API.experimental.uploadChatFile( - file, - organizationId, - ); - setUploadStates((prev) => - new Map(prev).set(file, { - status: "uploaded", - fileId: result.id, - }), - ); - if (shouldPersist) { - addPersistedAttachment(file, result.id, organizationId!); - } + const result = await API.experimental.uploadChatFile(file, uploadOrgId); + commitUploadOutcome(file, uploadOrgId, uploadEpoch, { + status: "uploaded", + fileId: result.id, + }); if (isImage) { // Pre-warm the HTTP cache so the timeline can // render the image instantly after send. Text @@ -257,13 +269,10 @@ export function useFileAttachments( void fetch(getChatFileURL(result.id)); } } catch (err: unknown) { - const errorMessage = formatAgentAttachmentUploadError(err); - setUploadStates((prev) => - new Map(prev).set(file, { - status: "error", - error: errorMessage, - }), - ); + commitUploadOutcome(file, uploadOrgId, uploadEpoch, { + status: "error", + error: formatAgentAttachmentUploadError(err), + }); } })(); }; @@ -273,6 +282,28 @@ export function useFileAttachments( // file can't be resurrected. WeakSet lets entries get GC'd. const abandonedResizesRef = useRef>(new WeakSet()); + // Permission refetches can change the org without user action. Replace state + // after commit so stale file IDs cannot cross orgs and an abandoned render + // cannot prune localStorage through restorePersistedAttachments. + const adoptOrganization = useEffectEvent((orgId: string) => { + adoptionEpochRef.current += 1; + for (const file of attachments) { + abandonedResizesRef.current.add(file); + } + revokePreviewUrls(); + setTextContents(new Map()); + setStateOrgId(orgId); + const restored = restorePersistedAttachments(orgId); + setAttachments(restored.attachments); + setUploadStates(restored.uploadStates); + setPreviewUrls(restored.previewUrls); + }); + useEffect(() => { + if (persist && organizationId && stateOrgId !== organizationId) { + adoptOrganization(organizationId); + } + }, [persist, stateOrgId, organizationId]); + type AttachItem = { file: File; needsResize: boolean }; const processResizes = async ( @@ -508,11 +539,18 @@ export function useFileAttachments( } }; + // Hide state that belongs to another organization. Exposing it could send + // stale file IDs or remove persisted attachments from the previous org. + const orgMismatch = + persist && stateOrgId !== null && stateOrgId !== organizationId; + return { - attachments, - textContents, - uploadStates, - previewUrls, + organizationAdopted: + !persist || (Boolean(organizationId) && stateOrgId === organizationId), + attachments: orgMismatch ? [] : attachments, + textContents: orgMismatch ? new Map() : textContents, + uploadStates: orgMismatch ? new Map() : uploadStates, + previewUrls: orgMismatch ? new Map() : previewUrls, handleAttach, handleRemoveAttachment, startUpload,