Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion coderd/aibridge/prices/prices.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ func SeedFromBytes(ctx context.Context, db database.Store, data []byte) error {
if len(rows) == 0 {
return xerrors.New("price seed is empty")
}
return db.UpsertAIModelPrices(ctx, data)
return db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: data,
Source: database.AIModelPriceSourceDefault,
})
}

func parseSeed(data []byte) ([]seedRow, error) {
Expand Down
98 changes: 82 additions & 16 deletions coderd/aibridge/prices/prices_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,24 @@ func TestSeedFromBytes(t *testing.T) {
require.Zero(t, gpt.CacheWritePrice.Int64)
})

t.Run("SeededPricesAreDefault", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, _ := dbtestutil.NewDB(t)

require.NoError(t, prices.SeedFromBytes(ctx, db, []byte(testSeedJSON)))

got, err := db.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{
Provider: "openai", Model: "gpt-4o",
})
require.NoError(t, err)
require.Equal(t, database.AIModelPriceSourceDefault, got.Source)
require.Equal(t, int64(2_500_000), got.InputPrice.Int64)
require.Equal(t, int64(10_000_000), got.OutputPrice.Int64)
require.Equal(t, int64(1_250_000), got.CacheReadPrice.Int64)
require.False(t, got.CacheWritePrice.Valid)
})

t.Run("Idempotent", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
Expand Down Expand Up @@ -108,14 +126,17 @@ func TestSeedFromBytes(t *testing.T) {
// cache_write_price is set to a non-NULL value here even though the
// embedded seed leaves it NULL for OpenAI; Seed must replace it with
// NULL to keep the table in sync with the seed.
require.NoError(t, db.UpsertAIModelPrices(ctx, []byte(`[{
"provider": "openai",
"model": "gpt-4o",
"input_price": 1,
"output_price": 2,
"cache_read_price": 3,
"cache_write_price": 4
}]`)))
require.NoError(t, db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: []byte(`[{
"provider": "openai",
"model": "gpt-4o",
"input_price": 1,
"output_price": 2,
"cache_read_price": 3,
"cache_write_price": 4
}]`),
Source: database.AIModelPriceSourceDefault,
}))
before, err := db.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{
Provider: "openai", Model: "gpt-4o",
})
Expand Down Expand Up @@ -143,14 +164,17 @@ func TestSeedFromBytes(t *testing.T) {

// Insert a row for a (provider, model) the seed doesn't cover. After
// Seed it should still be there with its values intact.
require.NoError(t, db.UpsertAIModelPrices(ctx, []byte(`[{
"provider": "test-provider",
"model": "test-model-not-in-seed",
"input_price": 12345,
"output_price": 67890,
"cache_read_price": null,
"cache_write_price": null
}]`)))
require.NoError(t, db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: []byte(`[{
"provider": "test-provider",
"model": "test-model-not-in-seed",
"input_price": 12345,
"output_price": 67890,
"cache_read_price": null,
"cache_write_price": null
}]`),
Source: database.AIModelPriceSourceDefault,
}))

require.NoError(t, prices.SeedFromBytes(ctx, db, []byte(testSeedJSON)))

Expand All @@ -162,6 +186,48 @@ func TestSeedFromBytes(t *testing.T) {
require.Equal(t, int64(67890), got.OutputPrice.Int64)
})

t.Run("LeavesCustomPricesUntouched", func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitShort)
db, _ := dbtestutil.NewDB(t)

// Price a model the seed also covers.
require.NoError(t, db.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: []byte(`[{
"provider": "openai",
"model": "gpt-4o",
"input_price": 1,
"output_price": 2,
"cache_read_price": 3,
"cache_write_price": 4
}]`),
Source: database.AIModelPriceSourceCustom,
}))
before, err := db.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{
Provider: "openai", Model: "gpt-4o",
})
require.NoError(t, err)
require.Equal(t, database.AIModelPriceSourceCustom, before.Source)
require.Equal(t, int64(1), before.InputPrice.Int64)
require.Equal(t, int64(2), before.OutputPrice.Int64)
require.Equal(t, int64(3), before.CacheReadPrice.Int64)
require.Equal(t, int64(4), before.CacheWritePrice.Int64)

// Re-applying the price book writes its own row and leaves this one be.
require.NoError(t, prices.SeedFromBytes(ctx, db, []byte(testSeedJSON)))

got, err := db.GetAIModelPriceByProviderModel(ctx, database.GetAIModelPriceByProviderModelParams{
Provider: "openai", Model: "gpt-4o",
})
require.NoError(t, err)
require.Equal(t, int64(1), got.InputPrice.Int64)
require.Equal(t, int64(2), got.OutputPrice.Int64)
require.Equal(t, int64(3), got.CacheReadPrice.Int64)
require.Equal(t, int64(4), got.CacheWritePrice.Int64)
require.Equal(t, database.AIModelPriceSourceCustom, got.Source)
require.Equal(t, before.UpdatedAt, got.UpdatedAt)
})

// Verifies the chain: AsAIBridged context -> dbauthz wrapper auth check
// -> subjectAibridged's permission grant. A missing or wrong action on
// the subject would surface as "unauthorized: rbac: forbidden" here, even
Expand Down
150 changes: 145 additions & 5 deletions coderd/aibridgedserver/aibridgedserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1731,7 +1731,6 @@ func TestRecordTokenUsage(t *testing.T) {
// No override
expectTokenUsageCostLookups(db, intc, nil, group, nil, price)

// input 300 + output 1200 + cache read 15 + cache write 40.
const wantCost int64 = 1555

db.EXPECT().InTx(gomock.Any(), nil).DoAndReturn(
Expand Down Expand Up @@ -2197,7 +2196,6 @@ func TestRecordTokenUsage(t *testing.T) {
func(fn func(database.Store) error, _ *database.TxOptions) error { return fn(db) },
)

// input 300 + output 1200 + cache read 15 + cache write 40.
const wantCost int64 = 1555

db.EXPECT().InsertAIBridgeTokenUsage(gomock.Any(), gomock.Cond(func(p database.InsertAIBridgeTokenUsageParams) bool {
Expand Down Expand Up @@ -2506,7 +2504,7 @@ func TestRecordTokenUsageAuthorized(t *testing.T) {
"cache_write_price": 4_000_000,
}})
require.NoError(t, err)
require.NoError(t, rawDB.UpsertAIModelPrices(ctx, priceSeed), "seed model prices")
require.NoError(t, rawDB.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{Seed: priceSeed, Source: database.AIModelPriceSourceDefault}), "seed model prices")

// The interception's provider name resolves to this provider, whose type keys
// the price lookup.
Expand Down Expand Up @@ -2559,7 +2557,6 @@ func TestRecordTokenUsageAuthorized(t *testing.T) {
require.Equal(t, sql.NullInt64{Int64: 6_000_000, Valid: true}, tokenUsage.OutputPriceMicros, "output price")
require.Equal(t, sql.NullInt64{Int64: 300_000, Valid: true}, tokenUsage.CacheReadPriceMicros, "cache read price")
require.Equal(t, sql.NullInt64{Int64: 4_000_000, Valid: true}, tokenUsage.CacheWritePriceMicros, "cache write price")
// input 300 + output 1200 + cache read 15 + cache write 40.
const wantCost int64 = 1555
require.Equal(t, sql.NullInt64{Int64: wantCost, Valid: true}, tokenUsage.CostMicros, "cost")

Expand All @@ -2577,6 +2574,149 @@ func TestRecordTokenUsageAuthorized(t *testing.T) {
require.Equal(t, wantCost, spend.SpendMicros, "spend micros")
}

// TestRecordTokenUsageModelPriceResolution covers which price an interception
// snapshots when a model carries a price from the embedded book, a price set
// through the API, or both.
func TestRecordTokenUsageModelPriceResolution(t *testing.T) {
t.Parallel()

const provider, model = "anthropic", "claude-sonnet-4-6"

priceSeed := func(input, output, cacheRead, cacheWrite int64) json.RawMessage {
seed, err := json.Marshal([]map[string]any{{
"provider": provider,
"model": model,
"input_price": input,
"output_price": output,
"cache_read_price": cacheRead,
"cache_write_price": cacheWrite,
}})
require.NoError(t, err)
return seed
}

tests := []struct {
name string
defaultSeed json.RawMessage
customSeed json.RawMessage
want database.AIBridgeTokenUsage
}{
{
name: "DefaultOnly",
defaultSeed: priceSeed(3_000_000, 6_000_000, 300_000, 4_000_000),
want: database.AIBridgeTokenUsage{
InputPriceMicros: sql.NullInt64{Int64: 3_000_000, Valid: true},
OutputPriceMicros: sql.NullInt64{Int64: 6_000_000, Valid: true},
CacheReadPriceMicros: sql.NullInt64{Int64: 300_000, Valid: true},
CacheWritePriceMicros: sql.NullInt64{Int64: 4_000_000, Valid: true},
// 100 input, 200 output, 50 cache read, and 10 cache write
// tokens, priced per million: 300 + 1200 + 15 + 40.
CostMicros: sql.NullInt64{Int64: 1555, Valid: true},
},
},
{
// A model the price book does not cover, priced through the API.
name: "CustomOnly",
customSeed: priceSeed(2_000_000, 4_000_000, 200_000, 1_000_000),
want: database.AIBridgeTokenUsage{
InputPriceMicros: sql.NullInt64{Int64: 2_000_000, Valid: true},
OutputPriceMicros: sql.NullInt64{Int64: 4_000_000, Valid: true},
CacheReadPriceMicros: sql.NullInt64{Int64: 200_000, Valid: true},
CacheWritePriceMicros: sql.NullInt64{Int64: 1_000_000, Valid: true},
// 100 input, 200 output, 50 cache read, and 10 cache write
// tokens, priced per million: 200 + 800 + 10 + 10.
CostMicros: sql.NullInt64{Int64: 1020, Valid: true},
},
},
{
name: "CustomWinsOverDefault",
defaultSeed: priceSeed(3_000_000, 6_000_000, 300_000, 4_000_000),
customSeed: priceSeed(9_000_000, 12_000_000, 900_000, 8_000_000),
want: database.AIBridgeTokenUsage{
InputPriceMicros: sql.NullInt64{Int64: 9_000_000, Valid: true},
OutputPriceMicros: sql.NullInt64{Int64: 12_000_000, Valid: true},
CacheReadPriceMicros: sql.NullInt64{Int64: 900_000, Valid: true},
CacheWritePriceMicros: sql.NullInt64{Int64: 8_000_000, Valid: true},
// 100 input, 200 output, 50 cache read, and 10 cache write
// tokens, priced per million: 900 + 2400 + 45 + 80.
CostMicros: sql.NullInt64{Int64: 3425, Valid: true},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
logger := testutil.Logger(t)

rawDB, _ := dbtestutil.NewDB(t)
authzDB := dbauthz.New(rawDB, rbac.NewStrictAuthorizer(prometheus.NewRegistry()), logger, coderdtest.AccessControlStorePointer())

org := dbgen.Organization(t, rawDB, database.Organization{})
user := dbgen.User(t, rawDB, database.User{})
dbgen.OrganizationMember(t, rawDB, database.OrganizationMember{OrganizationID: org.ID, UserID: user.ID})

if tt.defaultSeed != nil {
require.NoError(t, rawDB.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: tt.defaultSeed,
Source: database.AIModelPriceSourceDefault,
}), "seed model prices")
}
if tt.customSeed != nil {
require.NoError(t, rawDB.UpsertAIModelPrices(ctx, database.UpsertAIModelPricesParams{
Seed: tt.customSeed,
Source: database.AIModelPriceSourceCustom,
}), "set custom model price")
}

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,
ProviderName: aiProvider.Name,
Model: model,
}, nil)

srv, err := aibridgedserver.NewServer(ctx, aibridgedserver.Options{
Store: authzDB,
AISeatTracker: agplaiseats.Noop{},
AccessURL: "/",
GatewayCfg: codersdk.AIBridgeConfig{},
Experiments: requiredExperiments,
Logger: logger,
Clock: quartz.NewReal(),
})
require.NoError(t, err)

_, err = srv.RecordTokenUsage(ctx, &proto.RecordTokenUsageRequest{
InterceptionId: intc.ID.String(),
MsgId: "msg_price_resolution",
InputTokens: 100,
OutputTokens: 200,
CacheReadInputTokens: 50,
CacheWriteInputTokens: 10,
CreatedAt: timestamppb.New(time.Date(2026, 6, 25, 14, 30, 0, 0, time.UTC)),
})
require.NoError(t, err, "record token usage")

tokenUsages, err := rawDB.GetAIBridgeTokenUsagesByInterceptionID(ctx, intc.ID)
require.NoError(t, err)
require.Len(t, tokenUsages, 1)

tokenUsage := tokenUsages[0]
require.Equal(t, tt.want.InputPriceMicros, tokenUsage.InputPriceMicros, "input price")
require.Equal(t, tt.want.OutputPriceMicros, tokenUsage.OutputPriceMicros, "output price")
require.Equal(t, tt.want.CacheReadPriceMicros, tokenUsage.CacheReadPriceMicros, "cache read price")
require.Equal(t, tt.want.CacheWritePriceMicros, tokenUsage.CacheWritePriceMicros, "cache write price")
require.Equal(t, tt.want.CostMicros, tokenUsage.CostMicros, "cost")
})
}
}

// TestRecordTokenUsageProviderResolution covers provider resolution against a real
// database through dbauthz, where the live-row filter and name reuse apply.
func TestRecordTokenUsageProviderResolution(t *testing.T) {
Expand All @@ -2601,7 +2741,7 @@ func TestRecordTokenUsageProviderResolution(t *testing.T) {
{"provider": string(database.AIProviderTypeAzure), "model": gptModel, "input_price": azureInputPrice},
})
require.NoError(t, err)
require.NoError(t, rawDB.UpsertAIModelPrices(setupCtx, priceSeed), "seed model prices")
require.NoError(t, rawDB.UpsertAIModelPrices(setupCtx, database.UpsertAIModelPricesParams{Seed: priceSeed, Source: database.AIModelPriceSourceDefault}), "seed model prices")

srv, err := aibridgedserver.NewServer(setupCtx, aibridgedserver.Options{
Store: authzDB,
Expand Down
4 changes: 2 additions & 2 deletions coderd/database/dbauthz/dbauthz.go
Original file line number Diff line number Diff line change
Expand Up @@ -8851,11 +8851,11 @@ 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 {
func (q *querier) UpsertAIModelPrices(ctx context.Context, arg database.UpsertAIModelPricesParams) error {
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAiModelPrice); err != nil {
return err
}
return q.db.UpsertAIModelPrices(ctx, seed)
return q.db.UpsertAIModelPrices(ctx, arg)
}

func (q *querier) UpsertAISeatState(ctx context.Context, arg database.UpsertAISeatStateParams) (bool, error) {
Expand Down
5 changes: 4 additions & 1 deletion coderd/database/dbauthz/dbauthz_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6927,7 +6927,10 @@ func (s *MethodTestSuite) TestAIBridge() {

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)
check.Args(database.UpsertAIModelPricesParams{
Seed: json.RawMessage(`[]`),
Source: database.AIModelPriceSourceDefault,
}).Asserts(rbac.ResourceAiModelPrice, policy.ActionUpdate)
}))

s.Run("GetAIModelPriceByProviderModel", s.Mocked(func(db *dbmock.MockStore, _ *gofakeit.Faker, check *expects) {
Expand Down
5 changes: 2 additions & 3 deletions coderd/database/dbmetrics/querymetrics.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading