Issue #3586379: Persist token counts and show budget usage in CCC UI.
Description
Persists estimated token counts on context items and surfaces budget-relative usage in the CCC admin UI.
Token estimates use Drupal AI's @ai.tokenizer service (which uses yethee/tiktoken under the hood). The ai_context module does not declare a direct Composer dependency on tiktoken.
Persist token counts
- Adds a translatable, revisionable
token_countbase field onai_context_item, populated inpreSave()viaai_context.token_estimator. - Re-estimates only when the translation's
contentchanged (status-only or label-only saves keep the stored count). - Recalculates
token_countfor every translation on save. - Adds
ai_context_update_10015()to install the field and batch-backfill counts for all items and translations. The batch skips items deleted mid-run.
Token estimator and model-change recount
- Wires
AiContextTokenEstimatorto@ai.tokenizer(model fromai_context.settings:provider_config.model, fallbackgpt-4). - Replaces fixed 1K/3K listing tiers with budget-relative tiers:
- low: < 10% of budget
- mid: 10%–19.99%
- high: ≥ 20%
- Tooltip percentages use
floor()so they align with tier thresholds (e.g. 199/2000 → 9%, not 10%). - When the resolved tokenizer model changes, stored counts are backfilled via batch (web) or synchronously (CLI). Triggers include General settings form save, direct
ai_context.settingsconfig save, and configuration import (AiContextSettingsConfigSubscriber). Import defers backfill until request terminate so entity saves run after sync completes.
Context item listing
- Tokens column reads stored
getTokenCount()instead of re-estimating content on each row. - Badges and tooltips use budget-relative tiers and show percentage of the site token budget.
- Compact badge labels show raw counts below 1,000 (e.g.
17,120) andKnotation from 1,000 upward (e.g.2K,2–12Kfor parent ranges).
Renderer budget enforcement
- Enforces
max_tokensagainst the fully assembled output (blocks, separators, trailing newline), not per-block sums. - Uses token-aware snippet fitting via
TextChunkerwhen a provider model is configured (replaces char/4 approximation).
Agent subscription budget summary
- Adds
AiContextSubscriptionBudgetCalculator,AiContextSubscriptionBudgetSummary, andAiContextAgentBudgetSummaryLazyBuilder. - Agent settings form shows a budget summary at the top (
within,rangefor conditional subcontext, orover), loaded via a lazy builder at render time. - Summary totals use stored guidance content only. The form note explains that rendered context also adds metadata and separators, so runtime usage is usually higher.
- Calculator mirrors runtime selection rules:
- Excludes
never_includechildren from totals. - When conditional subcontext AI is disabled, conditional children are excluded unless
always_include. - Always-included conditional children count in the low bound, not only the high bound.
match_allwith empty scope subscriptions includes all published items (respectingnever_include).minimalwith empty subscriptions includes globals, unindexed items, target-entity matches, and non-subscription indexed scopes (parity with selector candidates).- Excludes inheriting children when their parent is not in the budget candidate set (MINIMAL parity).
- Excludes
- Caches summaries per agent configuration, module settings, scope-plugin configuration, and request context (scope context values, target entity, and request path) for one hour. Cache tags include
ai_context_item_list,config:ai_context.settings,config:ai_context.agents, and eachconfig:ai_context.scope_settings.*object. The lazy builder varies by target entity, content language, and URL path.
Services
ai_context.token_estimator— tokenizer injection and budget tiersai_context.token_count_backfill_scheduler— batch/sync backfill when the tokenizer model changesai_context.settings_config_subscriber— config save/import hooks for model-change recountai_context.subscription_budget— agent budget calculator (cached)ai_context.agent_budget_summary_lazy_builder— agent form budget summary lazy builder
No breaking API or hook changes.
Closes #3586379
Testing instructions
Automated
From the Drupal project root (with DDEV running):
cd web/modules/contrib/ai_context
./lint.sh
cd ../../../..
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextTokenEstimatorTest.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextUpdate10015Test.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextSubscriptionBudgetCalculatorTest.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextSubscriptionBudgetMinimalParityTest.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextRendererTest.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextSelectorEventsTest.php
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextItemTranslationTest.php --filter TokenCount
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextSettingsConfigValidationTest.php --filter TokenRecount
ddev phpunit web/modules/contrib/ai_context/tests/src/Kernel/AiContextSettingsConfigImportValidationTest.php --filter ImportRecalculates
ddev phpunit web/modules/contrib/ai_context/tests/src/Functional/AiContextItemListTest.php --filter testTokensColumn
ddev phpunit web/modules/contrib/ai_context/tests/src/Functional/AiContextAgentSettingsTest.php --filter testAgentContextBudgetSummaryCoverage map
| Area | Primary tests |
|---|---|
| Token tiers, persistence, content-unchanged save, compact labels | AiContextTokenEstimatorTest |
| Update backfill, deleted-item skip, model in batch | AiContextUpdate10015Test |
| Budget summary rules (within/over/range, match_all) | AiContextSubscriptionBudgetCalculatorTest |
| MINIMAL parity, cache invalidation, lazy builder, render overhead note | AiContextSubscriptionBudgetMinimalParityTest |
| Assembled-output budget, token-aware truncation | AiContextRendererTest |
| TEXT_RENDERED token recount alignment | AiContextSelectorEventsTest |
| Multilingual token_count | AiContextItemTranslationTest |
| Config save recount | AiContextSettingsConfigValidationTest |
| Config import recount | AiContextSettingsConfigImportValidationTest |
| Listing badges | AiContextItemListTest::testTokensColumn |
| Agent form budget UI | AiContextAgentSettingsTest::testAgentContextBudgetSummary |
Manual
- Check out this branch and run
ddev drush updb -y && ddev drush crso update10015backfillstoken_count. - Context listing (
/admin/config/ai/context/items): confirm the Tokens column shows compact counts (e.g.17,120,2K,2–12K) with color badges; hover for tooltip with token count and budget percentage. - General settings (
/admin/config/ai/context/settings/general): note the site Maximum tokens value (install default 1200). Change the AI provider/model and save; confirm a batch recount runs when items exist. - Agent settings (
/admin/config/ai/context/agents/{agent}): confirm the Context budget summary and the note that counts use stored content only:- Within budget for small subscriptions
- Range message when a subscribed parent has conditional subcontext children
- Exceeded message when subscribed tokens exceed the agent/site budget
- Edit and save a context item; confirm
token_countupdates when content changes and stays the same when only status/label changes. - Optional: set Conditional subcontext off at
/admin/config/ai/context/settings/itemsand confirm agent budget no longer treats conditional children as part of the high bound unless always-included. - Optional accessibility: confirm listing badge tooltips are exposed to assistive technology and the agent budget summary is readable in the tab order.
Checklist
- I have updated the MR title to use format:
Issue #1234: My issue title. - I have updated the MR description to include:
Closes #1234 - I have performed a self-review of my own code
- I have added or updated tests, or explained in the description why this change is not covered by tests
- I have updated documentation for any new or changed functionality or explained why they aren't
- I have written testing instructions and verified them locally
- I have noted any required post-merge steps (config imports, cache rebuilds, manual changes)
- This MR contains no breaking API or hook changes, or they are explicitly documented in the description
Post-merge: sites upgrading from before this MR must run drush updb (update 10015) and rebuild caches. No config import required.
If system.schema for ai_context is already 10015 but the token_count column is missing (partial upgrade), reset the schema to 10014 and re-run drush updb:
drush php:eval "\Drupal::keyValue('system.schema')->set('ai_context', 10014);"
drush updb -y
drush crDocumentation: docs/developers/services.md documents token_estimator, subscription_budget, model-change backfill, caching, lazy builder, and content-vs-render overhead. docs/developers/api-stability.md notes the internal services.
AI Compliance
Note
Check the one that best describes your usage, or leave all unchecked if AI was not significantly used.
-
AI Assisted Code
Mainly written by a human; AI used for autocomplete or partial generation under full human supervision. -
AI Generated Code
Mainly generated by AI, reviewed and approved by a human before this MR was created. -
Vibe Coded
Generated by AI and only functionally reviewed before this MR was created.