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

  1. Fallback dispatch failure$provider->{$operationType}() used snake_case method names; ProviderProxy expects camelCase, causing AiOperationTypeMissingException on multi-word operation types.
  2. Hardcoded role restrictionadministrator/editor roles were hardcoded as the only allowed callers; editor is not a Drupal core role, blocking most sites.
  3. Missing config schemaquota.translate_output_ratio was read and written without a schema entry or install default, causing config:export diffs.
  4. Runtime counters in configpricing_sync.last_synced and pricing_sync.synced_count were written to config on every sync despite already being stored in State.
  5. Unsupported API parametermax_tokens: 1 was included in /v1/messages/count_tokens requests; the parameter is not part of that endpoint spec.
  6. Notification spamhook_ai_metering_quota_exceeded() fired on every over-quota call, not just the first occurrence per month.
  7. 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.
  8. PHPStan level 6 gaps — Missing @return array<int, array<string, mixed>> annotations in CostReporter and array_filter(…, 'strlen') callback in MeteringSettingsForm.

Solution

  1. Convert operation type to camelCase before dynamic dispatch: lcfirst(str_replace("_", "", ucwords($op, "_")))
  2. Replace role check with hasPermission("use ai_metering"); add permission to ai_metering.permissions.yml.
  3. Add translate_output_ratio: 1.10 to config/install/ai_metering.settings.yml and matching type: float entry to schema.
  4. Remove config writes from syncFromLitellm(); update getLastSynced()/getSyncedCount() to read from State. Add ai_metering_update_9006() to clear stale config on existing sites.
  5. Remove max_tokens from the count_tokens request body.
  6. Wrap invokeAll() in a State-keyed guard (ai_metering.quota_notified.{uid}.{Y-m}) — fires at most once per user per month.
  7. Declare ai_metering_chartjs in ai_metering.libraries.yml as an external library; add as dependency of ai_metering_dashboard; remove inline <script> tag from template.
  8. Fix docblock return types and replace string callback with typed closure.

Testing

Steps to verify (after fix):

  1. Enable module, assign use ai_metering permission to an authenticated role.
  2. Configure an Ollama fallback provider in AI Metering settings.
  3. Set a very low token quota (e.g. 1 token) for a test user.
  4. Trigger an ai_translate call — quota exceeded, Ollama fallback activates (no exception on translate_text).
  5. Trigger a second call — fallback activates again but hook_ai_metering_quota_exceeded does NOT fire a second time (check watchdog/email).
  6. Run drush config:export — no diff.
  7. Run drush ai-metering:sync-pricing — no config:export diff afterwards.
  8. 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

Merge request reports

Loading