🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

feat: add ai_user_daily_spend table and queries - #26562

Merged
ssncferreira merged 7 commits into
mainfrom
ssncf/ai-user-daily-spend
Jul 2, 2026
Merged

feat: add ai_user_daily_spend table and queries#26562
ssncferreira merged 7 commits into
mainfrom
ssncf/ai-user-daily-spend

Conversation

@ssncferreira

@ssncferreira ssncferreira commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Description

Adds the spend tracking table and queries needed by AIGOV-427 (post-response accumulation) and AIGOV-428 (pre-request enforcement).

Changes

  • Add ai_user_daily_spend table to aggregate per-user, per-effective-group AI spend by UTC day.
  • Add UpsertUserAIDailySpend and GetUserAISpendSince queries.

Closes https://linear.app/codercom/issue/AIGOV-426/add-daily-spend-table-and-queries

Note

Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira

ssncferreira commented Jun 22, 2026

Copy link
Copy Markdown
Contributor Author

@datadog-coder

This comment has been minimized.

@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch from 0e9ce46 to 5375f15 Compare June 22, 2026 12:36
@linear-code

linear-code Bot commented Jun 22, 2026

Copy link
Copy Markdown

AIGOV-426

@ssncferreira

Copy link
Copy Markdown
Contributor Author

/coder-agents-review

@coder-agents-review

coder-agents-review Bot commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

Chat: Review posted | View chat
Requested: 2026-06-22 12:46 UTC by @ssncferreira
Spend: $23.50 / $100.00

Review history
  • R1 (2026-06-22): 11 reviewers, 3 Nit, 1 P2, 1 P4, COMMENT. Review

deep-review v0.9.0 | Round 1 | 335d6bd..5375f15

Last posted: Round 1, 5 findings (1 P2, 1 P4, 3 Nit), COMMENT. Review

Finding inventory

Findings

# Sev Status Location Summary Round Reviewer Posted
CRF-1 Nit Open aicostcontrol.sql:94 GetUserSpendSince omits effective_group_id from name R1 Netero Yes
CRF-2 P2 Open aicostcontrol.sql:88 Negative cost_micros silently reduces spend on existing rows R1 Hisoka P2, Knov P2, Mafuuu P3, Bisky P3 Yes
CRF-3 Nit Open aicostcontrol.sql:86 "Returns the resulting row" comment is redundant R1 Gon Yes
CRF-4 Nit Open aicostcontrol.sql:90 Inline comment restates SQL arithmetic R1 Gon Yes
CRF-5 P4 Open querier_test.go:11959 GetUserSpendSince no timezone normalization test for period_start R1 Bisky Yes

Round log

Round 1

Netero + Panel. 1 P2, 0 P3, 1 P4, 3 Nit. Reviewed against 335d6bd..5375f15.
Panel: Bisky, Hisoka, Mafu-san, Mafuuu, Pariston, Knuckle, Meruem, Komugi, Gon, Leorio, Knov.
Netero pre-panel: 1 Nit (naming). Panel proceeded.

About deep-review

CRF = Coder Review Finding (P0-P4, Nit, Note)

Reviewer Focus
Bisky tests
Chopper ops/errors
Churn-guard change verification
Ging language modernization
Gon naming
Hisoka edge cases
Killua perf
Kite change integrity
Knov contracts
Knuckle SQL
Komugi flake/determinism
Kurapika security
Law decomposition
Leorio docs
Luffy product
Mafu-san process
Mafuuu contracts
Melody dispatch/pairing
Meruem structural
Nami frontend
Netero mechanical checks
Pariston premise testing
Pen-botter product gaps
Razor verification
Robin duplication
Ryosuke Go arch
Takumi concurrency
Zoro shape

🤖 Managed by Coder Agents.

@coder-agents-review coder-agents-review Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean, well-scoped PR. The table design is sound: composite PK covers the enforcement query pattern, the upsert serializes correctly under READ COMMITTED, UTC normalization is tested at timezone boundaries, and the test density at 85.9% (372 test LOC for 61 production LOC) is thorough. Pariston tried to build a case against this change and couldn't.

Severity count: 1 P2, 1 P4, 3 Nit.

The single P2 is a convergent finding from six reviewers: the upsert's ON CONFLICT path accepts negative cost_micros, silently reducing accumulated spend on existing rows. Since this table drives budget enforcement, a caller bug passing a negative delta would corrupt the enforcement data. The fix is a one-line Go validation.

Notes from the panel worth knowing (not posted as findings):

  • The secondary index (effective_group_id, day) has no query consumer yet; it's a sound forward investment for group-level aggregation (AIGOV-428). (Knuckle)
  • GetUserSpendSince authz checks ActionRead on the user, not ActionReadPersonal. Any actor that can read the user can query spend in any group. For budget enforcement this is correct; for user-facing spend visibility, the API layer may need tighter scoping. (Mafuuu, Knov)
  • No retention/purge mechanism for ai_user_daily_spend. At moderate scale this is manageable, but the table grows unboundedly. Worth a dbpurge handler when the callers land. (Hisoka)
  • The authz wrapper for GetUserSpendSince requires the user to exist (GetUserByID), but the table preserves rows after user deletion. Post-deletion reporting would need a separate system-level query. (Knov)

"I tried to build a case against this change and couldn't. The problem is correctly understood, the solution is proportional, and the fix is at the right level." - Pariston

🤖 This review was automatically generated with Coder Agents.

-- The day parameter is normalized to its UTC calendar day before storage.
-- Returns the resulting row.
INSERT INTO ai_user_daily_spend (user_id, effective_group_id, day, spend_micros)
VALUES (@user_id, @effective_group_id, ((@day::timestamptz) AT TIME ZONE 'UTC')::date, @cost_micros)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 [CRF-2] The upsert's ON CONFLICT path does spend_micros = ai_user_daily_spend.spend_micros + EXCLUDED.spend_micros. CostMicros is int64 (signed). The table CHECK only fires when the result goes negative.

On INSERT (new row): negative cost_micros is rejected because the result itself is negative. Safe.

On UPDATE (existing row): if current spend_micros is 1000 and cost_micros is -500, the result is 500, which passes the CHECK. Spend is silently reduced.

The query comment says "Adds cost_micros to the spend" and the table drives budget enforcement (AIGOV-428). A caller bug passing a negative delta silently corrupts enforcement data: spend goes down, headroom goes up, budget appears unspent. No test covers negative cost_micros; all test values are non-negative.

Fix: validate CostMicros >= 0 in the Go dbauthz wrapper (or a shared helper) before calling the query. This produces a clear application error rather than relying on the schema-level floor. (Hisoka P2, Knov P2, Mafuuu P3, Bisky P3)

🤖

@ssncferreira ssncferreira Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is relevant, but dbauthz is the database-related authorization layer. This should be done at the application layer instead. The schema CHECK remains the data-integrity backstop.

Comment thread coderd/database/queries/aicostcontrol.sql Outdated
Comment thread coderd/database/queries/aicostcontrol.sql Outdated
spend_micros = ai_user_daily_spend.spend_micros + EXCLUDED.spend_micros
RETURNING *;

-- name: GetUserSpendSince :one

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit [CRF-1] The query filters on user_id, effective_group_id, and day >= period_start, but the name conveys only User and Since. A caller reading the name could expect spend across all groups. A name like GetUserSpendByEffectiveGroupSince would be more precise, matching the pattern of other queries in this file (GetGroupAIBudget, GetUserAIBudgetOverride). (Netero)

🤖

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both UserID and EffectiveGroupID are required in Params, so the filter scope is clear at every call site. I think the current name is enough, but happy to change it if there is agreement

Comment thread coderd/database/querier_test.go Outdated
@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch 4 times, most recently from f1439e4 to 1fb725d Compare June 23, 2026 16:10
Comment thread coderd/database/dbauthz/dbauthz.go Outdated
return database.AIUserDailySpend{}, xerrors.Errorf("cost_micros must be non-negative, got %d", arg.CostMicros)
}
// Daily spend writes are made by the aibridged process.
if err := q.authorizeContext(ctx, policy.ActionUpdate, rbac.ResourceAibridgeInterception); err != nil {

@ssncferreira ssncferreira Jun 23, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This operation is performed by the AI Bridge daemon, so we're checking that it has Update on AIBridgeInterception. The operation isn't actually modifying an interception, though. But seems the best approach with what we currently have.

Other alternatives:

  • Introduce a new resource for cost control. Would this make sense?
  • Follow the token usage pattern, where we validate that the actor matches the interception's initiator via authorizeAIBridgeInterceptionAction. This would require passing an interception_id here and at the database layer.

Thoughts?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since it's performed by the daemon, we'd expect the context to have subjectAibridged, so yeah I think a new resource is the most idiomatic approach here since it is indeed a new resource in the system.

@ssncferreira ssncferreira Jun 24, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@dannykopping We can do that, but my only concern is that for group budget and user overrides budget we use operations on Groups/Users resources, not on a specific AI Cost Control resource. Would this mean that for the upcoming changes on the list groups and list members (including the spend) pages, we should include read operations on this new resource? 🤔

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have any strong feelings here; it's a bit murky. Having a separate resource feels like the cleanest approach, but it'd require some refactoring of the existing budget code.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it is not entirely clear for me as well. We have two components for cost control:

  • Budget config (group budgets, user overrides): these are properties of User/Group resources, so authorizing on those entities makes sense, we're updating the entity itself.
  • Spend records: The write is clean, aibridged updates the user's spend, so a new resource fits. For the read we will have 2 actors:
    1. on the pre-request path aibridged reads the user's spend (resource-level makes sense)
    2. admin views the groups/members pages: this is the tricky one. These pages will include the user's spend, so this would mean that in order for admins to see this page, they need to have read operation on both Users/Groups and this new resource. Which means we would be coupling users/groups with spend. "Can read User X" would implicitly mean "can read X's spend".

As a result, for simplicity, I'll introduce a new ResourceAICostControl with just the Update action for now. Reads stay on the parent entities (User/Group). We might need to revisit this later. Let me know if this makes sense. @evgeniy-scherbina would also be good to have your opinion, since you have more context on the rbac side of things for cost control 🙂

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't mind to keep it as it is.

  • For Budget Config (Groups and UserOverrides) we treated it as extension of Groups and Users so we reused corresponding RBAC resources.
  • We can treat Spend records as extension of interception (because it's an aggregation of token_usages). I understand it's not perfect, but neither is adding new resource for this specific case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After internal discussion, we decided to keep the authorization check as is (Update on AIBridgeInterception) rather than creating a new resource. This check is enough for now, and it guarantees that only subjectAibridged and Owner roles can effectively increment user spend. We can revisit this later if needed.

@ssncferreira
ssncferreira marked this pull request as ready for review June 23, 2026 16:31
Comment thread coderd/database/queries/aicostcontrol.sql Outdated
Comment thread coderd/database/dbauthz/dbauthz.go Outdated
Comment thread coderd/database/querier_test.go
Comment thread coderd/database/querier_test.go
@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch 3 times, most recently from d174b59 to bb18cf7 Compare June 25, 2026 09:50
@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch 4 times, most recently from 888ac85 to dee171f Compare June 30, 2026 14:16
@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch 2 times, most recently from 6e8896d to 41ad5f2 Compare July 2, 2026 14:51
@ssncferreira
ssncferreira force-pushed the ssncf/ai-user-daily-spend branch from 41ad5f2 to ae35c71 Compare July 2, 2026 14:58

ssncferreira commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

Merge activity

  • Jul 2, 3:29 PM UTC: A user started a stack merge that includes this pull request via Graphite.
  • Jul 2, 3:29 PM UTC: @ssncferreira merged this pull request with Graphite.

@ssncferreira
ssncferreira merged commit fcdd029 into main Jul 2, 2026
29 of 30 checks passed
@ssncferreira
ssncferreira deleted the ssncf/ai-user-daily-spend branch July 2, 2026 15:29
ssncferreira added a commit that referenced this pull request Jul 2, 2026
## Description

Adds post-response spend accumulation to `RecordTokenUsage`.

## Changes

- Wrap the token usage insert and daily spend increment in a single transaction.
- Skip the spend update when the user is unbudgeted, the model is unpriced, or the computed cost is non-positive.

Depends on #26562

Closes https://linear.app/codercom/issue/AIGOV-427/add-post-response-spend-accumulation

> [!NOTE]
> Initially generated by Claude Opus 4.7, modified and reviewed by @ssncferreira
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants