mirror of
https://github.com/coder/coder.git
synced 2026-10-31 19:36:41 +08:00
d014d4c372fb7dfdb3b0ea00a9935fec9b9528f9
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
211f490910 |
refactor: share the AI Gateway price book contract (#28310)
Follow-up to the review discussion on the price book refresh automation: https://github.com/coder/coder/pull/28146#discussion_r3806439966 ## Problem The price seed row struct was duplicated three times, so a JSON tag change had to be repeated in each copy: - `scripts/aibridgepricesgen.priceRow` - `scripts/aibridgepricesdiff.priceRow` - `coderd/aibridge/prices.seedRow` The `(provider, model)` key and its parsing were duplicated too. ## Change Add `coderd/aibridge/prices/pricebook` holding the shared contract: `Row`, `Key`, `Row.Key()`, and `Parse`. The package depends only on the standard library, so the scripts can import it without pulling in the database layer that `coderd/aibridge/prices` brings. Consumer-specific behaviour stays where it is: upstream conversion and validation in the generator, comparison and Markdown rendering in the diff tool, DB seeding and runtime lookup in the seeder. The batch SQL upsert still extracts JSON fields by name, so it remains a separate contract boundary. The note about keeping it in sync now lives on the shared `Row`. No generated artifacts change; the serialized schema is identical. --- <sub>Opened by Coder Agents on behalf of @evgeniy-scherbina.</sub> |
||
|
|
19d1ee9fc8 |
feat: support custom prices for models in the price book (#28328)
## Description The experimental CLI could only price models the price book does not cover, so an operator with a negotiated rate for a model Coder already prices had no way to record it. The restriction existed because a single row served both the price book and prices set through the API, and the startup seeder would overwrite an override on the next restart. Each source now keeps its own row (https://github.com/coder/coder/pull/28204), so that no longer applies. This removes the restriction: a price set through the CLI takes effect over the price book for any model, and the book stays underneath it. ## Changes - Remove the validation that rejected models the price book covers. - Delete `prices.IsDefaultPriced` and the index behind it, which existed only for that check. - Generalize the `coder exp ai-model-prices update` help. - Document that a custom price overrides the price book and does not follow its updates. - Rework the spend approximation callout around what a custom price can and cannot correct. Closes https://linear.app/codercom/issue/AIGOV-594 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
9390e51422 |
feat: distinguish default and custom AI model prices (#28204)
## Description Prices in `ai_model_prices` come from two places: the price book embedded in each Coder release, and prices set through the experimental CLI (https://github.com/coder/coder/pull/27926). Until now, they were indistinguishable, so the startup seeder re-applied the book over every row, and a price someone set would silently revert on the next restart. This adds a `source` column with values `default` and `custom`, and keeps a row for each. Readers resolve a model to its custom price when one exists and fall back to the book, including the cost path that prices an interception. Keeping both rows means the price book stays current underneath an override, so a future delete can restore it. Note: the endpoint still only accepts models the price book does not cover. Overriding a price the book already carries is handled by the follow-up issue https://linear.app/codercom/issue/AIGOV-594. ## Changes - Add a migration for the `ai_model_price_source` enum and a `source` column, backfilled to `default` and then `NOT NULL`. `source` joins the primary key, so a model can hold one row per source. - Take a `source` parameter on `UpsertAIModelPrices`. Each source keeps its own row, so the price book and a custom price never overwrite each other. - Resolve to the custom price in `GetAIModelPriceByProviderModel` and `GetAIModelPrices`, so both the interception cost snapshot and the list endpoint use it. - Seed the price book as `default`, and write prices set through the endpoint as `custom`. Closes https://linear.app/codercom/issue/AIGOV-592 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
5e0710de92 |
chore: refresh AI model price book (#28365)
## Price book changes 61 models added, 14 models removed, 51 models changed. <details> <summary>Added</summary> - azure/claude-mythos-5 - azure/claude-opus-4-7 - azure/kimi-k2.7-code - bedrock/global.openai.gpt-5.6-luna - bedrock/global.openai.gpt-5.6-sol - bedrock/global.openai.gpt-5.6-terra - bedrock/xai.grok-4.6 - copilot/gemini-3.7-flash - copilot/grok-4.6 - copilot/kimi-k3 - copilot/mai-code-1.1-flash - google/gemini-3.7-flash - openrouter/bytedance-seed/seed-2-1-turbo - openrouter/bytedance-seed/seed-2.0-code - openrouter/deepseek/deepseek-v4-pro-0813 - openrouter/dots-studio/dots-3-note-preview:free - openrouter/google/gemini-3.7-flash - openrouter/inclusionai/ling-3.0-flash - openrouter/liquid/lfm-2.5-2.6b:free - openrouter/meta/muse-glimmer-30b - openrouter/meta/muse-spark-1.2 - openrouter/nvidia/nemotron-3.5-lightning - openrouter/nvidia/nemotron-3.5-lightning:free - openrouter/qwen/qwen3.8-2.4t-a95b - openrouter/qwen/qwen3.8-27b - openrouter/sakana/sakana-namazu - openrouter/upstage/solar-pro4 - openrouter/x-ai/grok-4.6 - openrouter/z-ai/glm-5.2:free - openrouter/z-ai/glm-5.3 - openrouter/~z-ai/glm-latest - vercel/alibaba/qwen3.8-2.4t-a95b - vercel/alibaba/qwen3.8-27b - vercel/deepseek/deepseek-v4-pro-0813 - vercel/google/gemini-3.7-flash - vercel/inclusionai/ling-3.0-flash - vercel/meta/muse-glimmer-30b - vercel/meta/muse-spark-1.2 - vercel/meta/muse-spark-1.2-contributor - vercel/nvidia/nemotron-3.5-lightning - vercel/openai/gpt-4.1-fast - vercel/openai/gpt-4.1-mini-fast - vercel/openai/gpt-4.1-nano-fast - vercel/openai/gpt-4o-fast - vercel/openai/gpt-4o-mini-fast - vercel/openai/gpt-5-fast - vercel/openai/gpt-5-mini-fast - vercel/openai/gpt-5.1-thinking-fast - vercel/openai/gpt-5.2-fast - vercel/openai/gpt-5.3-codex-fast - vercel/openai/gpt-5.4-fast - vercel/openai/gpt-5.4-mini-fast - vercel/openai/gpt-5.5-fast - vercel/openai/gpt-5.6-luna-fast - vercel/openai/gpt-5.6-sol-fast - vercel/openai/gpt-5.6-terra-fast - vercel/openai/o3-fast - vercel/openai/o4-mini-fast - vercel/sakana/namazu - vercel/xai/grok-4.6 - vercel/zai/glm-5.3 </details> <details> <summary>Removed</summary> - anthropic/claude-opus-4-1 - anthropic/claude-opus-4-1-20250805 - google/gemini-2.0-flash - google/gemini-2.0-flash-lite - google/gemini-3-pro-preview - openrouter/ai21/jamba-large-1.7 - openrouter/inclusionai/ling-3.0-flash:free - openrouter/mancer/weaver - openrouter/openai/gpt-5.3-chat - vercel/anthropic/claude-opus-4.1 - vercel/inclusionai/ling-3.0-flash-free - vercel/openai/gpt-5.1-instant - vercel/openai/gpt-5.3-chat - vercel/zai/glm-4.6v </details> <details> <summary>Changed</summary> - azure/gpt-5.6-luna - azure/gpt-5.6-sol - azure/gpt-5.6-terra - bedrock/openai.gpt-5.6-sol - openrouter/deepseek/deepseek-chat-v3-0324 - openrouter/deepseek/deepseek-v3.1-terminus - openrouter/deepseek/deepseek-v4-flash - openrouter/deepseek/deepseek-v4-flash-0731 - openrouter/deepseek/deepseek-v4-pro - openrouter/google/gemini-3.6-flash - openrouter/google/gemma-4-31b-it - openrouter/gryphe/mythomax-l2-13b - openrouter/minimax/minimax-m2.5 - openrouter/minimax/minimax-m2.7 - openrouter/mistralai/mistral-small-3.2-24b-instruct - openrouter/moonshotai/kimi-k2.5 - openrouter/moonshotai/kimi-k2.6 - openrouter/moonshotai/kimi-k2.7-code - openrouter/openai/gpt-5.6-luna - openrouter/openai/gpt-5.6-luna-pro - openrouter/openai/gpt-5.6-sol - openrouter/openai/gpt-5.6-sol-pro - openrouter/openai/gpt-5.6-terra - openrouter/openai/gpt-5.6-terra-pro - openrouter/openai/gpt-oss-120b - openrouter/qwen/qwen-plus-2025-07-28:thinking - openrouter/qwen/qwen2.5-vl-72b-instruct - openrouter/qwen/qwen3-14b - openrouter/qwen/qwen3-235b-a22b-2507 - openrouter/qwen/qwen3-30b-a3b - openrouter/qwen/qwen3-coder-30b-a3b-instruct - openrouter/qwen/qwen3-vl-235b-a22b-thinking - openrouter/qwen/qwen3-vl-30b-a3b-instruct - openrouter/qwen/qwen3.5-35b-a3b - openrouter/qwen/qwen3.6-27b - openrouter/qwen/qwen3.6-35b-a3b - openrouter/tencent/hy3-preview - openrouter/thinkingmachines/inkling - openrouter/thinkingmachines/inkling-small - openrouter/z-ai/glm-5 - openrouter/z-ai/glm-5.2 - openrouter/~deepseek/deepseek-v4-flash-latest - openrouter/~google/gemini-flash-latest - openrouter/~moonshotai/kimi-latest - openrouter/~openai/gpt-latest - openrouter/~x-ai/grok-latest - vercel/deepseek/deepseek-v4-flash - vercel/deepseek/deepseek-v4-pro - vercel/google/gemini-3.6-flash - vercel/openai/gpt-5.6-sol - vercel/zai/glm-5.2 </details> ## Review notes Regenerated by `make gen/aibridge-prices` from the live [models.dev](https://models.dev) catalog. Both artifacts come from one snapshot, so they ship together: - `coderd/aibridge/prices/data/prices.json` - `site/src/pages/AgentsPage/components/ChatModelAdminPanel/knownModels/knownModelsGenerated.json` These are customer-visible cost numbers taken from upstream data, so this PR is never merged automatically. The summary above lists what moved; check the diff for exact figures before approving. Opened automatically by the [aigateway-prices-refresh workflow](https://github.com/coder/coder/actions/runs/32352560765). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> |
||
|
|
2d9b6eda8f |
feat: add experimental CLI to price unpriced AI models (#27926)
## Description AI Gateway computes the cost of an interception from `ai_model_prices`, which is seeded on every server start from a price book embedded in the binary. A model the price book does not cover records a NULL cost, so its spend is invisible to cost reporting and is not enforced against budgets. The only fix was to wait for a Coder release that added the model. This adds an experimental CLI, backed by an experimental HTTP endpoint, for pricing those models. Models the price book already covers are rejected, because the seeder re-applies the book on every start and would overwrite an operator price. Support for custom pricing will be handled in https://linear.app/codercom/issue/AIGOV-589/extend-experimental-cli-command-to-set-custom-ai-model-prices. ## Commands ``` coder exp ai-model-prices list [--provider] [--model] coder exp ai-model-prices update [file|-] [--provider] [--model] [--input-price] [--output-price] [--cache-read-price] [--cache-write-price] [--yes] ``` ## Changes - Add `GET` and `POST /api/experimental/ai/model-prices`, gated behind the AI Bridge entitlement and the existing `ai_model_price` RBAC resource. - Add a `GetAIModelPrices` query with optional `provider` and `model` filters applied in SQL. - Validate the whole request before writing anything, so one bad entry cannot leave the table half updated, and report every problem at once. - Reject prices for models the embedded price book already covers, through a new `prices.IsDefaultPriced`. - Add the `coder exp ai-model-prices` command with `list` and `update`. `update` accepts a JSON document or the single-model flags and prints a plan, asking to confirm unless the document is piped in or `--yes` is passed. - Consolidate the supported provider list into `coderd/aibridge/prices/providers` so the price generator and the server share one definition. - Add `codersdk` types and client methods for both endpoints, and bound the request body at 1 MiB. - Document the command in the AI Gateway cost controls page. Closes https://linear.app/codercom/issue/AIGOV-567/experimental-cli-command-to-set-prices-for-unpriced-ai-models > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
5efa7abe7d |
fix: only write AI model prices that changed (#27923)
Previously, the AI Gateway price seeder rewrote every row of `ai_model_prices` on each server start, because `ON CONFLICT` fires on a key conflict rather than on a value difference. `updated_at` therefore recorded when the server last restarted rather than when a price last changed. Guard the `DO UPDATE` branch so a conflicting row is only rewritten when one of its four prices differs. The comparison uses `IS DISTINCT FROM` rather than `<>` because the price columns are nullable, and `<>` yields NULL when either side is NULL, which would skip the update and leave a stale price in place. Related to https://linear.app/codercom/issue/AIGOV-567/experimental-cli-command-to-set-prices-for-unpriced-ai-models > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira |
||
|
|
60161fd375 |
chore: update model prices to include more providers (#27837)
Adds the full set of supported provider types to `scripts/aibridgepricesgen` and updates stored model prices. Notes: * We need to rename a few keys from models.dev JSON to match our internal provider types. * `prices.json` is now marked as generated. --------- Co-authored-by: Susana Ferreira <susana@coder.com> |
||
|
|
d57965ee7f |
chore: regenerate prices.json from models.dev (#27549)
Implements: https://linear.app/codercom/issue/AIGOV-493/update-pricesjson-with-current-model-rates-before-cost-control-release Run `make gen/aibridge-prices` to regenerate `prices.json` from `models.dev`. This update: - adds support for the upstream `claude-opus-5` model; - removes older `openai` models that have been removed upstream, keeping us in sync with `models.dev`. |
||
|
|
8eaf4f507b |
feat: generate the known-models catalog and aigateway prices (#27146)
- Regenerates `prices.json` from models.dev. The seeder only upserts, so existing deployments keep delisted models. - Generate the frontend known-models catalog instead of hand-writing it. `make gen/aibridge-prices` fetches models.dev once - Moved patches to model definitions to separate `overrides.jq` which handles both `claude-sonnet-4-5` 200k context and 'aliasing' Fable 5 as Mythos 5. - Editorial choices of selection, order, aliases, and reasoning defaults live in `curation.json`. - Adds golden join tests with one error case per validation, a no-network drift test comparing curation to the checked-in artifact, and pinned invariants for the Anthropic thinking-mode split (the wrong side returns HTTP 400) and the sonnet-4-5 context pin. Adding a model is now one `curation.json` entry plus `make gen/aibridge-prices`, assuming it is present on models.dev. > This PR was authored by Coder Agents on Cian's behalf. --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> |
||
|
|
4124d1137d |
feat: add ai_model_prices table (#24932)
# Summary Implements https://linear.app/codercom/issue/AIGOV-282/add-ai-model-price-table-and-seed-generator This PR lays the groundwork for AI Bridge cost controls (per the AI Governance RFC). It adds the foundation needed for future cost tracking: a place to store per-model token prices, a way to keep those prices in sync with upstream pricing data, and a startup mechanism that ensures every deployment has prices loaded before AI Bridge starts processing requests. The price data comes from [models.dev](https://models.dev/), a community-maintained catalogue of AI provider pricing. A generator script fetches the latest prices, filters to Anthropic and OpenAI for now, and produces a seed file checked into the repository. On every server startup the seed is applied to the database, so new releases automatically pick up any price corrections that landed since the previous one. Existing rows are overwritten with the latest prices; rows for models no longer in the seed are left untouched. # Batching the AI model price seed: three approaches Context: at server startup we seed the `ai_model_prices` table from an embedded JSON price book (~70 rows today, will grow as we add providers, potentially 4000+). Each row is: ```text (provider, model, input_price, output_price, cache_read_price, cache_write_price) ``` Any of the four price columns can be: - `NULL` → “price unknown for this dimension” - explicit `0` → “free” The batch must be an UPSERT so re-running is idempotent and existing rows pick up new prices. We considered three implementations. --- ## Approach 1 — Per-row UPSERT in a Go loop ```go for _, row := range rows { if err := db.UpsertAIModelPrice(ctx, database.UpsertAIModelPriceParams{ Provider: row.Provider, Model: row.Model, InputPrice: nullInt64(row.InputPrice), // ... }); err != nil { return err } } ``` ### Pros - Trivial. - NULL handling falls out naturally from `sql.NullInt64`. ### Cons - `N` round-trips per seed. - With ~70 rows that means ~70 statement executions on every startup, even inside a transaction. - Doesn't scale gracefully as the price book grows, potentially 4000+. --- ## Approach 2 — `UNNEST` with parallel arrays Pass each column as a separate Go slice. Postgres unnests them in parallel into a virtual table, then `INSERT ... SELECT`. ```sql INSERT INTO ai_model_prices ( provider, model, input_price, output_price, cache_read_price, cache_write_price ) SELECT UNNEST(@providers::text[]), UNNEST(@models::text[]), NULLIF(UNNEST(@input_prices::bigint[]), -1), NULLIF(UNNEST(@output_prices::bigint[]), -1), NULLIF(UNNEST(@cache_read_prices::bigint[]), -1), NULLIF(UNNEST(@cache_write_prices::bigint[]), -1) ON CONFLICT (provider, model) DO UPDATE SET input_price = EXCLUDED.input_price, output_price = EXCLUDED.output_price, cache_read_price = EXCLUDED.cache_read_price, cache_write_price = EXCLUDED.cache_write_price, updated_at = NOW(); ``` Go side: flatten rows into six parallel slices. Use a sentinel (`-1`) for “missing”, since `lib/pq` can't encode `NULL` into a `bigint[]` element. ```go providers := make([]string, len(rows)) models := make([]string, len(rows)) inputs := make([]int64, len(rows)) outputs := make([]int64, len(rows)) cacheR := make([]int64, len(rows)) cacheW := make([]int64, len(rows)) for i, r := range rows { providers[i] = r.Provider models[i] = r.Model inputs[i] = -1 if r.InputPrice != nil { inputs[i] = *r.InputPrice } outputs[i] = -1 if r.OutputPrice != nil { outputs[i] = *r.OutputPrice } cacheR[i] = -1 if r.CacheReadPrice != nil { cacheR[i] = *r.CacheReadPrice } cacheW[i] = -1 if r.CacheWritePrice != nil { cacheW[i] = *r.CacheWritePrice } } return db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{ Providers: providers, Models: models, InputPrices: inputs, OutputPrices: outputs, CacheReadPrices: cacheR, CacheWritePrices: cacheW, }) ``` ### Pros - Single round-trip. ### Cons - The generated `sqlc` params become plain `[]int64`, which can't represent `NULL`. --- ## Approach 3 — `jsonb_array_elements` over a single `@seed::jsonb` (chosen) Pass the raw seed JSON as one parameter; let Postgres expand and parse it. ```sql INSERT INTO ai_model_prices ( provider, model, input_price, output_price, cache_read_price, cache_write_price ) SELECT elem->>'provider', elem->>'model', (elem->>'input_price')::bigint, (elem->>'output_price')::bigint, (elem->>'cache_read_price')::bigint, (elem->>'cache_write_price')::bigint FROM jsonb_array_elements(@seed::jsonb) AS elem ON CONFLICT (provider, model) DO UPDATE SET input_price = EXCLUDED.input_price, output_price = EXCLUDED.output_price, cache_read_price = EXCLUDED.cache_read_price, cache_write_price = EXCLUDED.cache_write_price, updated_at = NOW(); ``` Go side reduces to: ```go return db.UpsertAIModelPrices(ctx, seedJSON) ``` ### Pros - Single round-trip. - NULLs fall out naturally: - `(elem->>'cache_write_price')::bigint` becomes `NULL` - no sentinels - The seed is already JSON: - Existing precedent: - `jsonb_array_elements` is already used elsewhere in the codebase ### Cons - Less type-safe at the SQL boundary than `UNNEST` - Slightly less standard than `UNNEST` - Readers need familiarity with: - `jsonb_array_elements` - `->>` extraction syntax - Postgres pays JSON parse cost - negligible at our scale --- --- # Decision We picked Approach 3. It collapses the round-trips like `UNNEST` does, but without: - nullable-array workarounds - sentinel values |