Token Management¶
How Mozaiks measures, records, and surfaces LLM token usage across workflow runs.
Design Stance¶
Token tracking in Mozaiks has three runtime layers:
RuntimeUsageLedgerrecords factual LLM measurements.TokenWalletLedgerrecords provider-neutral token allocations, credits, debits, refunds, and balance projections.RuntimeTokenBudgetAlertLedgerrecords AG2 observer alerts for live run budget warnings and critical thresholds.
Neither layer owns payment providers, checkout, invoices, or hosted-product pricing. Subscription intent is declared in app/config/subscriptions.yaml; verified fulfillment is normalized into OSS BillingFulfillmentCommand records before it mutates subscription assignment state or token wallet balances. Payment collection remains app-owned or hosted-product behavior.
Token logic must not leak into workflow authoring or module business logic unless a workflow explicitly needs budget-aware behavior. Modules and payment adapters should call the wallet primitive after their own authorization or payment checks succeed.
Factory builds use factory_app/workflows/SubscriptionContractDesigner to produce the generated app's provider-neutral subscription/token contract. That workflow decides whether config/subscriptions.yaml should exist, what token_wallets and token_allowances are declared, which module actions need entitlement_gate, and which generated workflows should carry metering declarations. It does not create payment-provider resources or mutate token balances.
MozaiksPay is the recommended managed adapter path for hosted checkout and token top-ups. It is not the canonical owner of subscription state, entitlement state, token balances, usage records, or wallet ledgers. After MozaiksPay or another adapter verifies a payment/subscription fact, it submits a provider- neutral fulfillment command to the OSS runtime.
For the broader app monetization boundary, see Monetization Contract. The short rule is that OSS defines the subscription, token, entitlement, facade, and fulfillment contracts; hosted products or app-owned adapters verify money movement and submit the provider-neutral effects.
How Tokens Are Captured¶
AG2 Usage Middleware¶
MozaiksUsageMiddleware in mozaiksai/core/usage/middleware.py is a BaseMiddleware subclass registered with AG2's conversation machinery. On each LLM response, it extracts token counts from AG2's usage data and writes a usage event to the RuntimeUsageLedger.
# Factory helper — used at AG2 agent initialization
from mozaiksai.core.usage.middleware import build_ag2_usage_middleware
The middleware captures: - input_tokens / output_tokens / total_tokens - model identifier - app_id, tenant_id, workspace_id, user_id, chat_id from workflow context variables when present - estimated cost via mozaiksai/core/usage/pricing.py
Before each LLM call, the middleware runs TokenUsageGuard. The default preflight requirement is one token so a depleted wallet is blocked. Hosts or workflows can tighten this by setting one of these context variables:
token_preflight_required_tokenstoken_watchdog_required_tokenstoken_budget_required_tokens
The process-wide fallback is MOZAIKS_TOKEN_PREFLIGHT_REQUIRED_TOKENS.
AG2 Token Watchdog¶
mozaiksai/core/usage/watchdog.py attaches AG2's built-in TokenMonitor to runtime-created agents and adds a Mozaiks alert bridge observer. AG2 owns the observer mechanics and emits ObserverAlert events. Mozaiks records those alerts as chat.token_budget_alert events with app/run/user scope.
Thresholds resolve in this order:
- Run context:
token_watchdog_warn_tokens,token_watchdog_alert_tokens - Run context aliases:
token_budget_warn_tokens,token_budget_alert_tokens - Run context maximum:
token_budget_max_tokensfor the alert threshold - Environment defaults:
MOZAIKS_TOKEN_WATCHDOG_WARN_TOKENSMOZAIKS_TOKEN_WATCHDOG_ALERT_TOKENS
Set MOZAIKS_TOKEN_WATCHDOG_ENABLED=false to disable observer attachment. Watchdog alerts are live observability signals. They do not create invoices, grant plans, or replace token-wallet debits.
RuntimeUsageLedger¶
mozaiksai/core/usage/ledger.py owns the persistence layer.
from mozaiksai.core.usage import get_runtime_usage_ledger
ledger = get_runtime_usage_ledger()
await ledger.record_usage_delta(payload) # write a usage event
usage = await ledger.query_usage(app_id=..., user_id=..., limit=500) # read
Usage events are persisted to MongoDB under: - Database: SYSTEM_DATABASE (the shared runtime system database) - Collection: RuntimeUsageEvents
Indexes: event_id (unique), (app_id, event_ts), (app_id, user_id, event_ts), (app_id, chat_id).
Budget alerts are persisted separately under RuntimeTokenBudgetAlerts with the same app/user/chat indexing pattern.
TokenWalletLedger¶
mozaiksai/core/tokens/wallet.py owns provider-neutral token accounting.
from mozaiksai.core.tokens import get_token_wallet_ledger
ledger = get_token_wallet_ledger()
await ledger.credit(
app_id="app_1",
user_id="user_1",
wallet_id="ai_tokens",
amount=100000,
idempotency_key="payment:checkout_session_123",
)
await ledger.debit(
app_id="app_1",
user_id="user_1",
wallet_id="ai_tokens",
amount=1200,
idempotency_key="usage:usage_event_123",
)
balance = await ledger.query_balance(app_id="app_1", user_id="user_1")
Wallet entries are idempotent by key and persisted under:
RuntimeTokenWalletEntries— append-only operation recordsRuntimeTokenWalletBalances— projected balance per app/wallet/scope
Balances are scoped by app_id, wallet_id, and either user, tenant, or app scope. Debits reject by default when the balance is insufficient; wallets can explicitly allow negative balances for overage-style products.
Workspace context participates in active-plan resolution and usage event metadata. It does not create a separate wallet balance scope: token balances still follow each wallet's declared scope value (user, tenant, or app fallback).
BillingFulfillmentService¶
mozaiksai/core/billing/fulfillment.py owns the provider-neutral bridge from a verified billing fact to runtime state. It accepts BillingFulfillmentCommand objects from trusted adapters and applies deterministic effects:
subscription_activated/subscription_updated: upsert the configured assignment-store record and materialize any plan token allowances.subscription_cancelled: mark the assignment inactive/cancelled without deleting history.token_top_up_paid/token_credit_granted: credit the configured token wallet idempotently.refund_applied/chargeback_applied: debit or reject the reversal without hiding the failed clawback.
The platform ingress is:
The apply route requires an internal API key or billing-admin authorization. Generated app pages should not call it. Generated apps request checkout or top-up sessions through an app-owned billing facade; only a trusted adapter submits fulfillment after verification.
Generated app runtime acceptance tests cover this ingress as part of the full chain: POST /api/billing/fulfillment/apply writes the configured assignment_store, ConfiguredEntitlementAdapter grants the gated action, plan allowances appear in /api/me/tokens, and depleted wallets raise INSUFFICIENT_TOKENS before the LLM provider call.
summarize_usage_events¶
summarize_usage_events(events) in ledger.py aggregates a list of raw events into totals by model, returning:
{
"total_tokens": int,
"input_tokens": int,
"output_tokens": int,
"estimated_cost_usd": float,
"by_model": {...},
"event_count": int,
}
Querying Usage¶
Platform API Endpoint¶
Returns the current user's usage ledger entries plus declared subscription limits from app/config/subscriptions.yaml and token wallet summaries:
{
"events": [...],
"total_tokens": 12400,
"by_model": {"gpt-4o": {"total_tokens": 12400, "estimated_cost_usd": 0.18}},
"subscription_usage": {
"plan_id": "pro",
"limits": {...}
},
"token_budget_alerts": [
{
"severity": "warning",
"workflow_name": "AppGenerator",
"agent_name": "PlannerAgent",
"total_tokens": 50000
}
],
"token_wallets": {
"wallets": [
{
"wallet_id": "ai_tokens",
"balance": {"balance": 87600}
}
]
}
}
Authentication: requires bearer token via require_any_auth.
Additional token endpoints:
GET /api/me/tokens— current user's token wallet balancesPOST /api/me/tokens/sync— idempotently materialize current subscription allowancesGET /api/me/tokens/ledger?wallet_id=ai_tokens— current user's wallet ledger entries
Cost Estimation¶
mozaiksai/core/usage/pricing.py provides estimate_token_cost(model, tokens). Estimates are informational only — not authoritative billing data.
Production deployments should keep an operator-maintained JSON or YAML catalog. When ai-pricing/catalogs/usage-pricing.generated.json exists, the runtime uses it automatically unless MOZAIKS_USAGE_PRICING_CATALOG_PATH points somewhere else. Installed wheels also include the same generated catalog under the runtime package so source checkouts and package installs get the same default coverage. Mozaiks can generate that catalog from LiteLLM's maintained model-price reference:
python scripts\update_usage_pricing_catalog.py `
--output ai-pricing\catalogs\usage-pricing.generated.json
The GitHub workflow .github/workflows/update-usage-pricing-catalog.yml runs the same refresh weekly and opens a normal PR when provider reference prices change. AG2 intentionally does not maintain model prices; Mozaiks treats provider pricing as a runtime catalog concern so model launches and provider price changes do not require framework code changes.
The updater is intentionally CI/PR based instead of runtime-live fetching. This keeps cost estimates reproducible, avoids startup dependency on GitHub, and lets upstream schema changes fail in automation before they affect operators. Each generated catalog records the upstream revision plus a content SHA-256, and the updater fails if normalization produces an unexpectedly small catalog or a large row-count drop from the existing generated catalog.
Example:
schema_version: mozaiks.usage_pricing.v1
models:
gpt-5-nano:
input_per_1m_usd: 0.05
cached_input_per_1m_usd: 0.005
output_per_1m_usd: 0.40
claude-sonnet-4.6:
input_per_1k_usd: 0.003
cached_input_per_1k_usd: 0.0003
output_per_1k_usd: 0.015
default:
input_per_1k_usd: 0.001
cached_input_per_1k_usd: 0.0001
output_per_1k_usd: 0.004
Precedence:
- Provider/runtime-supplied explicit cost, if present in the usage event
- Model-specific env vars, such as
MOZAIKS_USAGE_GPT_5_NANO_INPUT_PER_1K_USD - Global env vars,
MOZAIKS_USAGE_INPUT_PER_1K_USDandMOZAIKS_USAGE_OUTPUT_PER_1K_USD MOZAIKS_USAGE_PRICING_OVERRIDE_PATHfor negotiated/custom provider ratesMOZAIKS_USAGE_PRICING_CATALOG_PATH, usually the generated LiteLLM catalog or the repo defaultai-pricing/catalogs/usage-pricing.generated.json- Built-in non-authoritative fallback table for known historical models
Operators that need negotiated rates, internal models, or temporary upstream patches should create a real override file and point MOZAIKS_USAGE_PRICING_OVERRIDE_PATH at it. The repo-local ai-pricing/catalogs/usage-pricing.overrides.json path is ignored by Git and excluded from package manifests so private rates do not get committed or published accidentally.
Override file shape:
{
"schema_version": "mozaiks.usage_pricing.v1",
"models": {
"custom-provider/private-model": {
"input_per_1m_usd": 0.5,
"cached_input_per_1m_usd": 0.05,
"output_per_1m_usd": 1.5
}
}
}
The usage ledger stores both estimated_cost_usd and cost_source on each event. Dashboards should show costs from catalog, estimated, or provided as configured estimates and treat default_table as local/dev fallback data. Set MOZAIKS_USAGE_PRICING_DISABLE_DEFAULT_CATALOG=true only for tests or diagnostics that need to prove behavior without the generated default catalog.
Pricing Health¶
/api/me/usage includes a pricing_health object derived from the event cost_source values and the active pricing catalogs:
{
"status": "ready",
"catalog_model_count": 2486,
"catalog_updated_at": "2026-07-11T05:41:51.909060+00:00",
"used_model_count": 2,
"unpriced_model_count": 0,
"priced_event_count": 18,
"unpriced_event_count": 0,
"coverage_percent": 100.0,
"cost_source_counts": {
"catalog": 18
}
}
Statuses:
ready— active catalog/override pricing covered the measured events.unpriced_models— one or more events hadcost_source: not_configured.fallback_prices— at least one event used the built-in fallback table.catalog_unavailable— a configured catalog path was missing or invalid.not_configured— no catalog, override, env rate, or measured usage exists.
Studio /usage and app-specific usage pages surface this health instead of showing silent $0.00 averages for unpriced model traffic.
Customer Markups and Billable Estimates¶
Provider prices and customer prices are different facts:
- Provider prices answer: "What did OpenAI, Anthropic, or another provider charge the operator?"
- Customer markups answer: "What does this app charge its users for that measured usage?"
Provider prices belong in the runtime usage-pricing catalog. Customer markups belong in the app's app/config/subscriptions.yaml because two apps can use the same model and charge users differently.
Example app-level policy:
schema_version: mozaiks.subscriptions.v1
label: Example SaaS
default_plan_id: pro
usage_charge_policies:
- meter_id: ai_tokens
label: AI usage
source: runtime_llm_usage
basis: provider_cost_usd
markup_percent: 35
minimum_charge_usd: 0
rounding: cent
plans:
- plan_id: pro
label: Pro
capabilities: [ai.chat]
basis: provider_cost_usd estimates customer charge from the runtime's provider cost estimate plus markup. basis: tokens uses a declared unit_price_usd_per_1k instead:
usage_charge_policies:
- meter_id: ai_tokens
label: AI usage
source: runtime_llm_usage
basis: tokens
unit_price_usd_per_1k: 0.01
markup_percent: 20
rounding: micro_usd
/api/me/usage enriches the current user's usage with billable_amount_usd when a runtime_llm_usage charge policy is configured. This is still an estimate. Payment provider prices, checkout sessions, invoices, refunds, tax, and settlement remain app-owned or hosted-product behavior.
Subscription Limits and Allowances¶
app/config/subscriptions.yaml (SaaS apps only) declares plan-level capability grants. The usage ledger data can be combined with these limits to drive UI quota displays or soft-enforcement gates via EntitlementPort.
Plans may also declare token wallets and allowances:
schema_version: mozaiks.subscriptions.v1
default_plan_id: pro
token_wallets:
- wallet_id: ai_tokens
label: AI tokens
unit: tokens
usage_meter_id: ai_tokens
scope: user
auto_debit_usage: true
usage_charge_policies:
- meter_id: ai_tokens
label: AI usage
source: runtime_llm_usage
basis: provider_cost_usd
markup_percent: 35
plans:
- plan_id: pro
label: Pro
capabilities: [ai.chat]
usage_limits:
- meter_id: ai_tokens
unit: tokens
monthly_limit: 100000
token_allowances:
- wallet_id: ai_tokens
amount: 100000
cadence: monthly
When auto_debit_usage is true, the runtime usage event dispatcher materializes the active plan allowance and debits the wallet from factual chat.usage_delta events. This is accounting, not payment processing. If the app declares assignment_store.workspace_id_field, the active plan is resolved with app/user/tenant/workspace scope before allowances are materialized.
Generated apps should treat this file as declarative infrastructure. They may display balances through /api/me/usage and /api/me/tokens, and may declare future metering intent for expensive module/workflow actions, but they must not create parallel usage or wallet collections.
Docker-Backed Runtime Smoke¶
Use this smoke when validating the full subscription/token path against a real MongoDB process. It applies a test fulfillment command, uses a stub LLM provider, and does not spend provider tokens.
docker start mozaiksai-mongo
python scripts\smoke_subscription_token_runtime_e2e.py `
--mongo-uri mongodb://localhost:27017/mozaiks_subscription_token_smoke `
--require-docker
The smoke verifies:
- factory-shaped
config/subscriptions.yamlloading - real Mongo subscription assignment lookup
- provider-neutral fulfillment into the assignment store and
RuntimeTokenWallet*collections - an allowed stub LLM boundary call while balance is sufficient
- idempotent usage debit
- depleted-balance denial before the provider client is invoked
The pytest wrapper is opt-in:
$env:MOZAIKS_RUN_SUBSCRIPTION_TOKEN_DOCKER_SMOKE="1"
$env:MONGO_URI="mongodb://localhost:27017/mozaiks_subscription_token_smoke"
pytest -q tests/test_subscription_token_runtime_real_mongo.py
The smoke creates a unique test database and cleans up its app data and runtime token wallet records before exit.
Local Manual Top-Up Smoke¶
For local/manual credit tests, use the same fulfillment service with source="test" or source="manual" and no payment secrets:
from mozaiksai.core.billing.fulfillment import (
BillingFulfillmentCommand,
BillingFulfillmentService,
)
service = BillingFulfillmentService(app_root="./my-app/app")
await service.apply_durable(
BillingFulfillmentCommand(
command_id="manual_top_up_001",
event_type="token_credit_granted",
source="test",
app_id="my-app",
user_id="local-user",
wallet_id="ai_tokens",
token_amount=10000,
)
)
Then verify:
This is the supported manual top-up path for OSS/local smoke testing. Do not add ad hoc wallet mutation endpoints or generated app-local ledger writes.
Where Not to Put Token Logic¶
- Do not put payment provider behavior in
RuntimeUsageLedgerorTokenWalletLedger. - Do not emit usage events from workflow tools directly — use the middleware.
- Do not hardcode model pricing — use
pricing.pyas the single source. - Do not put customer markups in the provider pricing catalog.
- Do not query the usage collection directly from module handlers — use
get_runtime_usage_ledger()or the/api/me/usageendpoint. - Do not create app-local token balance ledgers when the OSS token wallet ledger covers the use case.
Related Architecture¶
- API Reference —
/api/me/usageendpoint - Transport and Streaming
- AG2 Ownership Boundary