Skip to content

feat(budgets): scope budgets to request labels - #590

Merged
SantiagoDePolonia merged 5 commits into
mainfrom
feat/requests-labelling
Jul 25, 2026
Merged

feat(budgets): scope budgets to request labels#590
SantiagoDePolonia merged 5 commits into
mainfrom
feat/requests-labelling

Conversation

@SantiagoDePolonia

@SantiagoDePolonia SantiagoDePolonia commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Part of #319 — this covers the budgeting integration the issue asked for. Header-to-label ingestion already shipped; label filtering and grouping in the audit log remain open, as do conditional (if header X equals Y) tagging rules. Deliberately not auto-closing the issue.

Budgets could only limit a user_path subtree. An operator running several applications through one path — the exact case in the issue — had no way to cap their spend separately without minting a key per application.

What changed

A budget now carries a scope and a subject, mirroring the shape rate limit rules already use:

Scope Subject Matches
user_path /team/alpha that path and everything below it (unchanged)
label Mobile-App-iOS every request carrying that label

Labels are matched verbatim — no subtree semantics, no case folding — the same way the usage label filter and by-label breakdown already compare them. They come from the tagging headers and from the managed key that authenticated the request, so a request carrying several labels is charged against every matching label budget. A breach returns the usual 429 budget_exceeded, now naming the subject:

budget exceeded for label Mobile-App-iOS daily limit: spent 0.000001 of 0.000000

Surfaces:

  • Dashboard — a scope selector on the budget editor, a label chip on label rows
  • Admin APIscope + subject on /admin/budgets, with user_path kept as the shorthand spelling for user-path budgets
  • IaCbudgets.labels: in config.yaml
  • GET /v1/usage — label budgets appear in the caller's own status

Label budgets are deliberately YAML/admin-only: labels are matched verbatim and are not env-name safe. That is the same rationale model rate limit rules already use for skipping an env form.

Performance

Enforcement runs on every request and used to issue one SUM query per matching budget. Adding label budgets on top of that would have multiplied the query count, so SumUsageCost is replaced by a batched Store.SumSpend([]SpendWindow): one scan with conditional aggregation on SQL, one $group with conditional sums on MongoDB.

That makes the pre-existing user-path path faster too. Timed against a 50k-row usage table, full budget evaluation:

matching budgets before after
1 ~30 ms ~30 ms
5 96 ms 35 ms
25 443 ms 70 ms

Parity at one budget, ~6x at twenty-five. A unit test asserts the store is hit exactly once regardless of how many budgets match.

Migration

budgets is rekeyed to (scope, subject, period_seconds). Both SQL backends rebuild the table inside one transaction and MongoDB rewrites its documents in place — the same pattern ratelimit used for its own scope migration. The rebuild also absorbs the two columns older releases added with ALTER TABLE, so the separate migration list is gone.

Verified on a real SQLite database written in the old shape: rows preserved, old index dropped, and a label budget with the same subject spelling now coexists (the old primary key would have rejected it). tests/e2e/upgrade-compat.sh covers it going forward, and there is a migration test over both SQL dialects.

Cleanups along the way

  • Three near-identical evaluation loops in budget.Service folded into one match + evaluate pair; StatusesForPath becomes StatusesFor
  • SQL-only path helpers moved out of the shared store.go; per-store key validation deduplicated into normalizeBudgetKey
  • Fixed the budgets doc's admin API examples, which showed URL path parameters those endpoints never had

Verification

Live against a running gateway with a real provider:

  • label budget blocks its own label; a different label, a case-differing label, and an unlabelled request all pass
  • API-key labels enforce with no tagging header present
  • /v1/usage reports the label budget for the caller
  • pre-scope database migrates, reads back, and accepts new writes

Full Go suite, e2e, lint, 344 dashboard tests, swagger/openapi regenerated, dashboard bundle rebuilt.

Note

Response cache hits still return before budget enforcement, as documented. Worth knowing when validating budgets by hand — it briefly looked like a bug during testing.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added label-based budgets alongside user-path budgets, with exact (case-sensitive) label matching and composite scope/subject support.
    • Updated dashboard budget editor/listing and admin budget operations to use scope and subject.
    • Budget status/enforcement responses now include scope and subject (and evaluate matching budgets together using a single batched spend check).
  • Bug Fixes
    • Added/updated SQL and MongoDB migrations for upgrading existing budgets to scoped storage.
  • Documentation
    • Expanded budgets documentation and examples for label matching, configuration seeding, and the updated admin API request/response shapes.

Review round 1

Fixed — legacy MongoDB index blocked the migration (Greptile P1 / CodeRabbit Critical, independently found by both).

migratePreScopeDocuments unset user_path while the pre-scope unique index on (user_path, period_seconds) was still in place. A missing field indexes as null, so every migrated document sharing a period collapsed onto {user_path: null, period_seconds: N}. Reproduced against MongoDB 8:

E11000 duplicate key error ... index: user_path_1_period_seconds_1
dup key: { user_path: null, period_seconds: 86400 }

Startup would have aborted for any Mongo deployment holding two budgets with the same period — the ordinary case. Fixed by dropping the legacy index first, with a mongotest case that seeds two colliding pre-scope documents and asserts they survive.

internal/ratelimit has carried the identical bug since rule scopes shipped — that is where this migration was copied from. Reproduced there too and fixed alongside, with its own regression test.

Fixed — label propagation was untested at the server layer (CodeRabbit). The fakes were widened to budget.Subjects but nothing asserted labels actually reach the checker. Added a table-driven case covering labels with and without a bound user path.

Not changed, with reasons

  • Concurrent-startup race on the SQL rebuild (CodeRabbit Major) — the loser of the race fails and succeeds on restart. That is the documented, deliberate behaviour of the ratelimit migration this one mirrors; diverging in one of the two would be worse than the transient. Worth changing in both or neither.
  • subject + user_path both set is only rejected for label budgets (CodeRabbit Minor) — exact parity with the shipped rate_limits admin API. Same call: both or neither.
  • Stale-config DELETE parameter count (CodeRabbit Minor) — real, and I narrowed it: 3 binds per budget instead of 2 moves SQLite's old 999-parameter ceiling from ~500 to ~333 config-declared budgets. Far beyond any realistic declaration, and restructuring the NOT-IN delete to chunk safely is not worth it here.
  • Mongo SumSpend chunking parity (CodeRabbit Trivial) — SQL chunks for a bind-parameter limit that MongoDB does not have. Window count is bounded by matching budgets; the 16MB command limit is orders of magnitude away.
  • MCP_ENABLED in the generated spec (CodeRabbit out-of-scope warning) — three lines of pre-existing drift that make swagger corrected. CI enforces spec-vs-source drift, so reverting it would just re-break that check.

Review round 2

Fixed — the PostgreSQL label predicate was untested (CodeRabbit Major).

SumSpend's label branch is the one part written twice — json_each on SQLite, jsonb_exists on PostgreSQL — and only the SQLite half had a test, because that test opens a raw SQLite database to get a usage store. A mistake in the PostgreSQL predicate would have matched nothing silently: label budgets would simply never enforce on PG, with every test still green.

The case now runs on both backends. I confirmed it is not vacuous by corrupting the bound subject and watching only the PostgreSQL subtest fail.

usage has not moved onto sqlx, so its PostgreSQL store still takes a *pgxpool.Pool directly. sqlxtest grows an exported NewPostgresPool so a test spanning a migrated and an unmigrated store can point both at one throwaway schema.

The second half of that finding — that the SQLite seed inserts raw strings into usage.labels — is not accurate: entries go through the usage store, which serializes Labels to a valid JSON array. The assertions on label sums would fail otherwise.

Not changed, with reasons

  • Document label budgets in the /v1/usage swagger (Minor) — the response model is generated from usageStatusBudget, which now carries scope and subject; the regenerated spec in this PR already reflects them.
  • Scope in dashboard confirmation labels (Minor) — the confirm text names the subject and period. Scope belongs there for the same reason it is in the list chip; deferring only because it is cosmetic and this PR is already wide. Worth a follow-up.
  • spendBounds non-empty precondition (Trivial) — its only caller is SumSpend, which returns early on an empty slice.
  • Label-scope branch in usage_status_handler_test.go (Trivial) — the branch is three lines mirroring the admin handler, which this PR does cover with a label case.

Copilot AI review requested due to automatic review settings July 25, 2026 18:14

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@SantiagoDePolonia, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 9c3fff4d-c7d8-4357-a4ef-b7fe4c99e4ac

📥 Commits

Reviewing files that changed from the base of the PR and between d0921be and 93091f8.

⛔ Files ignored due to path filters (3)
  • internal/admin/dashboard/static/dist/assets/index--rzAzL2R.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-BCec-psC.css is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (13)
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • docs/openapi.json
  • internal/budget/store_sql_test.go
  • internal/storage/sqlx/sqlxtest/sqlxtest.go
  • web/dashboard/src/pages/budgets/BudgetList.svelte
  • web/dashboard/src/pages/budgets/BudgetsPage.svelte
  • web/dashboard/src/pages/budgets/budgets-helpers.js
  • web/dashboard/src/pages/overview/ProviderStatusSection.svelte
  • web/dashboard/src/pages/rate-limits/RateLimitInspector.svelte
  • web/dashboard/src/pages/rate-limits/RateLimitList.svelte
  • web/dashboard/src/styles/dashboard.css
  • web/dashboard/tests/budgets.test.js
📝 Walkthrough

Walkthrough

Budgets now support user_path and exact request-label scopes. Budget identities, configuration, enforcement, storage, admin APIs, dashboard forms, migrations, schemas, tests, and documentation use the new (scope, subject, period) model with batched spend evaluation.

Changes

Scoped budgeting

Layer / File(s) Summary
Budget domain and configuration
config/budget.go, internal/budget/*
Adds label budget configuration, scope-aware normalization and matching, structured budget identities, and batched spend-window contracts.
Enforcement and admin APIs
internal/admin/handler_budgets.go, internal/server/*, internal/budget/service.go
Propagates request labels into budget checks, resolves scoped admin requests, returns scoped status data, and evaluates matching budgets through one batched store call.
Database persistence and migration
internal/budget/store_*.go, internal/budget/*_test.go
Changes SQL and MongoDB budget keys to (scope, subject, period_seconds), migrates legacy records, and adds multi-window spend aggregation.
Dashboard, schemas, and documentation
web/dashboard/src/pages/budgets/*, docs/*, cmd/gomodel/docs/docs.go
Adds scope/subject budget forms, display and sorting behavior, updated API schemas, label-budget examples, and matching semantics documentation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

Suggested reviewers: copilot

Poem

I’m a rabbit with budgets, hopping label to path,
Counting each carrot in one batched math.
Scope in my satchel, subjects in view,
Old paths still work, while new labels do too.
Mongo and SQL keep the burrow aligned—
A tidy little limit for every kind.

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The OpenAPI/CLAUDE docs also add an unrelated MCP_ENABLED config flag, which is outside the budgeting/labeling scope. Split the MCP_ENABLED docs/spec regeneration into a separate change or remove it from this PR.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR delivers the budgeting portion of #319: label-scoped budgets, request-label matching, and API/dashboard/config/migration updates.
Title check ✅ Passed The title is concise and accurately highlights the main change: adding label-scoped budgets.
Description check ✅ Passed The description is detailed and covers the change, rationale, performance, migration, and verification, but it does not follow the repository's template headings.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/requests-labelling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@mintlify

mintlify Bot commented Jul 25, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟢 Ready View Preview Jul 25, 2026, 6:15 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@greptile-apps

greptile-apps Bot commented Jul 25, 2026

Copy link
Copy Markdown

Confidence Score: 3/5

This PR should not merge until the MongoDB upgrade migration drops or replaces the legacy unique index before removing user_path.

Existing MongoDB installations with multiple user paths sharing a budget period encounter a duplicate-key error during startup migration because all migrated documents lose the field used by the still-active legacy unique index.

Files Needing Attention: internal/budget/store_mongodb.go

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex ran the requested verification, but its local artifact references were not uploaded.
  • The pre-spend segment of the label-budget live flow is captured in label-budget-live-flow-01-before.log, showing the pre-spend GET status and successful exact-label request.
  • The post-spend segment is captured in label-budget-live-flow-02-after.log, showing all post-spend HTTP statuses, the exact 429 error body and Retry-After header, permitted nonmatching requests, upstream request-count assertion, and caller usage response.
  • The exact generated E2E harness used for both executions is documented in label-budget-live-flow-harness.go.

View all artifacts

T-Rex Ran code and verified through T-Rex

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Existing MongoDB budgets] --> B[Startup migration]
  B --> C[Add scope and subject]
  C --> D[Unset legacy user_path]
  D --> E[Drop legacy unique index]
  E --> F[Create scoped unique index]
Loading

Reviews (1): Last reviewed commit: "feat(budgets): scope budgets to request ..." | Re-trigger Greptile

@codecov-commenter

codecov-commenter commented Jul 25, 2026

Copy link
Copy Markdown

{Key: "scope", Value: string(ScopeUserPath)},
{Key: "subject", Value: "$user_path"},
}}},
bson.D{{Key: "$unset", Value: "user_path"}},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Legacy index blocks migration

When an existing MongoDB deployment has budgets for different user paths with the same period, this update unsets user_path while the legacy (user_path, period_seconds) unique index is still active. The migrated documents then share the same missing-path index key, causing UpdateMany to fail with a duplicate-key error and preventing the gateway from starting after upgrade.

T-Rex Ran code and verified through T-Rex

@coderabbitai coderabbitai 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.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/budget/store_sql_test.go (1)

190-256: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

SumSpend is only exercised on SQLite.

Both spend tests open a raw sqlite DB instead of going through sqlxtest.Run, so the PostgreSQL branch of spendSubjectMatch — notably jsonb_exists(labels, ?) and the non-unixepoch time comparison in sumSpendChunk — has no coverage at all. That is the highest-risk new SQL in this PR. Routing these through sqlxtest.Run (as TestSQLStoreMigratesPreScopeTable does) would exercise both engines wherever a Postgres instance is available.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store_sql_test.go` around lines 190 - 256, Update the spend
tests, including TestSQLStoreSumSpendHonorsSubjectBoundaryAndCacheType and the
other SumSpend test, to run through sqlxtest.Run like
TestSQLStoreMigratesPreScopeTable instead of opening a raw SQLite database. Keep
the existing setup and assertions within the harness so PostgreSQL executes
spendSubjectMatch and sumSpendChunk when available, while retaining SQLite
coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/admin/handler_budgets.go`:
- Around line 374-383: Update the subject/user_path validation around rawSubject
and rawUserPath so payloads containing both fields are rejected for every scope,
including budget.ScopeUserPath, instead of silently preferring subject. Preserve
the existing fallback that uses rawUserPath when subject is empty, and keep the
label-budget-specific error behavior where applicable.

In `@internal/budget/store_mongodb.go`:
- Around line 419-441: Update MongoDBStore.SumSpend to process windows in
spendChunkSize-sized batches, matching SQLStore.SumSpend, and execute a bounded
aggregation per chunk. Preserve the existing mongoSpendCondition,
spendTotalField, and spendHasUsageField accumulation behavior, then combine
chunk results into the same []Spend output and error semantics.
- Around line 59-76: Update migratePreScopeDocuments to drop the legacy
user_path_1_period_seconds_1 index before calling UpdateMany, keeping the drop
best-effort for fresh databases. Preserve the existing migration filter,
pipeline, and error wrapping, and remove the post-migration index drop.

In `@internal/budget/store_sql_migrate.go`:
- Around line 38-60: Update the migration error handling in the InTx callback
around the scoped-schema rebuild so a failed migration re-inspects the budgets
table schema and returns nil when another process has already completed the
migration. Preserve the existing error propagation for failures where the schema
is still unmigrated or cannot be inspected, allowing concurrent startup attempts
to converge.

In `@internal/budget/store_sql.go`:
- Around line 155-162: The stale-config delete construction around the budgets
loop must avoid exceeding SQLite’s parameter limit because each budget adds
three bind arguments. Chunk the budget conditions using the existing chunking
approach (such as slices.Chunk), execute the delete per chunk while preserving
the NOT-matching semantics, and retain the current behavior when budgets is
empty.

In `@internal/server/budget_support_test.go`:
- Around line 21-27: Add assertions covering Subjects.Labels in both affected
server tests: in internal/server/budget_support_test.go:21-27, extend a
request-labels case to verify countingBudgetChecker.subjects.Labels alongside
UserPath; in internal/server/usage_status_handler_test.go:31-41, assert
budgets.gotSubjects.Labels equals the labels attached to the request context.
Update the existing table-driven test cases without changing the fakes’
behavior.

---

Outside diff comments:
In `@internal/budget/store_sql_test.go`:
- Around line 190-256: Update the spend tests, including
TestSQLStoreSumSpendHonorsSubjectBoundaryAndCacheType and the other SumSpend
test, to run through sqlxtest.Run like TestSQLStoreMigratesPreScopeTable instead
of opening a raw SQLite database. Keep the existing setup and assertions within
the harness so PostgreSQL executes spendSubjectMatch and sumSpendChunk when
available, while retaining SQLite coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: ef845c64-da50-4195-b965-c361dd2401e9

📥 Commits

Reviewing files that changed from the base of the PR and between 8210cda and 7921c5f.

⛔ Files ignored due to path filters (3)
  • internal/admin/dashboard/static/dist/assets/index-BSE-2hNO.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-C5b8F5Yi.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (35)
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • config/budget.go
  • config/config.example.yaml
  • config/config_test.go
  • docs/features/budgets.mdx
  • docs/features/labelling.mdx
  • docs/openapi.json
  • internal/admin/handler_budgets.go
  • internal/admin/handler_budgets_test.go
  • internal/budget/factory.go
  • internal/budget/service.go
  • internal/budget/service_test.go
  • internal/budget/store.go
  • internal/budget/store_mongodb.go
  • internal/budget/store_mongodb_test.go
  • internal/budget/store_sql.go
  • internal/budget/store_sql_migrate.go
  • internal/budget/store_sql_test.go
  • internal/budget/types.go
  • internal/budget/types_test.go
  • internal/server/budget_support.go
  • internal/server/budget_support_test.go
  • internal/server/mcp_service_test.go
  • internal/server/usage_status_handler.go
  • internal/server/usage_status_handler_test.go
  • tests/e2e/budget_test.go
  • tests/e2e/upgrade-compat.sh
  • tests/integration/dbassert/budget.go
  • web/dashboard/src/pages/budgets/BudgetEditor.svelte
  • web/dashboard/src/pages/budgets/BudgetList.svelte
  • web/dashboard/src/pages/budgets/BudgetsPage.svelte
  • web/dashboard/src/pages/budgets/budgets-helpers.js
  • web/dashboard/src/pages/budgets/budgets.svelte.js
  • web/dashboard/tests/budgets.test.js

Comment on lines +374 to +383
rawSubject = strings.TrimSpace(rawSubject)
if rawSubject == "" {
if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
return "", "", errors.New("subject is required for label budgets; user_path only names user-path budgets")
}
rawSubject = rawUserPath
} else if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
// Silently dropping the conflicting field would mask authoring mistakes.
return "", "", errors.New("user_path must not be set alongside subject for label budgets")
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Conflicting subject + user_path is only rejected for label budgets.

For the default user_path scope, a payload carrying both fields silently drops user_path and keys off subject — the exact authoring mistake the label branch deliberately rejects. Since this identity feeds delete/reset, the silent winner can address the wrong budget.

🛡️ Proposed fix
 	rawSubject = strings.TrimSpace(rawSubject)
 	if rawSubject == "" {
 		if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
 			return "", "", errors.New("subject is required for label budgets; user_path only names user-path budgets")
 		}
 		rawSubject = rawUserPath
-	} else if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
+	} else if strings.TrimSpace(rawUserPath) != "" {
 		// Silently dropping the conflicting field would mask authoring mistakes.
-		return "", "", errors.New("user_path must not be set alongside subject for label budgets")
+		return "", "", errors.New("set either subject or user_path, not both")
 	}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
rawSubject = strings.TrimSpace(rawSubject)
if rawSubject == "" {
if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
return "", "", errors.New("subject is required for label budgets; user_path only names user-path budgets")
}
rawSubject = rawUserPath
} else if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
// Silently dropping the conflicting field would mask authoring mistakes.
return "", "", errors.New("user_path must not be set alongside subject for label budgets")
}
rawSubject = strings.TrimSpace(rawSubject)
if rawSubject == "" {
if scope != budget.ScopeUserPath && strings.TrimSpace(rawUserPath) != "" {
return "", "", errors.New("subject is required for label budgets; user_path only names user-path budgets")
}
rawSubject = rawUserPath
} else if strings.TrimSpace(rawUserPath) != "" {
// Silently dropping the conflicting field would mask authoring mistakes.
return "", "", errors.New("set either subject or user_path, not both")
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/admin/handler_budgets.go` around lines 374 - 383, Update the
subject/user_path validation around rawSubject and rawUserPath so payloads
containing both fields are rejected for every scope, including
budget.ScopeUserPath, instead of silently preferring subject. Preserve the
existing fallback that uses rawUserPath when subject is empty, and keep the
label-budget-specific error behavior where applicable.

Comment thread internal/budget/store_mongodb.go
Comment on lines +419 to +441
func (s *MongoDBStore) SumSpend(ctx context.Context, windows []SpendWindow) ([]Spend, error) {
if len(windows) == 0 {
return nil, nil
}
group := bson.D{{Key: "_id", Value: nil}}
for i, window := range windows {
condition, err := mongoSpendCondition(window)
if err != nil {
return nil, err
}
group = append(group,
bson.E{Key: spendTotalField(i), Value: bson.D{{Key: "$sum", Value: bson.D{{Key: "$cond", Value: bson.A{
condition,
bson.D{{Key: "$ifNull", Value: bson.A{"$total_cost", 0}}},
0,
}}}}}},
bson.E{Key: spendHasUsageField(i), Value: bson.D{{Key: "$sum", Value: bson.D{{Key: "$cond", Value: bson.A{
bson.D{{Key: "$and", Value: bson.A{condition, bson.D{{Key: "$gt", Value: bson.A{"$total_cost", nil}}}}}},
1,
0,
}}}}}},
)
}

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.

🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

No chunking parity with the SQL store.

SQLStore.SumSpend splits windows into spendChunkSize batches; here every window adds two accumulators to one $group stage, so a request matching many label budgets builds an unbounded pipeline. Consider chunking the windows the same way to bound pipeline size.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store_mongodb.go` around lines 419 - 441, Update
MongoDBStore.SumSpend to process windows in spendChunkSize-sized batches,
matching SQLStore.SumSpend, and execute a bounded aggregation per chunk.
Preserve the existing mongoSpendCondition, spendTotalField, and
spendHasUsageField accumulation behavior, then combine chunk results into the
same []Spend output and error semantics.

Comment on lines +38 to +60
// One transaction: a crash mid-rebuild must not leave the table renamed
// away, or the next startup would create a fresh empty budgets table and
// orphan every limit. If concurrent replicas race here, one commits and the
// others fail fast and see the migrated schema on restart.
return db.InTx(ctx, func(q sqlx.Querier) error {
statements := []string{
`ALTER TABLE budgets RENAME TO budgets_pre_scope`,
db.Dialect().ExpandTypes(sqlBudgetsSchema),
`INSERT INTO budgets (scope, subject, period_seconds, amount, source, last_reset_at, created_at, updated_at)
SELECT 'user_path', user_path, period_seconds, amount, ` + sourceExpr + `, ` + lastResetExpr + `, created_at, updated_at
FROM budgets_pre_scope`,
`DROP TABLE budgets_pre_scope`,
// SQLite carried the old index along with the renamed table;
// PostgreSQL drops it with the table, so this is a no-op there.
`DROP INDEX IF EXISTS idx_budgets_user_path`,
}
for _, statement := range statements {
if _, err := q.Exec(ctx, statement); err != nil {
return fmt.Errorf("migrate budgets to scoped schema: %w", err)
}
}
return nil
})

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.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Concurrent startup loses the race hard instead of converging.

On PostgreSQL (READ COMMITTED) two replicas can both observe user_path and both attempt the rebuild; the loser blocks on the lock and then fails (relation "budgets" does not exist / duplicate table), which propagates out of NewSQLStore and aborts startup. During a rolling deploy that turns into a crash-looping replica until it is restarted. Cheap fix: on error, re-inspect the columns and return nil if another process already migrated.

♻️ Converge instead of failing
-	return db.InTx(ctx, func(q sqlx.Querier) error {
+	err = db.InTx(ctx, func(q sqlx.Querier) error {
 		statements := []string{
 			`ALTER TABLE budgets RENAME TO budgets_pre_scope`,
 			db.Dialect().ExpandTypes(sqlBudgetsSchema),
@@
 		return nil
 	})
+	if err != nil {
+		// Another replica may have committed the same rebuild first.
+		if after, inspectErr := budgetColumns(ctx, db); inspectErr == nil && after["subject"] {
+			return nil
+		}
+		return err
+	}
+	return nil
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store_sql_migrate.go` around lines 38 - 60, Update the
migration error handling in the InTx callback around the scoped-schema rebuild
so a failed migration re-inspects the budgets table schema and returns nil when
another process has already completed the migration. Preserve the existing error
propagation for failures where the schema is still unmigrated or cannot be
inspected, allowing concurrent startup attempts to converge.

Comment on lines 155 to 162
if len(budgets) > 0 {
conditions := make([]string, 0, len(budgets))
for _, budget := range budgets {
conditions = append(conditions, `(user_path = ? AND period_seconds = ?)`)
args = append(args, budget.UserPath, budget.PeriodSeconds)
conditions = append(conditions, `(scope = ? AND subject = ? AND period_seconds = ?)`)
args = append(args, budget.Scope, budget.Subject, budget.PeriodSeconds)
}
query += ` AND NOT (` + strings.Join(conditions, " OR ") + `)`
}

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.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Parameter count in the stale-config delete now grows 3× per budget.

Each declared budget contributes three bind parameters, so the single DELETE exceeds SQLite's 999-parameter ceiling at ~333 config budgets (previously ~500). SumSpend explicitly chunks for this reason; this statement doesn't. Consider chunking with slices.Chunk here as well, or deleting by source = ? minus a temp/IN (VALUES ...) set.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store_sql.go` around lines 155 - 162, The stale-config delete
construction around the budgets loop must avoid exceeding SQLite’s parameter
limit because each budget adds three bind arguments. Chunk the budget conditions
using the existing chunking approach (such as slices.Chunk), execute the delete
per chunk while preserving the NOT-matching semantics, and retain the current
behavior when budgets is empty.

Comment on lines +21 to 27
subjects budget.Subjects
}

func (c *countingBudgetChecker) Check(_ context.Context, userPath string, _ time.Time) error {
func (c *countingBudgetChecker) Check(_ context.Context, subjects budget.Subjects, _ time.Time) error {
c.calls++
c.userPath = userPath
c.subjects = subjects
return nil

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Subjects.Labels is unasserted across the server-layer budget tests. Both fakes were widened from a string user path to budget.Subjects, but every assertion still reads only UserPath, so the new label-propagation contract has no test guarding it in this package.

  • internal/server/budget_support_test.go#L21-L27: assert checker.subjects.Labels in a case that sets request labels, not just UserPath.
  • internal/server/usage_status_handler_test.go#L31-L41: assert budgets.gotSubjects.Labels matches the labels attached to the request context.

As per path instructions for **/*_test.go: "Add or update table-driven tests for behavior changes".

📍 Affects 2 files
  • internal/server/budget_support_test.go#L21-L27 (this comment)
  • internal/server/usage_status_handler_test.go#L31-L41
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/budget_support_test.go` around lines 21 - 27, Add assertions
covering Subjects.Labels in both affected server tests: in
internal/server/budget_support_test.go:21-27, extend a request-labels case to
verify countingBudgetChecker.subjects.Labels alongside UserPath; in
internal/server/usage_status_handler_test.go:31-41, assert
budgets.gotSubjects.Labels equals the labels attached to the request context.
Update the existing table-driven test cases without changing the fakes’
behavior.

Source: Path instructions

SantiagoDePolonia and others added 2 commits July 25, 2026 20:27
Budgets could only limit a `user_path` subtree, so an operator running
several applications through one path had no way to cap their spend
separately without minting a key per application (issue #319).

A budget now carries a scope and a subject, mirroring the shape rate limit
rules already use:

- `user_path` — a path and its descendants, unchanged
- `label` — every request carrying that label, matched verbatim

Labels come from the tagging headers and from the managed key that
authenticated the request, so a request carrying several labels is charged
against every matching label budget. Label budgets are declared under
`budgets.labels:` in config.yaml or through the dashboard and admin API;
they have no env-var form, because labels are matched verbatim and are not
env-name safe (the same rationale model rate limit rules already use).

Enforcement no longer runs one SUM query per matching budget. `Store.SumSpend`
takes every matching window and answers them in one scan — conditional
aggregation on SQL, one `$group` on MongoDB. Timed against a 50k-row usage
table: parity at one matching budget (~30ms), 96ms -> 35ms at five, 443ms ->
70ms at twenty-five.

The budgets table is rekeyed to (scope, subject, period_seconds), so both SQL
backends rebuild it in one transaction and MongoDB rewrites its documents in
place. The rebuild also absorbs the two columns older releases added with
ALTER TABLE, which removes the separate migration list.

Also folds three near-identical evaluation loops in the budget service into
one match/evaluate pair, moves the SQL-only path helpers out of the shared
store file, and corrects the admin API examples in the budgets docs, which
showed URL path parameters those endpoints never had.

Part of #319 — this covers the budgeting integration the issue asked for.
Label filtering and grouping in the audit log remain open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The scope migration unset `user_path` while the pre-scope unique index on
(user_path, period_seconds) was still in place. A missing field indexes as
null, so every migrated document sharing a period collapsed onto the key
{user_path: null, period_seconds: N} and the rewrite failed with a duplicate
key error — aborting startup for any MongoDB deployment holding two budgets
with the same period, which is the ordinary case rather than an edge one.

`internal/ratelimit` has carried the same ordering since rule scopes shipped,
so it is fixed alongside. Both now drop the legacy index first, and both gain
a mongotest case that seeds two pre-scope documents sharing a period and
asserts they survive the upgrade.

Also asserts at the server layer that request labels reach the budget checker
with the user path, which is what lets a label budget match at all.

Reported by Greptile and CodeRabbit on #590.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 18:28
@SantiagoDePolonia
SantiagoDePolonia force-pushed the feat/requests-labelling branch from 7921c5f to d0921be Compare July 25, 2026 18:28

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai coderabbitai 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.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/budget/store_sql_test.go (1)

190-314: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Both SumSpend tests bypass sqlxtest.Run, so the PostgreSQL predicate is never exercised.

spendSubjectMatch emits a completely different label predicate per dialect (json_each vs jsonb_exists), yet these tests open raw SQLite only. The PostgreSQL branch of the new aggregation ships untested.

As per coding guidelines: "Add or update tests for behavior changes, using table-driven tests where appropriate. Cover request translation, response normalization, error handling, default configuration, and provider-specific parameter mapping."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store_sql_test.go` around lines 190 - 314, Update
TestSQLStoreSumSpendHonorsSubjectBoundaryAndCacheType and
TestSQLStoreSumSpendChunksLargeBatches to run through sqlxtest.Run so each case
executes against the configured dialects, including PostgreSQL. Preserve the
existing assertions and test data while ensuring the PostgreSQL-specific
spendSubjectMatch label predicate is exercised; apply the same harness change to
both SumSpend tests.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cmd/gomodel/docs/docs.go`:
- Around line 10178-10186: Update the `/v1/usage` endpoint description in the
generated documentation to state that the response may include matching
label-scoped budgets, in addition to statuses for the effective `user_path`,
consistent with the exposed `scope` and `subject` fields.

In `@internal/budget/store_sql.go`:
- Around line 316-338: Update label-budget tests in
internal/budget/store_sql_test.go (lines 190-314) to run through sqlxtest.Run,
ensuring both SQLite and PostgreSQL paths exercise spendSubjectMatch, including
the jsonb_exists(labels, ?) branch. Adjust SQLite seed data to use valid JSON
values in usage.labels rather than raw strings such as "iOS"; no direct change
is required in internal/budget/store_sql.go (lines 316-338) unless needed to
support the cross-database test coverage.

In `@internal/budget/store.go`:
- Around line 30-43: Document the non-empty precondition for spendBounds,
explicitly stating that callers must provide at least one SpendWindow because
the helper indexes windows[0]. Keep the existing range-union behavior unchanged.

In `@internal/server/usage_status_handler_test.go`:
- Around line 30-41: Extend the usage status handler tests to cover the
label-scope response branch: send request labels, assert the
fakeBudgetStatusChecker’s gotSubjects contains the translated labels, and verify
ScopeLabel results serialize scope and subject while omitting user_path. Use the
existing test structure and table-driven style where appropriate, including the
normal response behavior alongside this new case.

In `@web/dashboard/src/pages/budgets/budgets.svelte.js`:
- Line 244: Update the reset and delete confirmation labels in budgets.svelte.js
at lines 244-244 and 286-286 to include budgetScopeLabel(item) alongside the
subject and period. Update the override message in budgets-helpers.js at line
309-309 to include the selected budget’s scope, ensuring every budget
confirmation identifies subject, period, and scope.

---

Outside diff comments:
In `@internal/budget/store_sql_test.go`:
- Around line 190-314: Update
TestSQLStoreSumSpendHonorsSubjectBoundaryAndCacheType and
TestSQLStoreSumSpendChunksLargeBatches to run through sqlxtest.Run so each case
executes against the configured dialects, including PostgreSQL. Preserve the
existing assertions and test data while ensuring the PostgreSQL-specific
spendSubjectMatch label predicate is exercised; apply the same harness change to
both SumSpend tests.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: f05696e4-06a7-4f87-ad37-f99dc6dbe2ce

📥 Commits

Reviewing files that changed from the base of the PR and between 7921c5f and d0921be.

⛔ Files ignored due to path filters (3)
  • internal/admin/dashboard/static/dist/assets/index-BSE-2hNO.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/assets/index-C5b8F5Yi.js is excluded by !**/dist/**
  • internal/admin/dashboard/static/dist/index.html is excluded by !**/dist/**
📒 Files selected for processing (37)
  • CLAUDE.md
  • cmd/gomodel/docs/docs.go
  • config/budget.go
  • config/config.example.yaml
  • config/config_test.go
  • docs/features/budgets.mdx
  • docs/features/labelling.mdx
  • docs/openapi.json
  • internal/admin/handler_budgets.go
  • internal/admin/handler_budgets_test.go
  • internal/budget/factory.go
  • internal/budget/service.go
  • internal/budget/service_test.go
  • internal/budget/store.go
  • internal/budget/store_mongodb.go
  • internal/budget/store_mongodb_test.go
  • internal/budget/store_sql.go
  • internal/budget/store_sql_migrate.go
  • internal/budget/store_sql_test.go
  • internal/budget/types.go
  • internal/budget/types_test.go
  • internal/ratelimit/store_mongodb.go
  • internal/ratelimit/store_mongodb_test.go
  • internal/server/budget_support.go
  • internal/server/budget_support_test.go
  • internal/server/mcp_service_test.go
  • internal/server/usage_status_handler.go
  • internal/server/usage_status_handler_test.go
  • tests/e2e/budget_test.go
  • tests/e2e/upgrade-compat.sh
  • tests/integration/dbassert/budget.go
  • web/dashboard/src/pages/budgets/BudgetEditor.svelte
  • web/dashboard/src/pages/budgets/BudgetList.svelte
  • web/dashboard/src/pages/budgets/BudgetsPage.svelte
  • web/dashboard/src/pages/budgets/budgets-helpers.js
  • web/dashboard/src/pages/budgets/budgets.svelte.js
  • web/dashboard/tests/budgets.test.js

Comment thread cmd/gomodel/docs/docs.go
Comment on lines +10178 to +10186
"scope": {
"type": "string"
},
"spent": {
"type": "number"
},
"subject": {
"type": "string"
},

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.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Document label budgets in /v1/usage.

The endpoint description still says statuses are for the effective user_path only. Update it to explain that matching label-scoped budgets can also appear now that this response exposes scope and subject.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cmd/gomodel/docs/docs.go` around lines 10178 - 10186, Update the `/v1/usage`
endpoint description in the generated documentation to state that the response
may include matching label-scoped budgets, in addition to statuses for the
effective `user_path`, consistent with the exposed `scope` and `subject` fields.

Comment thread internal/budget/store_sql.go
Comment thread internal/budget/store.go
Comment on lines +30 to +43
// spendBounds returns the union of every window's time range, which is the
// range a batched spend query has to scan.
func spendBounds(windows []SpendWindow) (time.Time, time.Time) {
start, end := windows[0].Start, windows[0].End
for _, window := range windows[1:] {
if window.Start.Before(start) {
start = window.Start
}
if window.End.After(end) {
end = window.End
}
}
return start, end
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

State the non-empty precondition on spendBounds.

Both callers guard len(windows) == 0, but the helper panics on an empty slice and its doc comment does not say so. A one-line note (or an explicit guard) keeps a future caller from tripping it.

♻️ Proposed doc tweak
 // spendBounds returns the union of every window's time range, which is the
-// range a batched spend query has to scan.
+// range a batched spend query has to scan. Callers must pass at least one
+// window.
 func spendBounds(windows []SpendWindow) (time.Time, time.Time) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// spendBounds returns the union of every window's time range, which is the
// range a batched spend query has to scan.
func spendBounds(windows []SpendWindow) (time.Time, time.Time) {
start, end := windows[0].Start, windows[0].End
for _, window := range windows[1:] {
if window.Start.Before(start) {
start = window.Start
}
if window.End.After(end) {
end = window.End
}
}
return start, end
}
// spendBounds returns the union of every window's time range, which is the
// range a batched spend query has to scan. Callers must pass at least one
// window.
func spendBounds(windows []SpendWindow) (time.Time, time.Time) {
start, end := windows[0].Start, windows[0].End
for _, window := range windows[1:] {
if window.Start.Before(start) {
start = window.Start
}
if window.End.After(end) {
end = window.End
}
}
return start, end
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/budget/store.go` around lines 30 - 43, Document the non-empty
precondition for spendBounds, explicitly stating that callers must provide at
least one SpendWindow because the helper indexes windows[0]. Keep the existing
range-union behavior unchanged.

Comment on lines 30 to 41
type fakeBudgetStatusChecker struct {
results []budget.CheckResult
err error
gotPath string
results []budget.CheckResult
err error
gotSubjects budget.Subjects
}

func (f *fakeBudgetStatusChecker) Check(context.Context, string, time.Time) error { return nil }
func (f *fakeBudgetStatusChecker) Check(context.Context, budget.Subjects, time.Time) error { return nil }

func (f *fakeBudgetStatusChecker) StatusesForPath(_ context.Context, userPath string, _ time.Time) ([]budget.CheckResult, error) {
f.gotPath = userPath
func (f *fakeBudgetStatusChecker) StatusesFor(_ context.Context, subjects budget.Subjects, _ time.Time) ([]budget.CheckResult, error) {
f.gotSubjects = subjects
return f.results, f.err
}

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.

📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the label-scope response branch.

The fake now captures gotSubjects, but no test asserts that request labels reach StatusesFor, nor that a ScopeLabel result serializes with scope/subject and omits user_path (usage_status_handler.go lines 209-227). That branch is new behavior and currently untested.

As per coding guidelines: "Add or update tests for behavior changes, using table-driven tests where appropriate. Cover request translation, response normalization, error handling, default configuration, and provider-specific parameter mapping."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/server/usage_status_handler_test.go` around lines 30 - 41, Extend
the usage status handler tests to cover the label-scope response branch: send
request labels, assert the fakeBudgetStatusChecker’s gotSubjects contains the
translated labels, and verify ScopeLabel results serialize scope and subject
while omitting user_path. Use the existing test structure and table-driven style
where appropriate, including the normal response behavior alongside this new
case.

Source: Coding guidelines

return;
}
const label = String(item.user_path || "") + " " + budgetPeriodLabel(item);
const label = budgetSubject(item) + " " + budgetPeriodLabel(item);

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include scope in every budget confirmation label.

Budget identity now includes scope, but these messages display only subject and period. A user-path and label budget can therefore appear identical.

  • web/dashboard/src/pages/budgets/budgets.svelte.js#L244-L244: include budgetScopeLabel(item) in the reset confirmation.
  • web/dashboard/src/pages/budgets/budgets.svelte.js#L286-L286: include budgetScopeLabel(item) in the delete confirmation.
  • web/dashboard/src/pages/budgets/budgets-helpers.js#L309-L309: include the selected budget’s scope in the override message.
📍 Affects 2 files
  • web/dashboard/src/pages/budgets/budgets.svelte.js#L244-L244 (this comment)
  • web/dashboard/src/pages/budgets/budgets.svelte.js#L286-L286
  • web/dashboard/src/pages/budgets/budgets-helpers.js#L309-L309
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web/dashboard/src/pages/budgets/budgets.svelte.js` at line 244, Update the
reset and delete confirmation labels in budgets.svelte.js at lines 244-244 and
286-286 to include budgetScopeLabel(item) alongside the subject and period.
Update the override message in budgets-helpers.js at line 309-309 to include the
selected budget’s scope, ensuring every budget confirmation identifies subject,
period, and scope.

The label branch of SumSpend is the one part written twice — json_each on
SQLite, jsonb_exists on PostgreSQL — and only the SQLite half was tested,
because the test opened a raw SQLite database to get a usage store. A mistake
in the PostgreSQL predicate would have silently matched nothing, so label
budgets would never enforce there and no test would notice.

The case now runs on both backends. Verified it catches a broken predicate by
corrupting the bound subject and watching the PostgreSQL subtest fail.

`usage` has not moved onto sqlx, so its PostgreSQL store still takes a
*pgxpool.Pool directly; sqlxtest grows an exported NewPostgresPool so a test
spanning both can point them at one throwaway schema. Entry ids become fixed
UUIDs derived from their readable names, since PostgreSQL types usage.id as a
UUID.

Reported by CodeRabbit on #590.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 18:38

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

SantiagoDePolonia and others added 2 commits July 25, 2026 20:49
# Conflicts:
#	internal/admin/dashboard/static/dist/index.html
The budget list carried the scope twice: a "(tag) label" pill sat next to the
period, while the subject rendered in .budget-user-path regardless of what it
actually was. The pill said nothing the subject could not say itself, and the
class name was wrong for half the rows.

The tag icon now prefixes the label name inside the subject, and the shared
styling moves to .budget-scope-value with .budget-user-path and .budget-label
as hue-only modifiers. A label takes the same per-label colour the usage log,
breakdown chart, and API keys already use, so one label reads the same
everywhere. The rate limit list and inspector pick up the shared base class.

Also rounds the provider status toggle to 6px, matching .btn and
.table-action-btn rather than the full pill it was; the switch track and thumb
stay round. And the budget filter placeholder now mentions labels, which it can
now match on.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 25, 2026 19:01

Copilot AI 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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@mintlify

mintlify Bot commented Jul 25, 2026

Copy link
Copy Markdown

Preview deployment for your docs. Learn more about Mintlify Previews.

Project Status Preview Updated (UTC)
gomodel 🟡 Building Jul 25, 2026, 6:14 PM

💡 Tip: Enable Workflows to automatically generate PRs for you.

@SantiagoDePolonia
SantiagoDePolonia merged commit d8c4f79 into main Jul 25, 2026
27 of 28 checks passed
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