mirror of
https://github.com/coder/coder.git
synced 2026-10-31 19:36:41 +08:00
## Description With prices from the price book and prices set through the API both in the table, `list` showed only the price in effect and gave no way to tell where it came from. There was also no way to see the price book's entry for a model that had been overridden, since the listing resolves to the custom price. This adds a `source` column to the output and a `--source` filter for narrowing to one source, or to every row with `all`. Note: `--source` selects rows rather than models. A model carrying both a price book price and a custom one appears under either value, so the two filtered listings do not sum to the unfiltered one. `--source all` reports both rows at once. ## Changes - Add `Source` to `codersdk.AIModelPrice` and `AIModelPricesFilter`, and map it in `db2sdk`, which previously dropped it. - Filter `GetAIModelPrices` by source before resolution, so the price book's row for an overridden model stays reachable. - Accept `all` as a source, which reports every price a model holds instead of resolving to one. - Accept a `source` query parameter on the list endpoint, rejected with a 400 when outside the enum. - Add `--source` to the list command and a `source` column to its output. - Document the column and the filter, and lead the price section with `coder exp ai-model-prices list`. - Distinguish a deployment's default prices from the price book that ships with each release. Closes https://linear.app/codercom/issue/AIGOV-593 > [!NOTE] > Initially generated by Claude Opus 5, modified and reviewed by @ssncferreira
128 lines
4.4 KiB
Go
128 lines
4.4 KiB
Go
package codersdk
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
"time"
|
|
)
|
|
|
|
// AIModelPrice is a per-model token price used by AI Gateway to compute the
|
|
// cost of an interception.
|
|
//
|
|
// Prices are integer micro-units per million tokens, so 1000000 is $1.00 per
|
|
// million tokens. A nil price means the price is not known, which the cost
|
|
// calculation treats the same as zero. Distinguish that from an explicit 0,
|
|
// which declares the model free of charge.
|
|
type AIModelPrice struct {
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
InputPrice *int64 `json:"input_price"`
|
|
OutputPrice *int64 `json:"output_price"`
|
|
CacheReadPrice *int64 `json:"cache_read_price"`
|
|
CacheWritePrice *int64 `json:"cache_write_price"`
|
|
Source AIModelPriceSource `json:"source"`
|
|
CreatedAt time.Time `json:"created_at" format:"date-time"`
|
|
UpdatedAt time.Time `json:"updated_at" format:"date-time"`
|
|
}
|
|
|
|
// AIModelPriceSource is where a model price came from.
|
|
type AIModelPriceSource string
|
|
|
|
const (
|
|
// AIModelPriceSourceDefault is a price from the embedded price book.
|
|
AIModelPriceSourceDefault AIModelPriceSource = "default"
|
|
// AIModelPriceSourceCustom is a price set through the API.
|
|
AIModelPriceSourceCustom AIModelPriceSource = "custom"
|
|
)
|
|
|
|
// AIModelPriceSourceFilter selects which prices a listing reports. It is
|
|
// distinct from AIModelPriceSource because no stored price is "all".
|
|
//
|
|
// @typescript-ignore AIModelPriceSourceFilter
|
|
type AIModelPriceSourceFilter string
|
|
|
|
const (
|
|
AIModelPriceSourceFilterDefault = AIModelPriceSourceFilter(AIModelPriceSourceDefault)
|
|
AIModelPriceSourceFilterCustom = AIModelPriceSourceFilter(AIModelPriceSourceCustom)
|
|
// AIModelPriceSourceFilterAll reports every price a model holds, so a model
|
|
// carrying both appears twice.
|
|
AIModelPriceSourceFilterAll AIModelPriceSourceFilter = "all"
|
|
)
|
|
|
|
// MaxAIModelPricesBytes bounds an upsert request body.
|
|
const MaxAIModelPricesBytes = 1 << 20 // 1 MiB
|
|
|
|
// UpsertAIModelPricesRequest sets prices for the listed models. Models absent
|
|
// from the request are left untouched.
|
|
type UpsertAIModelPricesRequest struct {
|
|
Prices []AIModelPriceUpsert `json:"prices"`
|
|
}
|
|
|
|
// AIModelPriceUpsert is one model's prices in an upsert request. It carries
|
|
// only the writable fields of AIModelPrice.
|
|
type AIModelPriceUpsert struct {
|
|
Provider string `json:"provider"`
|
|
Model string `json:"model"`
|
|
InputPrice *int64 `json:"input_price"`
|
|
OutputPrice *int64 `json:"output_price"`
|
|
CacheReadPrice *int64 `json:"cache_read_price"`
|
|
CacheWritePrice *int64 `json:"cache_write_price"`
|
|
}
|
|
|
|
// AIModelPricesFilter narrows the listed model prices. An empty field does not
|
|
// filter on that attribute.
|
|
//
|
|
// @typescript-ignore AIModelPricesFilter
|
|
type AIModelPricesFilter struct {
|
|
Provider string `json:"provider,omitempty"`
|
|
Model string `json:"model,omitempty"`
|
|
// Source narrows to prices from one source. A model with both reports only
|
|
// its custom price unless this is set.
|
|
Source AIModelPriceSourceFilter `json:"source,omitempty"`
|
|
}
|
|
|
|
func (f AIModelPricesFilter) asRequestOption() RequestOption {
|
|
return func(r *http.Request) {
|
|
query := r.URL.Query()
|
|
if f.Provider != "" {
|
|
query.Set("provider", f.Provider)
|
|
}
|
|
if f.Model != "" {
|
|
query.Set("model", f.Model)
|
|
}
|
|
if f.Source != "" {
|
|
query.Set("source", string(f.Source))
|
|
}
|
|
r.URL.RawQuery = query.Encode()
|
|
}
|
|
}
|
|
|
|
// ListAIModelPrices returns the AI model prices matching the filter.
|
|
func (c *ExperimentalClient) ListAIModelPrices(ctx context.Context, filter AIModelPricesFilter) ([]AIModelPrice, error) {
|
|
res, err := c.Request(ctx, http.MethodGet, "/api/experimental/ai/model-prices", nil, filter.asRequestOption())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusOK {
|
|
return nil, ReadBodyAsError(res)
|
|
}
|
|
|
|
var prices []AIModelPrice
|
|
return prices, ReadBodyAsJSON(res, &prices)
|
|
}
|
|
|
|
// UpsertAIModelPrices sets prices for the models in req. The request is
|
|
// rejected in full if any model fails validation.
|
|
func (c *ExperimentalClient) UpsertAIModelPrices(ctx context.Context, req UpsertAIModelPricesRequest) error {
|
|
res, err := c.Request(ctx, http.MethodPost, "/api/experimental/ai/model-prices", req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer res.Body.Close()
|
|
if res.StatusCode != http.StatusNoContent {
|
|
return ReadBodyAsError(res)
|
|
}
|
|
return nil
|
|
}
|