feat(budget): per-user AI spending ceilings - #132
Conversation
Introduce tx_nrllm_user_budget keyed uniquely on be_user with six independent ceilings — requests, tokens, cost, each daily and monthly. Zero on any axis means unlimited on that axis. BudgetService::check(beUserUid, plannedCost) is a pre-flight check: fetches the user's budget row, aggregates actual usage from the existing tx_nrllm_service_usage on demand (no second-write per request, no counter drift), and returns a BudgetCheckResult naming the first bucket to trip. Daily checks take precedence over monthly. A user at exactly the limit is still allowed one more call because the incoming request's +1 / +plannedCost is added before comparison. This complements (does not replace) the existing per-configuration daily limits on tx_nrllm_configuration — configuration limits cap a preset, user budgets cap a person; both checks must pass. Like ADR-023 (capability permissions), this ADR ships the primitive only. Wiring BudgetService::check() into feature services is a deliberate follow-up to keep this PR narrow. See ADR-025 for rules, scope, and the counter-table alternative we ruled out. Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.Scanned FilesNone |
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #132 +/- ##
============================================
- Coverage 93.49% 93.02% -0.48%
- Complexity 2218 2259 +41
============================================
Files 89 92 +3
Lines 8150 8286 +136
============================================
+ Hits 7620 7708 +88
- Misses 530 578 +48
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
Adds per-backend-user budget ceilings (daily/monthly requests, tokens, and cost) as a new cost-control layer orthogonal to per-configuration limits, backed by on-demand aggregation from tx_nrllm_service_usage.
Changes:
- Introduces
tx_nrllm_user_budgettable + TCA + EN/DE labels for managing per-user ceilings in the TYPO3 backend. - Adds
UserBudgetmodel,UserBudgetRepository,BudgetService::check()andBudgetCheckResultto evaluate daily then monthly windows. - Adds unit tests covering
BudgetServicebehavior andBudgetCheckResultfactories/formatting, plus ADR-025 documentation.
Reviewed changes
Copilot reviewed 13 out of 13 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| ext_tables.sql | Adds schema for tx_nrllm_user_budget with unique be_user and daily/monthly ceiling fields. |
| Configuration/TCA/tx_nrllm_user_budget.php | Defines backend editing UI (tabs/palettes/fields) for user budget records. |
| Resources/Private/Language/locallang_tca.xlf | Adds EN labels for the new table/fields/tabs. |
| Resources/Private/Language/de.locallang_tca.xlf | Adds DE labels for the new table/fields/tabs. |
| Configuration/Services.yaml | Registers UserBudgetRepository and BudgetService as public services. |
| Classes/Domain/Model/UserBudget.php | Adds the domain model encapsulating budget ceiling fields and helper hasAnyLimit(). |
| Classes/Domain/Repository/UserBudgetRepository.php | Adds repository lookup by backend user; disables storage-page filtering. |
| Classes/Service/BudgetService.php | Implements the pre-flight budget check by aggregating usage and comparing against ceilings. |
| Classes/Domain/DTO/BudgetCheckResult.php | Adds a DTO for allowed/denied results with limit identifier + reason/current usage/limit values. |
| Tests/Unit/Service/BudgetServiceTest.php | Unit tests for daily/monthly evaluation order, limit tripping rules, and plannedCost handling. |
| Tests/Unit/Domain/DTO/BudgetCheckResultTest.php | Unit tests for factories, reason generation, and formatting behavior. |
| Documentation/Adr/Index.rst | Adds ADR-025 to ADR index. |
| Documentation/Adr/Adr025PerUserBudgets.rst | Documents the decision, rules, scope, and alternatives for per-user budgets. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Register UserBudget persistence mapping in Configuration/Extbase/
Persistence/Classes.php. Without this the repository was querying
a non-existent tx_nrllm_domain_model_userbudget (Extbase default
convention) instead of tx_nrllm_user_budget.
- Clamp negative plannedCost to 0 at BudgetService::check() entry.
A negative value could otherwise artificially reduce the projected
total and let callers bypass cost limits.
- Combine daily + monthly aggregates into ONE DB roundtrip via
conditional SUM() expressions. Replaces two per-request queries
with one when both windows are configured. `aggregateUsage` hook
renamed to `aggregateWindowUsage` with the new signature; tests
updated to stub the new method.
- Document the pre-flight race-condition (two concurrent requests
can both pass the check before either updates the usage table)
in the class docblock. Full serialisation would hot-path every
AI request, so it stays best-effort.
- BudgetCheckResult::denied() now renders a human-friendly reason
("AI budget exhausted: monthly cost is at ..."). Internal limit
identifier remains available as the stable machine key on
$exceededLimit.
- Backend labels for tx_nrllm_user_budget aligned with the existing
tx_nrllm_configuration style ("Max Requests/Day" instead of
"Max requests per day"). EN + DE updated.
Signed-off-by: Sebastian Mendel <github@sebastianmendel.de>
There was a problem hiding this comment.
Automated approval for maintainer PR
All automated quality gates passed. See SECURITY_CONTROLS.md for compensating controls.
Summary
Adds a new dimension of AI cost control: per-backend-user budgets independent of per-configuration limits.
tx_nrllm_user_budgetkeyed uniquely onbe_userwith six independent ceilings (requests / tokens / cost × daily / monthly;0= unlimited on that axis)UserBudgetdomain model +UserBudgetRepositoryBudgetService::check(beUserUid, plannedCost)as a pre-flight check — returnsBudgetCheckResultnaming which bucket tripped (if any)tx_nrllm_service_usage, so there's no second-write per request and no drift between countersResolution order
is_active = false→ allowedScope note
Ships the table + service + primitive. Wiring
BudgetService::check()into each feature service is a deliberate follow-up, same pattern as ADR-023.Relation to existing per-configuration limits
Orthogonal and complementary:
max_*_per_daycaps a presetTest plan
See ADR-025 for design rationale and the counter-table alternative we ruled out.