mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
@@ -6,6 +6,10 @@ type CheckConstraint string
|
||||
|
||||
// CheckConstraint enums.
|
||||
const (
|
||||
CheckAiModelPricesCacheReadPriceCheck CheckConstraint = "ai_model_prices_cache_read_price_check" // ai_model_prices
|
||||
CheckAiModelPricesCacheWritePriceCheck CheckConstraint = "ai_model_prices_cache_write_price_check" // ai_model_prices
|
||||
CheckAiModelPricesInputPriceCheck CheckConstraint = "ai_model_prices_input_price_check" // ai_model_prices
|
||||
CheckAiModelPricesOutputPriceCheck CheckConstraint = "ai_model_prices_output_price_check" // ai_model_prices
|
||||
CheckAPIKeysAllowListNotEmpty CheckConstraint = "api_keys_allow_list_not_empty" // api_keys
|
||||
CheckChatModelConfigsCompressionThresholdCheck CheckConstraint = "chat_model_configs_compression_threshold_check" // chat_model_configs
|
||||
CheckChatModelConfigsContextLimitCheck CheckConstraint = "chat_model_configs_context_limit_check" // chat_model_configs
|
||||
|
||||
@@ -626,6 +626,7 @@ var (
|
||||
},
|
||||
rbac.ResourceApiKey.Type: {policy.ActionRead}, // Validate API keys.
|
||||
rbac.ResourceAibridgeInterception.Type: {policy.ActionCreate, policy.ActionRead, policy.ActionUpdate, policy.ActionDelete},
|
||||
rbac.ResourceAiModelPrice.Type: {policy.ActionUpdate}, // Required for the startup price seeder.
|
||||
rbac.ResourceAiSeat.Type: {policy.ActionCreate}, // Required for UpsertAISeatState.
|
||||
}),
|
||||
User: []rbac.Permission{},
|
||||
@@ -2480,6 +2481,13 @@ func (q *querier) GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, in
|
||||
return q.db.GetAIBridgeUserPromptsByInterceptionID(ctx, interceptionID)
|
||||
}
|
||||
|
||||
func (q *querier) GetAIModelPriceByProviderModel(ctx context.Context, arg database.GetAIModelPriceByProviderModelParams) (database.AiModelPrice, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceAiModelPrice); err != nil {
|
||||
return database.AiModelPrice{}, err
|
||||
}
|
||||
return q.db.GetAIModelPriceByProviderModel(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
return fetch(q.log, q.auth, q.db.GetAPIKeyByID)(ctx, id)
|
||||
}
|
||||
@@ -7534,6 +7542,13 @@ func (q *querier) UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg datab
|
||||
return q.db.UpdateWorkspacesTTLByTemplateID(ctx, arg)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
|
||||
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAiModelPrice); err != nil {
|
||||
return err
|
||||
}
|
||||
return q.db.UpsertAIModelPrices(ctx, seed)
|
||||
}
|
||||
|
||||
func (q *querier) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionCreate, rbac.ResourceAiSeat); err != nil {
|
||||
return false, err
|
||||
|
||||
@@ -6126,6 +6126,16 @@ func (s *MethodTestSuite) TestAIBridge() {
|
||||
db.EXPECT().DeleteOldAIBridgeRecords(gomock.Any(), t).Return(int64(0), nil).AnyTimes()
|
||||
check.Args(t).Asserts(rbac.ResourceAibridgeInterception, policy.ActionDelete)
|
||||
}))
|
||||
|
||||
s.Run("UpsertAIModelPrices", s.Mocked(func(db *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
db.EXPECT().UpsertAIModelPrices(gomock.Any(), gomock.Any()).Return(nil).AnyTimes()
|
||||
check.Args(json.RawMessage(`[]`)).Asserts(rbac.ResourceAiModelPrice, policy.ActionUpdate)
|
||||
}))
|
||||
|
||||
s.Run("GetAIModelPriceByProviderModel", s.Mocked(func(db *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
|
||||
db.EXPECT().GetAIModelPriceByProviderModel(gomock.Any(), gomock.Any()).Return(database.AiModelPrice{}, nil).AnyTimes()
|
||||
check.Args(database.GetAIModelPriceByProviderModelParams{}).Asserts(rbac.ResourceAiModelPrice, policy.ActionRead)
|
||||
}))
|
||||
}
|
||||
|
||||
func (s *MethodTestSuite) TestTelemetry() {
|
||||
|
||||
@@ -5,6 +5,7 @@ package dbmetrics
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
@@ -976,6 +977,14 @@ func (m queryMetricsStore) GetAIBridgeUserPromptsByInterceptionID(ctx context.Co
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAIModelPriceByProviderModel(ctx context.Context, arg database.GetAIModelPriceByProviderModelParams) (database.AiModelPrice, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAIModelPriceByProviderModel(ctx, arg)
|
||||
m.queryLatencies.WithLabelValues("GetAIModelPriceByProviderModel").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "GetAIModelPriceByProviderModel").Inc()
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.GetAPIKeyByID(ctx, id)
|
||||
@@ -5368,6 +5377,14 @@ func (m queryMetricsStore) UpdateWorkspacesTTLByTemplateID(ctx context.Context,
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
|
||||
start := time.Now()
|
||||
r0 := m.s.UpsertAIModelPrices(ctx, seed)
|
||||
m.queryLatencies.WithLabelValues("UpsertAIModelPrices").Observe(time.Since(start).Seconds())
|
||||
m.queryCounts.WithLabelValues(httpmw.ExtractHTTPRoute(ctx), httpmw.ExtractHTTPMethod(ctx), "UpsertAIModelPrices").Inc()
|
||||
return r0
|
||||
}
|
||||
|
||||
func (m queryMetricsStore) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) {
|
||||
start := time.Now()
|
||||
r0, r1 := m.s.UpsertAISeatState(ctx, arg)
|
||||
|
||||
@@ -11,6 +11,7 @@ package dbmock
|
||||
|
||||
import (
|
||||
context "context"
|
||||
json "encoding/json"
|
||||
reflect "reflect"
|
||||
time "time"
|
||||
|
||||
@@ -1682,6 +1683,21 @@ func (mr *MockStoreMockRecorder) GetAIBridgeUserPromptsByInterceptionID(ctx, int
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIBridgeUserPromptsByInterceptionID", reflect.TypeOf((*MockStore)(nil).GetAIBridgeUserPromptsByInterceptionID), ctx, interceptionID)
|
||||
}
|
||||
|
||||
// GetAIModelPriceByProviderModel mocks base method.
|
||||
func (m *MockStore) GetAIModelPriceByProviderModel(ctx context.Context, arg database.GetAIModelPriceByProviderModelParams) (database.AiModelPrice, error) {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "GetAIModelPriceByProviderModel", ctx, arg)
|
||||
ret0, _ := ret[0].(database.AiModelPrice)
|
||||
ret1, _ := ret[1].(error)
|
||||
return ret0, ret1
|
||||
}
|
||||
|
||||
// GetAIModelPriceByProviderModel indicates an expected call of GetAIModelPriceByProviderModel.
|
||||
func (mr *MockStoreMockRecorder) GetAIModelPriceByProviderModel(ctx, arg any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetAIModelPriceByProviderModel", reflect.TypeOf((*MockStore)(nil).GetAIModelPriceByProviderModel), ctx, arg)
|
||||
}
|
||||
|
||||
// GetAPIKeyByID mocks base method.
|
||||
func (m *MockStore) GetAPIKeyByID(ctx context.Context, id string) (database.APIKey, error) {
|
||||
m.ctrl.T.Helper()
|
||||
@@ -10090,6 +10106,20 @@ func (mr *MockStoreMockRecorder) UpdateWorkspacesTTLByTemplateID(ctx, arg any) *
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpdateWorkspacesTTLByTemplateID", reflect.TypeOf((*MockStore)(nil).UpdateWorkspacesTTLByTemplateID), ctx, arg)
|
||||
}
|
||||
|
||||
// UpsertAIModelPrices mocks base method.
|
||||
func (m *MockStore) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
|
||||
m.ctrl.T.Helper()
|
||||
ret := m.ctrl.Call(m, "UpsertAIModelPrices", ctx, seed)
|
||||
ret0, _ := ret[0].(error)
|
||||
return ret0
|
||||
}
|
||||
|
||||
// UpsertAIModelPrices indicates an expected call of UpsertAIModelPrices.
|
||||
func (mr *MockStoreMockRecorder) UpsertAIModelPrices(ctx, seed any) *gomock.Call {
|
||||
mr.mock.ctrl.T.Helper()
|
||||
return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UpsertAIModelPrices", reflect.TypeOf((*MockStore)(nil).UpsertAIModelPrices), ctx, seed)
|
||||
}
|
||||
|
||||
// UpsertAISeatState mocks base method.
|
||||
func (m *MockStore) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) {
|
||||
m.ctrl.T.Helper()
|
||||
|
||||
Generated
+24
-1
@@ -223,7 +223,10 @@ CREATE TYPE api_key_scope AS ENUM (
|
||||
'chat:*',
|
||||
'ai_seat:*',
|
||||
'ai_seat:create',
|
||||
'ai_seat:read'
|
||||
'ai_seat:read',
|
||||
'ai_model_price:*',
|
||||
'ai_model_price:read',
|
||||
'ai_model_price:update'
|
||||
);
|
||||
|
||||
CREATE TYPE app_sharing_level AS ENUM (
|
||||
@@ -1061,6 +1064,23 @@ BEGIN
|
||||
END;
|
||||
$$;
|
||||
|
||||
CREATE TABLE ai_model_prices (
|
||||
provider text NOT NULL,
|
||||
model text NOT NULL,
|
||||
input_price bigint,
|
||||
output_price bigint,
|
||||
cache_read_price bigint,
|
||||
cache_write_price bigint,
|
||||
created_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
updated_at timestamp with time zone DEFAULT now() NOT NULL,
|
||||
CONSTRAINT ai_model_prices_cache_read_price_check CHECK ((cache_read_price >= 0)),
|
||||
CONSTRAINT ai_model_prices_cache_write_price_check CHECK ((cache_write_price >= 0)),
|
||||
CONSTRAINT ai_model_prices_input_price_check CHECK ((input_price >= 0)),
|
||||
CONSTRAINT ai_model_prices_output_price_check CHECK ((output_price >= 0))
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_model_prices IS 'Per-model token prices used by AI Bridge to compute interception cost.';
|
||||
|
||||
CREATE TABLE ai_seat_state (
|
||||
user_id uuid NOT NULL,
|
||||
first_used_at timestamp with time zone NOT NULL,
|
||||
@@ -3358,6 +3378,9 @@ ALTER TABLE ONLY workspace_resource_metadata ALTER COLUMN id SET DEFAULT nextval
|
||||
ALTER TABLE ONLY workspace_agent_stats
|
||||
ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
|
||||
|
||||
ALTER TABLE ONLY ai_model_prices
|
||||
ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
|
||||
ALTER TABLE ONLY ai_seat_state
|
||||
ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
DROP TABLE IF EXISTS ai_model_prices CASCADE;
|
||||
@@ -0,0 +1,19 @@
|
||||
CREATE TABLE ai_model_prices (
|
||||
provider TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
-- Prices per million tokens, in micro-units (1 unit = 1,000,000).
|
||||
-- A NULL column means the price is unknown for this dimension; an explicit zero means "free".
|
||||
input_price BIGINT CHECK (input_price >= 0),
|
||||
output_price BIGINT CHECK (output_price >= 0),
|
||||
cache_read_price BIGINT CHECK (cache_read_price >= 0),
|
||||
cache_write_price BIGINT CHECK (cache_write_price >= 0),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
PRIMARY KEY (provider, model)
|
||||
);
|
||||
|
||||
COMMENT ON TABLE ai_model_prices IS 'Per-model token prices used by AI Bridge to compute interception cost.';
|
||||
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_model_price:*';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_model_price:read';
|
||||
ALTER TYPE api_key_scope ADD VALUE IF NOT EXISTS 'ai_model_price:update';
|
||||
@@ -0,0 +1,10 @@
|
||||
INSERT INTO ai_model_prices (
|
||||
provider,
|
||||
model,
|
||||
input_price,
|
||||
output_price,
|
||||
cache_read_price,
|
||||
cache_write_price
|
||||
) VALUES
|
||||
('anthropic', 'claude-3-5-sonnet-20241022', 3000000, 15000000, 300000, 3750000),
|
||||
('openai', 'gpt-4o', 2500000, 10000000, 1250000, NULL);
|
||||
@@ -227,6 +227,9 @@ const (
|
||||
ApiKeyScopeAiSeat APIKeyScope = "ai_seat:*"
|
||||
ApiKeyScopeAiSeatCreate APIKeyScope = "ai_seat:create"
|
||||
ApiKeyScopeAiSeatRead APIKeyScope = "ai_seat:read"
|
||||
ApiKeyScopeAiModelPrice APIKeyScope = "ai_model_price:*"
|
||||
ApiKeyScopeAiModelPriceRead APIKeyScope = "ai_model_price:read"
|
||||
ApiKeyScopeAiModelPriceUpdate APIKeyScope = "ai_model_price:update"
|
||||
)
|
||||
|
||||
func (e *APIKeyScope) Scan(src interface{}) error {
|
||||
@@ -473,7 +476,10 @@ func (e APIKeyScope) Valid() bool {
|
||||
ApiKeyScopeChat,
|
||||
ApiKeyScopeAiSeat,
|
||||
ApiKeyScopeAiSeatCreate,
|
||||
ApiKeyScopeAiSeatRead:
|
||||
ApiKeyScopeAiSeatRead,
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
@@ -689,6 +695,9 @@ func AllAPIKeyScopeValues() []APIKeyScope {
|
||||
ApiKeyScopeAiSeat,
|
||||
ApiKeyScopeAiSeatCreate,
|
||||
ApiKeyScopeAiSeatRead,
|
||||
ApiKeyScopeAiModelPrice,
|
||||
ApiKeyScopeAiModelPriceRead,
|
||||
ApiKeyScopeAiModelPriceUpdate,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4307,6 +4316,18 @@ type APIKey struct {
|
||||
AllowList AllowList `db:"allow_list" json:"allow_list"`
|
||||
}
|
||||
|
||||
// Per-model token prices used by AI Bridge to compute interception cost.
|
||||
type AiModelPrice struct {
|
||||
Provider string `db:"provider" json:"provider"`
|
||||
Model string `db:"model" json:"model"`
|
||||
InputPrice sql.NullInt64 `db:"input_price" json:"input_price"`
|
||||
OutputPrice sql.NullInt64 `db:"output_price" json:"output_price"`
|
||||
CacheReadPrice sql.NullInt64 `db:"cache_read_price" json:"cache_read_price"`
|
||||
CacheWritePrice sql.NullInt64 `db:"cache_write_price" json:"cache_write_price"`
|
||||
CreatedAt time.Time `db:"created_at" json:"created_at"`
|
||||
UpdatedAt time.Time `db:"updated_at" json:"updated_at"`
|
||||
}
|
||||
|
||||
type AiSeatState struct {
|
||||
UserID uuid.UUID `db:"user_id" json:"user_id"`
|
||||
FirstUsedAt time.Time `db:"first_used_at" json:"first_used_at"`
|
||||
|
||||
@@ -6,6 +6,7 @@ package database
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@@ -244,6 +245,7 @@ type sqlcQuerier interface {
|
||||
GetAIBridgeTokenUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeTokenUsage, error)
|
||||
GetAIBridgeToolUsagesByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeToolUsage, error)
|
||||
GetAIBridgeUserPromptsByInterceptionID(ctx context.Context, interceptionID uuid.UUID) ([]AIBridgeUserPrompt, error)
|
||||
GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error)
|
||||
GetAPIKeyByID(ctx context.Context, id string) (APIKey, error)
|
||||
// there is no unique constraint on empty token names
|
||||
GetAPIKeyByName(ctx context.Context, arg GetAPIKeyByNameParams) (APIKey, error)
|
||||
@@ -1239,6 +1241,10 @@ type sqlcQuerier interface {
|
||||
UpdateWorkspaceTTL(ctx context.Context, arg UpdateWorkspaceTTLParams) error
|
||||
UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.Context, arg UpdateWorkspacesDormantDeletingAtByTemplateIDParams) ([]WorkspaceTable, error)
|
||||
UpdateWorkspacesTTLByTemplateID(ctx context.Context, arg UpdateWorkspacesTTLByTemplateIDParams) error
|
||||
// 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.
|
||||
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)
|
||||
UpsertAnnouncementBanners(ctx context.Context, value string) error
|
||||
|
||||
@@ -1787,6 +1787,61 @@ func (q *sqlQuerier) UpdateAIBridgeInterceptionEnded(ctx context.Context, arg Up
|
||||
return i, err
|
||||
}
|
||||
|
||||
const getAIModelPriceByProviderModel = `-- name: GetAIModelPriceByProviderModel :one
|
||||
SELECT provider, model, input_price, output_price, cache_read_price, cache_write_price, created_at, updated_at
|
||||
FROM ai_model_prices
|
||||
WHERE provider = $1 AND model = $2
|
||||
`
|
||||
|
||||
type GetAIModelPriceByProviderModelParams struct {
|
||||
Provider string `db:"provider" json:"provider"`
|
||||
Model string `db:"model" json:"model"`
|
||||
}
|
||||
|
||||
func (q *sqlQuerier) GetAIModelPriceByProviderModel(ctx context.Context, arg GetAIModelPriceByProviderModelParams) (AiModelPrice, error) {
|
||||
row := q.db.QueryRowContext(ctx, getAIModelPriceByProviderModel, arg.Provider, arg.Model)
|
||||
var i AiModelPrice
|
||||
err := row.Scan(
|
||||
&i.Provider,
|
||||
&i.Model,
|
||||
&i.InputPrice,
|
||||
&i.OutputPrice,
|
||||
&i.CacheReadPrice,
|
||||
&i.CacheWritePrice,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const upsertAIModelPrices = `-- name: UpsertAIModelPrices :exec
|
||||
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($1::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()
|
||||
`
|
||||
|
||||
// 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.
|
||||
func (q *sqlQuerier) UpsertAIModelPrices(ctx context.Context, seed json.RawMessage) error {
|
||||
_, err := q.db.ExecContext(ctx, upsertAIModelPrices, seed)
|
||||
return err
|
||||
}
|
||||
|
||||
const getActiveAISeatCount = `-- name: GetActiveAISeatCount :one
|
||||
SELECT
|
||||
COUNT(*)
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
-- name: UpsertAIModelPrices :exec
|
||||
-- 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.
|
||||
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();
|
||||
|
||||
-- name: GetAIModelPriceByProviderModel :one
|
||||
SELECT *
|
||||
FROM ai_model_prices
|
||||
WHERE provider = @provider AND model = @model;
|
||||
@@ -7,6 +7,7 @@ type UniqueConstraint string
|
||||
// UniqueConstraint enums.
|
||||
const (
|
||||
UniqueAgentStatsPkey UniqueConstraint = "agent_stats_pkey" // ALTER TABLE ONLY workspace_agent_stats ADD CONSTRAINT agent_stats_pkey PRIMARY KEY (id);
|
||||
UniqueAiModelPricesPkey UniqueConstraint = "ai_model_prices_pkey" // ALTER TABLE ONLY ai_model_prices ADD CONSTRAINT ai_model_prices_pkey PRIMARY KEY (provider, model);
|
||||
UniqueAiSeatStatePkey UniqueConstraint = "ai_seat_state_pkey" // ALTER TABLE ONLY ai_seat_state ADD CONSTRAINT ai_seat_state_pkey PRIMARY KEY (user_id);
|
||||
UniqueAibridgeInterceptionsPkey UniqueConstraint = "aibridge_interceptions_pkey" // ALTER TABLE ONLY aibridge_interceptions ADD CONSTRAINT aibridge_interceptions_pkey PRIMARY KEY (id);
|
||||
UniqueAibridgeTokenUsagesPkey UniqueConstraint = "aibridge_token_usages_pkey" // ALTER TABLE ONLY aibridge_token_usages ADD CONSTRAINT aibridge_token_usages_pkey PRIMARY KEY (id);
|
||||
|
||||
Reference in New Issue
Block a user