-
Notifications
You must be signed in to change notification settings - Fork 1.5k
feat(coderd): accumulate user daily AI spend on token usage #26741
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -86,9 +86,10 @@ type store interface { | |
| // ProviderConfigurator-related queries. InTx wraps the provider and key | ||
| // reads in a single read-only transaction; AcquireLock serializes against | ||
| // any in-flight env seed holding LockIDAIProvidersEnvSeed. | ||
| InTx(func(database.Store) error, *database.TxOptions) error | ||
| GetAIProviders(ctx context.Context, arg database.GetAIProvidersParams) ([]database.AIProvider, error) | ||
| GetAIProviderKeysByProviderIDs(ctx context.Context, providerIDs []uuid.UUID) ([]database.AIProviderKey, error) | ||
|
|
||
| InTx(func(database.Store) error, *database.TxOptions) error | ||
| } | ||
|
|
||
| type Server struct { | ||
|
|
@@ -301,36 +302,71 @@ func (s *Server) RecordTokenUsage(ctx context.Context, in *proto.RecordTokenUsag | |
| } | ||
|
|
||
| // Snapshot the effective group, per-token prices and compute cost. A | ||
| // missing price row or unbudgeted user yields NULL columns. | ||
| // missing price row or no effective group yields NULL columns. | ||
| cost, err := s.resolveTokenUsageCost(ctx, intc, in) | ||
| if err != nil { | ||
| return nil, xerrors.Errorf("resolve token usage cost: %w", err) | ||
| } | ||
|
|
||
| _, err = s.store.InsertAIBridgeTokenUsage(ctx, database.InsertAIBridgeTokenUsageParams{ | ||
| ID: uuid.New(), | ||
| InterceptionID: intcID, | ||
| ProviderResponseID: in.GetMsgId(), | ||
| InputTokens: in.GetInputTokens(), | ||
| OutputTokens: in.GetOutputTokens(), | ||
| CacheReadInputTokens: in.GetCacheReadInputTokens(), | ||
| CacheWriteInputTokens: in.GetCacheWriteInputTokens(), | ||
| Metadata: out, | ||
| CreatedAt: in.GetCreatedAt().AsTime(), | ||
| EffectiveGroupID: cost.effectiveGroupID, | ||
| InputPriceMicros: cost.inputPriceMicros, | ||
| OutputPriceMicros: cost.outputPriceMicros, | ||
| CacheReadPriceMicros: cost.cacheReadPriceMicros, | ||
| CacheWritePriceMicros: cost.cacheWritePriceMicros, | ||
| CostMicros: cost.costMicros, | ||
| }) | ||
| if err != nil { | ||
| return nil, xerrors.Errorf("insert token usage: %w", err) | ||
| if err := s.recordTokenUsageAndSpend(ctx, intc, cost, in, out); err != nil { | ||
| return nil, xerrors.Errorf("record token usage and spend: %w", err) | ||
| } | ||
|
|
||
| return &proto.RecordTokenUsageResponse{}, nil | ||
| } | ||
|
|
||
| // recordTokenUsageAndSpend atomically records the token usage (including the | ||
| // interception's cost) and, when the user is budgeted and the computed cost is | ||
| // positive, accumulates that cost into the user's daily spend. | ||
| func (s *Server) recordTokenUsageAndSpend(ctx context.Context, intc database.AIBridgeInterception, cost tokenUsageCost, in *proto.RecordTokenUsageRequest, metadataJSON []byte) error { | ||
| createdAt := in.GetCreatedAt().AsTime() | ||
| return s.store.InTx(func(tx database.Store) error { | ||
| if _, err := tx.InsertAIBridgeTokenUsage(ctx, database.InsertAIBridgeTokenUsageParams{ | ||
| ID: uuid.New(), | ||
| InterceptionID: intc.ID, | ||
| ProviderResponseID: in.GetMsgId(), | ||
| InputTokens: in.GetInputTokens(), | ||
| OutputTokens: in.GetOutputTokens(), | ||
| CacheReadInputTokens: in.GetCacheReadInputTokens(), | ||
| CacheWriteInputTokens: in.GetCacheWriteInputTokens(), | ||
| Metadata: metadataJSON, | ||
| CreatedAt: createdAt, | ||
| EffectiveGroupID: cost.effectiveGroupID, | ||
| InputPriceMicros: cost.inputPriceMicros, | ||
| OutputPriceMicros: cost.outputPriceMicros, | ||
| CacheReadPriceMicros: cost.cacheReadPriceMicros, | ||
| CacheWritePriceMicros: cost.cacheWritePriceMicros, | ||
| CostMicros: cost.costMicros, | ||
| }); err != nil { | ||
| return xerrors.Errorf("insert token usage: %w", err) | ||
| } | ||
|
|
||
| // Skip the spend update when there is no effective group or the interception has no cost. | ||
| if !cost.effectiveGroupID.Valid || !cost.costMicros.Valid || cost.costMicros.Int64 <= 0 { | ||
|
ssncferreira marked this conversation as resolved.
|
||
| s.logger.Debug(ctx, "skipping spend update", | ||
|
ssncferreira marked this conversation as resolved.
|
||
| slog.F("interception_id", intc.ID), | ||
| slog.F("initiator_id", intc.InitiatorID), | ||
| slog.F("has_effective_group", cost.effectiveGroupID.Valid), | ||
| slog.F("has_cost", cost.costMicros.Valid), | ||
| slog.F("cost_micros", cost.costMicros.Int64), | ||
| ) | ||
| return nil | ||
| } | ||
|
|
||
| if _, err := tx.IncrementUserAIDailySpend(ctx, database.IncrementUserAIDailySpendParams{ | ||
| UserID: intc.InitiatorID, | ||
| EffectiveGroupID: cost.effectiveGroupID.UUID, | ||
| // Day is derived from the record usage request CreatedAt | ||
|
ssncferreira marked this conversation as resolved.
|
||
| // so it matches the token usage row's created_at column. | ||
| Day: dbtime.StartOfDay(createdAt.UTC()), | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Initially, I was using a new clock for |
||
| CostMicros: cost.costMicros.Int64, | ||
| }); err != nil { | ||
| return xerrors.Errorf("increment user daily spend: %w", err) | ||
| } | ||
| return nil | ||
| }, nil) | ||
| } | ||
|
|
||
| func (s *Server) RecordPromptUsage(ctx context.Context, in *proto.RecordPromptUsageRequest) (*proto.RecordPromptUsageResponse, error) { | ||
| //nolint:gocritic // AIBridged has specific authz rules. | ||
| ctx = dbauthz.AsAIBridged(ctx) | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.