Issue #3593077: Code review hardening, permission gate, camelCase dispatch, config schema, State counters, API fix, notification dedup, Chart.js library
Summary
Addresses 8 issues identified in the initial code review of 1.0.x by camoa (#3593077).
Problem
- Fallback dispatch failure —
$provider->{$operationType}()used snake_case method names; ProviderProxy expects camelCase, causingAiOperationTypeMissingExceptionon multi-word operation types. - Hardcoded role restriction —
administrator/editorroles were hardcoded as the only allowed callers;editoris not a Drupal core role, blocking most sites. - Missing config schema —
quota.translate_output_ratiowas read and written without a schema entry or install default, causingconfig:exportdiffs. - Runtime counters in config —
pricing_sync.last_syncedandpricing_sync.synced_countwere written to config on every sync despite already being stored in State. - Unsupported API parameter —
max_tokens: 1was included in/v1/messages/count_tokensrequests; the parameter is not part of that endpoint spec. - Notification spam —
hook_ai_metering_quota_exceeded()fired on every over-quota call, not just the first occurrence per month. - CDN asset outside library system — Chart.js was loaded via a raw
<script src="cdn.jsdelivr.net/...">tag in the template, bypassing the Drupal library system. - PHPStan level 6 gaps — Missing
@return array<int, array<string, mixed>>annotations in CostReporter andarray_filter(…, 'strlen')callback in MeteringSettingsForm.
Solution
- Convert operation type to camelCase before dynamic dispatch:
lcfirst(str_replace("_", "", ucwords($op, "_"))) - Replace role check with
hasPermission("use ai_metering"); add permission toai_metering.permissions.yml. - Add
translate_output_ratio: 1.10toconfig/install/ai_metering.settings.ymland matchingtype: floatentry to schema. - Remove config writes from
syncFromLitellm(); updategetLastSynced()/getSyncedCount()to read from State. Addai_metering_update_9006()to clear stale config on existing sites. - Remove
max_tokensfrom the count_tokens request body. - Wrap
invokeAll()in a State-keyed guard (ai_metering.quota_notified.{uid}.{Y-m}) — fires at most once per user per month. - Declare
ai_metering_chartjsinai_metering.libraries.ymlas an external library; add as dependency ofai_metering_dashboard; remove inline<script>tag from template. - Fix docblock return types and replace string callback with typed closure.
Testing
Steps to verify (after fix):
- Enable module, assign
use ai_meteringpermission to an authenticated role. - Configure an Ollama fallback provider in AI Metering settings.
- Set a very low token quota (e.g. 1 token) for a test user.
- Trigger an
ai_translatecall — quota exceeded, Ollama fallback activates (no exception on translate_text). - Trigger a second call — fallback activates again but
hook_ai_metering_quota_exceededdoes NOT fire a second time (check watchdog/email). - Run
drush config:export— no diff. - Run
drush ai-metering:sync-pricing— no config:export diff afterwards. - Load admin dashboard — Chart.js loads from Drupal library, no inline CDN script.
Test coverage
Existing PHPUnit suite covers quota enforcement and event dispatch paths. Manual verification steps above cover the regression scenarios.
AI-Generated: Yes (Claude Sonnet — all PHP/YAML/Twig changes, reviewed and validated by @codeitwisely).
Edited by Jérôme Tchania