Skip to content

📊 feat: Real-Time Context Window & Token Usage Tracking#13670

Merged
danny-avila merged 30 commits into
devfrom
feat/context-usage-tracking
Jun 13, 2026
Merged

📊 feat: Real-Time Context Window & Token Usage Tracking#13670
danny-avila merged 30 commits into
devfrom
feat/context-usage-tracking

Conversation

@danny-avila

@danny-avila danny-avila commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

I implemented real-time context window and token usage tracking: a gauge beside the chat input shows tokens used vs. max context for the currently viewed message branch, with a hover breakdown of context composition (messages, system prompt, tool schemas, summary, free space), provider-reported input/output/cache totals, and opt-in session cost. The design mirrors the backend's existing token accounting instead of re-deriving it client-side, with three layered data sources that degrade gracefully: live per-model-call context snapshots, per-call usage events, and per-message tokenCount history with a server-resolved limits lookup.

  • Emit on_token_usage SSE events from ModelEndHandler per model call, carrying provider-reported input_tokens/output_tokens and cache details alongside the existing collectedUsage recording.
  • Forward on_context_usage snapshots (post-prune TokenBudgetBreakdown with effective budget, instruction overhead, and remaining context) emitted by @librechat/agents before each model call; the handler registration is guarded so it no-ops until @librechat/agents@3.2.35 ships and activates automatically once it does.
  • Removed the vestigial delete responseMessage.tokenCount in BaseClient so final events carry output token counts, and added tokenCount to tMessageSchema.
  • Added GET /api/endpoints/token-config: server-side resolution of context windows per configured model (including custom-endpoint tokenConfig overrides via a shared cache-key helper), with pricing rates included only when enabled — pattern matching against the pricing tables stays in one place.
  • Added interface.contextUsage (default on) to gate the gauge and interface.contextCost (default off) to gate cost display and pricing exposure.
  • Built the frontend on a module-level token index: O(n) build only on discrete cache replacements, O(depth) parent-chain walks for branch totals, per-conversation Jotai atoms split so throttled live-stream estimates re-render only the gauge — never the chat tree. Live estimates are calibrated by the contextMeta.calibrationRatio persisted on messages and corrected by provider-reported usage.
  • Persisted per-call token usage and the latest context snapshot in the generation job store (cross-replica safe; added the new fields to RedisJobStore deserialization), surfaced both through ResumeState, rebuilt client totals via backfill at sync, and re-seeded the live estimate from trailing aggregated content so revisiting an in-progress chat stays accurate.
  • Memoized buildTree by message-array identity in selectActiveBranchTail/getMessageBranchSiblingParentIds so multiple cache selectors share one tree build per snapshot.
  • Extended the e2e fake model to emit synthetic usage_metadata on a final chunk (OpenAI streaming pattern), exercising the full usage pipeline in mock runs.

Reuses the gauge ring and hover-card visual approach explored in #10958, rebuilt on branch-aware accounting with live streaming updates.

Dependency: full context-snapshot granularity requires @librechat/agents@3.2.35 (adds ON_CONTEXT_USAGE; branch ready in the agents repo). Until then the indicator runs on usage events and per-message token history.

Change Type

  • New feature (non-breaking change which adds functionality)

Testing

  • api: 349 tests pass (server/controllers/agents + BaseClient), including a new integration spec that drives the real Run.create → graph → ToolNode pipeline with a usage-emitting fake model across a two-call tool loop, asserting event payloads, ordering, collectedUsage parity, and resume persistence through GenerationJobManager.
  • packages/api: 578 endpoint tests + 36 stream unit tests pass, including new specs for buildTokenConfigMap resolution and usage/context resume persistence.
  • packages/data-provider: 1212 tests pass; packages/data-schemas: 1616 tests pass.
  • client: token index/branch-walk/cost unit tests (15) plus message-hook suites pass; tsc --noEmit clean.
  • Playwright mock suite: 49/49 pass, including a new spec that sends a message through the real agents pipeline, asserts the gauge fills from live SSE, verifies the breakdown popover, and confirms the gauge rebuilds from message history after reload.

Test Configuration:

  • Node.js v24.16.0, in-memory MongoDB / job store, mock Playwright harness (E2E_BASE_URL=http://localhost:3334 MOCK_LLM_PORT=8899 npm run e2e:mock).
  • Context-snapshot assertions verified against a local @librechat/agents@3.2.35 build; they self-skip on the published 3.2.33.

Checklist

  • My code adheres to this project's style guidelines
  • I have performed a self-review of my own code
  • I have commented in any complex areas of my code
  • My changes do not introduce new warnings
  • I have written tests demonstrating that my changes are effective or that my feature works
  • Local unit tests pass with my changes

Copilot AI review requested due to automatic review settings June 11, 2026 03:27
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds end-to-end, branch-aware context window + token usage (and optional cost) tracking by streaming backend-calculated snapshots/usage over SSE, persisting them for resumable streams, and rendering a compact gauge + breakdown UI beside the chat input.

Changes:

  • Stream and persist per-call token usage (on_token_usage) and per-call context snapshots (on_context_usage) for live display + resume.
  • Add server-resolved model token config endpoint (GET /api/endpoints/token-config) with optional pricing gated by interface config.
  • Implement client-side token index + per-conversation Jotai state, and a new input-adjacent gauge UI with breakdown and tests/e2e coverage.

Reviewed changes

Copilot reviewed 44 out of 44 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/data-schemas/src/methods/index.ts Re-export createTxMethods from methods barrel.
packages/data-schemas/src/index.ts Expose createTxMethods at package root.
packages/data-provider/src/types/runs.ts Add UsageEvents + usage/context event payload types.
packages/data-provider/src/types/agents.ts Extend run/resume types with collectedUsage + contextUsage.
packages/data-provider/src/types.ts Add tokenomics + token-config map types.
packages/data-provider/src/schemas.ts Allow tokenCount on messages via zod schema.
packages/data-provider/src/keys.ts Add React Query key for tokenConfig.
packages/data-provider/src/data-service.ts Add getTokenConfig() data-service call.
packages/data-provider/src/config.ts Add interface.contextUsage and interface.contextCost flags/defaults.
packages/data-provider/src/api-endpoints.ts Add /api/endpoints/token-config endpoint helper.
packages/api/src/stream/interfaces/IJobStore.ts Add serialized fields for context snapshot + token-usage replay.
packages/api/src/stream/implementations/RedisJobStore.ts Deserialize persisted contextUsage/tokenUsage.
packages/api/src/stream/GenerationJobManager.ts Persist latest context snapshot + per-call usage for resume.
packages/api/src/stream/tests/usageResume.spec.ts Add unit tests for usage/context resume state.
packages/api/src/endpoints/pricing.ts Implement buildTokenConfigMap() for context/pricing resolution.
packages/api/src/endpoints/pricing.spec.ts Add tests for token-config map resolution behavior.
packages/api/src/endpoints/index.ts Export new pricing/token-config utilities.
packages/api/src/endpoints/custom/initialize.ts Factor out getTokenConfigKey() for user-scoped caching.
e2e/specs/mock/usage.spec.ts Add Playwright mock test for live gauge + reload fallback.
e2e/setup/fake-model.js Emit synthetic provider-style usage_metadata in mock streaming.
client/src/utils/tokens.ts Add module-level token index + branch summation/cost helpers.
client/src/utils/tokens.spec.ts Add unit tests for token index, estimation, and formatting.
client/src/utils/messages.ts Memoize buildTree by message-array identity to avoid repeat builds.
client/src/utils/index.ts Export token utilities from client utils barrel.
client/src/store/usage.ts Add per-conversation Jotai atom families for usage/snapshots/live estimates.
client/src/store/index.ts Export usage store module and reorder imports.
client/src/locales/en/translation.json Add i18n strings for context/cost/usage breakdown UI.
client/src/hooks/SSE/useUsageHandler.ts Add SSE-driven imperative writers for usage/context atoms + token index.
client/src/hooks/SSE/useSSE.ts Wire on_context_usage/on_token_usage events into the SSE flow.
client/src/hooks/SSE/useResumableSSE.ts Restore usage/context/live-estimates from resume state and replay events.
client/src/hooks/SSE/index.ts Export useUsageHandler.
client/src/hooks/Chat/useTokenUsage.ts Build a view-model for gauge rendering using snapshots/estimates.
client/src/hooks/Chat/useTokenLimits.ts Resolve max context + rates via conversation/agent/spec/token-config.
client/src/hooks/Chat/index.ts Export new token usage/limits hooks.
client/src/data-provider/Endpoints/queries.ts Add useTokenConfigQuery() (infinite stale time).
client/src/components/Chat/Input/TokenUsage/index.tsx Add gauge trigger + hover breakdown, gated by interface config.
client/src/components/Chat/Input/TokenUsage/Gauge.tsx Render ring gauge with thresholds + indeterminate mode.
client/src/components/Chat/Input/TokenUsage/Breakdown.tsx Render breakdown popover (context composition, usage, cost).
client/src/components/Chat/Input/ChatForm.tsx Mount TokenUsage next to chat input controls.
api/server/routes/endpoints.js Register /api/endpoints/token-config route.
api/server/controllers/TokenConfigController.js Implement token-config controller with custom endpoint overrides + pricing gate.
api/server/controllers/agents/callbacks.js Emit per-call token usage and forward (guarded) context snapshots.
api/server/controllers/agents/tests/usageEvents.integration.spec.js Add integration coverage for usage/context events + persistence.
api/app/clients/BaseClient.js Stop deleting responseMessage.tokenCount so output token counts propagate.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1169 to +1173

const previousWrite = this.replayEventWriteQueues.get(streamId) ?? Promise.resolve();
await this.jobStore.updateJob(streamId, {
contextUsage: JSON.stringify((event as { data?: unknown }).data ?? null),
});
}
Comment on lines 1168 to 1172
runCode: z.boolean().optional(),
webSearch: z.boolean().optional(),
contextUsage: z.boolean().optional(),
contextCost: z.boolean().optional(),
peoplePicker: z
Comment thread client/src/store/usage.ts
Comment on lines +53 to +60
/** Jotai atomFamily entries are never GC'd — call on conversation switch/cleanup */
export function removeUsageAtoms(conversationId: string): void {
branchTotalsFamily.remove(conversationId);
contextSnapshotFamily.remove(conversationId);
usageTotalsFamily.remove(conversationId);
liveTokensFamily.remove(conversationId);
calibrationFamily.remove(conversationId);
}

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2c7f5c38af

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +1170 to +1171
contextUsage: z.boolean().optional(),
contextCost: z.boolean().optional(),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve context toggle values in app config

These new interface options are parsed here, but the resolved server/startup config is built by loadDefaultInterface, which whitelists each interface field and does not include contextUsage or contextCost (packages/data-schemas/src/app/interface.ts). As a result, deployments that set interface.contextUsage: false still render the token indicator, and interface.contextCost: true never reaches TokenConfigController, so pricing is omitted from /api/endpoints/token-config. Add these fields to the loaded interface config so the YAML values survive validation.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — added contextUsage/contextCost to loadDefaultInterface so the YAML values survive into the resolved interface config (defaults still flow from the schema).

Comment on lines +117 to +122
await this.emitUsage({
input_tokens: taggedUsage.input_tokens,
output_tokens: taggedUsage.output_tokens,
total_tokens: taggedUsage.total_tokens,
input_token_details: taggedUsage.input_token_details,
model: taggedUsage.model,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve top-level cache token fields in usage events

When a provider reports cache usage in the top-level cache_creation_input_tokens / cache_read_input_tokens fields (the existing spend path already supports this shape for Claude/Bedrock-style metadata), this emitted event drops those fields and only forwards input_token_details. With contextCost enabled the client then calculates zero cache write/read tokens for those calls, and resumed streams persist the same incomplete event. Include or normalize the top-level cache fields before streaming the usage event.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — the emitted event now normalizes top-level cache_creation_input_tokens/cache_read_input_tokens into input_token_details, matching the spend path's dual-shape handling.

Comment thread api/server/routes/endpoints.js Outdated
const router = express.Router();
/** Auth required for role/tenant-scoped endpoint config resolution. */
router.get('/', requireJwtAuth, endpointController);
router.get('/token-config', requireJwtAuth, tokenConfigController);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load app config for token config requests

This route only runs requireJwtAuth, and unlike the existing endpoint/model loaders it never populates req.config before TokenConfigController reads it. In that scenario appConfig is undefined, so customEndpoints is always empty and YAML/fetched tokenConfig overrides for custom endpoints are not applied; custom models whose context comes from those overrides return missing context/pricing from /api/endpoints/token-config. Add the config middleware here or have the controller load the app config before resolving endpoint token configs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — the token-config route now runs configMiddleware so req.config is populated before the controller resolves custom endpoint overrides.

Comment thread client/src/hooks/SSE/useUsageHandler.ts Outdated
Comment on lines +106 to +108
const endpoint = submission.conversation?.endpoint ?? '';
const model = data.model ?? submission.conversation?.model ?? '';
const rates = tokenConfig?.[endpoint]?.[model];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Look up agent usage rates by provider

For Agents runs backed by a non-OpenAI provider, submission.conversation.endpoint is agents while the usage event carries the actual provider in data.provider and the token-config rates are keyed under that provider endpoint. Looking up tokenConfig[agents][model] therefore leaves rates undefined for Anthropic/custom agents, so contextCost shows no session cost even though usage events arrive. Prefer the event/provider endpoint for Agents conversations before falling back to the conversation endpoint.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — rate lookup now prefers data.provider (the underlying provider for agent runs) before falling back to the conversation endpoint.

Comment thread client/src/hooks/SSE/useResumableSSE.ts Outdated
Comment on lines +769 to +771
} else if (pendingEvent.event === UsageEvents.ON_TOKEN_USAGE) {
if (!hasUsageBackfill) {
usageHandler(pendingEvent.data, resumeSubmission);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Count gap usage events after resume backfill

When resuming an in-memory stream that already has collectedUsage, token-usage events that arrive in the gap after getResumeState() but before the resumed subscription are returned in pendingEvents, not included in the backfill snapshot. This branch skips every pending usage event whenever any backfill exists, so multi-call agent runs can undercount the later model call; only replay events represented by the snapshot should be suppressed, while gap events still need to be folded.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — replaced the blanket skip with multiset matching against the backfill snapshot: replayed/gap usage events already in the snapshot are deduped, while events that raced past the snapshot read still fold.

const customEndpoints = appConfig?.endpoints?.[EModelEndpoint.custom] ?? [];
const cache = tokenConfigCache();
for (const endpointConfig of customEndpoints) {
const name = endpointConfig?.name;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize custom endpoint names for token overrides

loadConfigModels keys custom model lists by normalizeEndpointName(configName), but this controller stores token-config overrides under the raw YAML name. For configs such as name: Ollama (normalized elsewhere to ollama), buildTokenConfigMap looks up endpointTokenConfigs['ollama'] and misses both YAML and fetched overrides, so those custom models lose their configured context/pricing. Normalize the name before using it as the override/cache key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in e5ab7cf — the controller now keys overrides by normalizeEndpointName, matching how loadConfigModels and the token-config cache key custom endpoints.

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. What shall we delve into next?

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1843cff325

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/utils/tokens.ts Outdated
Comment on lines +252 to +253
const cacheIsAdditive = cacheWrite + cacheRead > input;
const baseInput = cacheIsAdditive ? input : Math.max(0, input - cacheRead - cacheWrite);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use provider semantics when pricing cache usage

When contextCost is enabled, this heuristic underprices providers whose cache tokens are additive but smaller than the non-cached input (for example Anthropic/Bedrock calls with input_tokens: 10000 and cache_read: 4000). The server-side billing path already keys this off usage.provider in packages/api/src/agents/usage.ts (inputTokensIncludesCache) because additive providers should bill the full input_tokens plus cache buckets, while this branch subtracts cache unless cache exceeds input, so the UI can show a materially lower session cost than the actual recorded spend.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34
const tokenKey = getTokenConfigKey(endpointConfig, name, req.user.id);
const cached = await cache.get(tokenKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate user-scoped token configs after model fetch

For custom endpoints whose model list is user-scoped (user-provided key/baseURL, or configured headers resolved against the user), the getModelsConfig(req) call above can fetch /models and build token pricing/context data, but fetchModels writes that data under its default name key because loadConfigModels does not pass this user-scoped tokenKey. This lookup then asks for ${endpoint}:${userId} and misses on a cold cache, so /api/endpoints/token-config returns fallback/static data without the fetched context window or pricing for those user-scoped custom endpoints until some other path happens to populate the same key.

Useful? React with 👍 / 👎.

Comment thread client/src/hooks/Chat/useTokenUsage.ts Outdated
Comment on lines +106 to +118
const snapshotActive =
snapshot != null &&
(isSubmitting || snapshot.anchorMessageId == null || branchTotals.containsAnchor);

if (snapshotActive && snapshot) {
const breakdown = snapshot.breakdown;
const maxTokens = snapshot.contextBudget ?? breakdown.maxContextTokens;
const instructionTokens = snapshot.effectiveInstructionTokens ?? breakdown.instructionTokens;
const baseUsed =
snapshot.remainingContextTokens != null
? maxTokens - snapshot.remainingContextTokens
: instructionTokens + breakdown.messageTokens;
const usedTokens = Math.max(0, baseUsed) + liveTokens;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include finalized output after clearing live tokens

After an agent run completes, finalizeUsage upserts the response token count into branchTotals and clears liveTokens, but this branch remains active whenever the just-submitted user message is still on the selected branch. Since the context snapshot was emitted before the LLM call, baseUsed + liveTokens then drops the finalized assistant output from the gauge until a later snapshot or reload, so completed conversations underreport context by the last response's tokens.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4827ae7b95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +33 to +34
const tokenKey = getTokenConfigKey(endpointConfig, name, req.user.id);
const cached = await cache.get(tokenKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the scoped token key through model fetching

For custom endpoints with a user-provided key or baseURL, getTokenConfigKey() returns a user-scoped key here, but the preceding getModelsConfig() path fetches those models via createLoadConfigModels without passing tokenKey; fetchModels then stores the derived token config under name (tokenKey ?? name). On a first load of /api/endpoints/token-config, this lookup misses the provider token config that was just fetched and returns only static fallbacks until a chat initializes the endpoint with the scoped key.

Useful? React with 👍 / 👎.

Comment on lines +55 to +56
tokenomics.prompt = overrideRates.prompt;
tokenomics.completion = overrideRates.completion;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include custom cache rates in token config

When a custom tokenConfig entry includes cache prices, this override branch only exposes prompt and completion. The billing path reads custom cache rates from endpointTokenConfig[model].write/read, while the UI falls back to prompt pricing when cacheWrite/cacheRead are absent, so cached-token costs shown by the new gauge are wrong for custom endpoints that configure different cache read/write rates.

Useful? React with 👍 / 👎.

Comment thread client/src/hooks/SSE/useUsageHandler.ts Outdated
Comment on lines +109 to +111
const rates =
(data.provider != null ? tokenConfig?.[data.provider]?.[model] : undefined) ??
tokenConfig?.[endpoint]?.[model];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Normalize Vertex AI before token-config lookups

When an Agent uses the Vertex AI provider, usage events carry provider: "vertexai", but the token-config response is populated under the google endpoint key from the default model loader (there is no default vertexai entry). This lookup therefore leaves rates undefined for Vertex/Gemini agent runs (and the same alias affects context-limit lookup), so the new usage/cost UI silently loses pricing and limit data even though it exists under google.

Useful? React with 👍 / 👎.

Comment on lines +133 to +137
if (
data.event === StepEvents.ON_MESSAGE_DELTA ||
data.event === StepEvents.ON_REASONING_DELTA
) {
tapStream(data.data, { ...submission, userMessage });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Sample live tokens for non-agent streams

This only feeds tapStream from agent step delta events. Standard chat streams handled by the later data.type/text branches (OpenAI, custom OpenAI-compatible endpoints, etc.) never call tapStream, so the newly mounted token gauge does not update during the response for those common endpoints and only changes after the final message is saved.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: 🚀 deployed

The LibreChat-pr-13670 index is now live on the MCP server.
Deploy run

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 05e48ec583

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/utils/tokens.ts Outdated
Comment on lines +252 to +253
const cacheIsAdditive = cacheWrite + cacheRead > input;
const baseInput = cacheIsAdditive ? input : Math.max(0, input - cacheRead - cacheWrite);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the same cache split as billing

When contextCost is enabled, this size-based heuristic under-reports session cost for additive-cache providers such as Anthropic/Bedrock whenever their cache tokens are smaller than the non-cached input (for example input_tokens=1000, cache_read=100 is charged as only 900 prompt tokens here). The backend billing path does not infer this from relative sizes; packages/api/src/agents/usage.ts treats only known subset providers as including cache in input_tokens, and otherwise charges input + write + read, so the UI can show a materially lower cost than the actual transactions for common cached Claude calls.

Useful? React with 👍 / 👎.

Comment on lines +33 to +34
const tokenKey = getTokenConfigKey(endpointConfig, name, req.user.id);
const cached = await cache.get(tokenKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Populate the token cache under the key you read

For custom endpoints whose API key/base URL or forwarded headers are user-scoped, getTokenConfigKey() returns ${endpoint}:${userId} here, but the getModelsConfig(req) call that just fetched those models invokes fetchModels without passing that token key (see packages/api/src/endpoints/config/models.ts), so fetchModels stores the generated token config under the plain endpoint name. As a result /api/endpoints/token-config omits fetched context/pricing for those endpoints until some other chat initialization happens to populate the user-scoped key, leaving the new gauge without the server-resolved limits for user-key/header based custom providers.

Useful? React with 👍 / 👎.

Comment thread client/src/hooks/SSE/useUsageHandler.ts Outdated

const foldUsage = (data: TTokenUsageEvent, submission: UsageSubmissionLike) => {
const convoKey = getConvoKey(submission);
const tokenConfig = queryClient.getQueryData<TTokenConfigMap>([QueryKeys.tokenConfig]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reprice usage after token config loads

When contextCost is enabled, this synchronous cache read can be undefined for the first fast on_token_usage event because the /token-config query is started by the gauge at the same time as the stream. That event is then folded into usageTotals with calcUsageCost(..., undefined) === 0 and is never recomputed when the token-config query finishes, so short/first model calls can permanently show $0.00 or undercount the session cost even though pricing is enabled.

Useful? React with 👍 / 👎.

}
}
}
registry.set(conversationId, index);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3 Badge Clear token indexes when conversations unmount

This module-level registry keeps a full message-token map for every conversation passed to buildIndex, but production code never calls the exported clearIndex cleanup path (repo-wide rg "clearIndex\(" only finds the export and tests). In a long-lived tab that opens many conversations, these maps retain all visited message IDs/token entries even after React Query and the Jotai atoms are removed, causing avoidable memory growth proportional to browsing history.

Useful? React with 👍 / 👎.

Comment thread client/src/utils/tokens.ts Outdated
return 0;
}
const input = usage.input_tokens ?? 0;
const output = usage.output_tokens ?? 0;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Derive completion cost from total tokens

For providers where usage_metadata.output_tokens omits billable reasoning/thinking tokens but total_tokens includes them, this charges only the raw output field. The backend billing path repairs that case in resolveCompletionTokens by using total_tokens - input_tokens (after cache adjustment), so Vertex/Gemini-style calls with hidden reasoning can show a lower session cost than the actual transaction total.

Useful? React with 👍 / 👎.

Comment on lines +59 to +64
tokenomics.prompt = deps.getMultiplier({
valueKey,
model,
endpoint,
tokenType: 'prompt',
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve tiered pricing for large prompts

These server-resolved rates are computed once without an inputTokenCount, so models with premium thresholds always expose their base prompt/completion multipliers to the client. For a long-context call that crosses a tiered threshold (for example the premium entries handled by getMultiplier when billing transactions), the UI will still multiply by the cheaper base rate and understate the session cost even though contextCost is intended to mirror backend accounting.

Useful? React with 👍 / 👎.

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7489c2be8c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +33 to +34
const tokenKey = getTokenConfigKey(endpointConfig, name, req.user.id);
const cached = await cache.get(tokenKey);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Use the populated token-config cache key

For custom endpoints with models.fetch plus a user-provided API key/base URL or user-scoped forwarded headers, getTokenConfigKey() returns a user-scoped key here, but the preceding getModelsConfig() fetch path stores the fetched token config under the endpoint name because it does not pass this tokenKey into fetchModels. In those deployments this lookup misses the token config that was just fetched, so /api/endpoints/token-config falls back to static/default values for custom models and the UI shows missing or wrong context/pricing until some other chat initialization happens to populate the scoped key.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 84436aa — the controller now falls back from the user-scoped key to the plain endpoint name, accepting whichever writer populated the cache (models-config fetch stores plain; chat initialization stores scoped).

Comment on lines +471 to +472
handle: async (event, data) => {
await emitEvent(res, streamId, { event, data });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect hidden sequential-agent context events

When a sequential agent run sets hide_sequential_outputs, the message/reasoning delta handlers gate emissions with checkIfLastAgent(...) || !metadata?.hide_sequential_outputs, but this new context-usage handler forwards every ON_CONTEXT_USAGE event unconditionally. Hidden intermediate agents can therefore still update and persist the client's context gauge with their token budget snapshots, exposing or confusing information that the stream otherwise suppresses; apply the same visibility predicate before emitting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 84436aa — the context-usage forwarder now applies the same visibility predicate as the delta handlers (checkIfLastAgent || !hide_sequential_outputs), so hidden intermediate agents no longer update the client gauge.

}
/>
<div className="mx-auto flex" />
<TokenUsage index={index} conversation={conversation} isSubmitting={isSubmitting} />

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Refresh the gauge when max context changes

When a user changes the active conversation's max context setting, this new gauge can stay on the old limit because ChatFormWrapper passes a memoized stableConversation whose dependency list does not include conversation.maxContextTokens, while useTokenLimits() now reads that field to compute maxContextTokens. Until some unrelated dependency such as model or endpoint changes, the gauge keeps rendering with the previous budget.

Useful? React with 👍 / 👎.

data.event === StepEvents.ON_MESSAGE_DELTA ||
data.event === StepEvents.ON_REASONING_DELTA
) {
tapStream(data.data, { ...currentSubmission, userMessage });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Clear live estimates on completed reconnect misses

After tapStream starts accumulating live output, a dropped connection that reconnects after the backend has already completed and deleted the job takes the existing 404 path, which invalidates/refetches messages but never calls resetLive or finalizeUsage. In that case the refetched response's persisted tokenCount is added to the stale live estimate, so the context gauge over-counts until another stream resets it.

Useful? React with 👍 / 👎.

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

- raise the popup breakdown's context progressbar contrast (muted
  surface-tertiary track, prominent text-primary fill) — that's the bar the
  contrast feedback was about
- revert the gauge ring restyle (kept its original border-heavy track /
  text-secondary fill); the ring wasn't the element in question
… Memo

- a null-anchor context snapshot was treated as active on every branch,
  leaking one generation's granular breakdown onto sibling branches. Require
  a non-null (response-message) anchor on the viewed branch instead, so
  siblings without a matching snapshot fall back to their own totals.
- revert the buildTree WeakMap memo in messages.ts. buildTree is pure (builds
  from shallow copies) so the memo was behaviorally identical, but it was the
  feature's only change to core branch-navigation selectors — removing it
  matches upstream and rules it out of branch-navigation debugging.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0de3f927d4

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +190 to +192
endpointTokenConfig = toBillingTokenConfig(
endpointConfig.tokenConfig as Record<string, Record<string, number>>,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve fallback context limits for partial tokenConfig

When a custom endpoint defines a static tokenConfig for only some of its models, this partial map becomes the endpointTokenConfig used by runtime model initialization. getModelMaxTokens(..., endpointTokenConfig) treats any provided map as authoritative and does not fall back to the built-in maxTokensMap, so an unlisted built-in model on the same endpoint can drop to the default context budget even though /token-config advertises the built-in fallback. This over-prunes or mis-budgets those unlisted models; keep the same fallback behavior in runtime max-token resolution or only apply the override when the selected model matches it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 859bf37. `getModelMaxTokens` / `getModelMaxOutputTokens` now try the override first and fall back to the built-in `maxTokensMap` / `maxOutputTokensMap` for any model a partial override does not list (`system_default` precedence preserved). This matches what `/token-config` advertises via `buildTokenConfigMap`, so unlisted built-in models on the same endpoint keep their real context budget instead of dropping to the default. Also consolidated the builder's now-redundant manual fallback into the single call, and added `tokens.spec.ts` coverage for the partial-override case.

Comment on lines +306 to +309
if (usageCost?.enabled === true && usageCost.pricing) {
try {
payload = { ...data, cost: computeUsageCostUSD(data, usageCost.pricing) };
} catch (err) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Price usage events with endpoint token config

When interface.contextCost is enabled for a custom/OpenRouter model whose rates come from tokenConfig, this computes the streamed cost with only the global pricing functions. The token-config response uses the endpoint-specific rates, so on_token_usage can fall back to default/static pricing for those models and show an incorrect session cost in the UI. Thread the resolved endpoint token config through usageCost and pass it to computeUsageCostUSD.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 859bf37. `usageCost` now carries `endpointTokenConfig` (set from `primaryConfig.endpointTokenConfig` once the agent resolves) and `computeUsageCostUSD(data, usageCost.pricing, usageCost.endpointTokenConfig)` prices with the endpoint-specific rates, so the streamed `on_token_usage` cost matches billing for custom/OpenRouter models with static `tokenConfig`.

Comment on lines +190 to +192
endpointTokenConfig = toBillingTokenConfig(
endpointConfig.tokenConfig as Record<string, Record<string, number>>,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Propagate static tokenConfig into agent spending

For Agents backed by a custom endpoint with the newly supported static tokenConfig, this branch creates the billing override, but initializeAgent only consumes options.endpointTokenConfig for max-context resolution and the later AgentClient is never constructed with it; AgentClient.recordCollectedUsage therefore sees this.options.endpointTokenConfig as undefined. In that scenario transactions and balance deductions use the global/default rates instead of the admin-configured rates, so pass the resolved config through to the AgentClient spending path as well.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 859bf37. `initializeAgent` now surfaces `options.endpointTokenConfig` onto the returned `InitializedAgent`, and `AgentClient` is constructed with `endpointTokenConfig: primaryConfig.endpointTokenConfig`, so `recordCollectedUsage` (and the subagent usage emitter, which reads `this.options.endpointTokenConfig`) spend at the admin-configured rates instead of defaults. This supersedes my earlier decline on the same threading — the config genuinely was not reaching the spending path.

…xt Limits

Custom-endpoint agents resolve an endpointTokenConfig during agent init but
it never reached the AgentClient, so spending, emitted cost, and runtime
max-token resolution all fell back to default rates for those agents.

- Surface options.endpointTokenConfig on the returned InitializedAgent.
- Pass it to the AgentClient (this.options.endpointTokenConfig) so the
  spending path bills at configured rates.
- Thread it through usageCost to computeUsageCostUSD so emitted per-event
  cost matches billing.
- getModelMaxTokens/getModelMaxOutputTokens fall back to the built-in map
  for models absent from a partial override (matches buildTokenConfigMap);
  consolidates the duplicated fallback in pricing.ts.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 859bf37eb7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +667 to +670
seedLive(
countTrailingOutputChars(data.resumeState?.aggregatedContent),
resumeSubmission,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Avoid seeding stale output after a resumed context snapshot

When resuming during a tool loop after the next on_context_usage snapshot has already been emitted, that snapshot's messageTokens already includes the prior model output/tool turn. This unconditional seedLive(countTrailingOutputChars(aggregatedContent)) then counts the trailing text before the tool part again, so reconnecting in that window shows inflated context usage until another snapshot/finalization resets it. Only seed trailing output when there is no resumed context snapshot, or track output produced after that snapshot.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6a8e7d7. The trailing-output seed now only runs when the resume carries no context snapshot. When a snapshot is present its `messageTokens` already accounts for produced output (and `contextHandler` resets the live estimate to 0), so the unconditional `seedLive` was double-counting the trailing text until the next snapshot/finalize. Any output streamed after the snapshot but before reconnect is picked back up by `tapStream` as deltas resume, and corrected at finalize.

Comment thread api/server/controllers/agents/client.js Outdated
: undefined,
};
if (streamId) {
GenerationJobManager.emitChunk(streamId, { event: UsageEvents.ON_TOKEN_USAGE, data });

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Await subagent usage emission in resumable streams

For resumable streams with subagent calls, GenerationJobManager.emitChunk is async because it persists the usage event and publishes it; firing it without awaiting means generation can reach final cleanup and delete the job before this subagent usage is stored, and any Redis/publish failure will also bypass the surrounding try/catch. This makes resumed clients miss subagent usage/cost even though the same usage is billed server-side.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6a8e7d7. The subagent emitter is now `async` and `await`s `GenerationJobManager.emitChunk` inside its `try/catch`, matching every other emitChunk caller (ActionService/MCP) — emitChunk commits the HSET before publishing, so awaiting keeps a Redis/publish failure inside the catch instead of surfacing as an unhandled rejection, and the persist completes within the emitter rather than floating into job cleanup. Note billing is unaffected regardless: the child usage is pushed to `collectedUsage` synchronously in the sink (before `onUsage`), so `recordCollectedUsage` bills it and the resume path recovers it via `backfillUsage(resumeState.collectedUsage)` independent of the emit.

subagentAggregatorsByToolCallId,
/** Resolved endpoint token/pricing config so spending and cost reflect
* configured rates for custom-endpoint agents instead of defaults. */
endpointTokenConfig: primaryConfig.endpointTokenConfig,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep token configs scoped to each run agent

When a graph contains connected/added agents that use a different endpoint or provider, passing only the primary agent's endpointTokenConfig into AgentClient makes both recordCollectedUsage and the emitted cost calculation price every collected usage item with the primary custom endpoint's overrides. For example, a secondary agent using the same model id on another endpoint can be over/under-charged by the primary endpoint's static tokenConfig; the per-agent resolved config needs to travel with each usage item or be selected by provider/agent.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid for the narrow case; deferring the full fix. Context: before this PR the agents path never passed `endpointTokenConfig` to `AgentClient` at all, so all usage (primary included) was billed at default rates. Threading `primaryConfig.endpointTokenConfig` (859bf37) fixes the common single-custom-endpoint-agent case and does not regress distinct-model-id secondaries — a model absent from the primary's `tokenConfig` falls back to the built-in rate map exactly as before. The only affected scenario is a secondary agent on a different endpoint that shares the same model id as a primary `tokenConfig` entry with different rates. Fully per-agent pricing needs each collected usage item to carry its agent's resolved config and a per-item resolver threaded through the shared `recordCollectedUsage` (also used by responses.js/openai.js) — tracked as a focused follow-up rather than expanding this PR's billing surface.

The granular context breakdown lives only in the live on_context_usage
snapshot — a single per-conversation slot, anchored to the latest response
and overwritten by each generation. Switching to a branch generated earlier
this session lost its tool/skill/system rows and fell back to coarse totals.

Retain each generation's finalized snapshot in a per-conversation map keyed
by its branch-unique response id (snapshotsByAnchorFamily). When the live
snapshot is off the viewed branch, walk the branch tail for its deepest
stored anchor and render that breakdown. Bounded by generation count and
cleared on conversation switch; the live/just-generated path is unchanged.
- useResumableSSE: skip the trailing-output live seed when the resume
  carries a context snapshot; the snapshot's messageTokens already counts
  produced output, so seeding it again inflated usage until the next reset.
- AgentClient subagent emitter: await GenerationJobManager.emitChunk like
  every other caller (it persists before publishing), so a floating promise
  can't race job cleanup and a Redis/publish failure is caught by the
  emitter's try/catch instead of surfacing as an unhandled rejection.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

1 similar comment
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

Add a test-only data-testid distinguishing the granular snapshot breakdown
(context-breakdown) from the coarse message-history estimate
(context-estimate), then assert granularity in the mock e2e harness:

- renders the granular breakdown from the live on_context_usage snapshot
  (guards that the snapshot event actually reaches the popover, not just the
  usage totals).
- preserves the granular breakdown after switching branches — regenerate to
  overwrite the single live snapshot, switch back, and confirm the rows
  survive via the per-anchor snapshot history map.

Branch regenerate/sibling selectors mirror the existing chat.spec branch test.
All three usage specs pass against the mock pipeline.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9f2c302e4a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread client/src/hooks/SSE/useResumableSSE.ts Outdated
Comment on lines +662 to +664
if (data.resumeState?.contextUsage) {
contextHandler(data.resumeState.contextUsage, resumeSubmission);
} else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Seed resumed output when a snapshot exists

When resuming an active stream after on_context_usage has been persisted, this branch skips seedLive(...). The stored context snapshot is the pre-invoke budget, while text already streamed during the disconnected window is only restored through aggregatedContent; backfilled usage updates totals but not confirmedRef, and Redis/cross-replica resumes do not replay those deltas as pending events. As a result, the context gauge undercounts already-produced output during the resumed run (and can finalize the snapshot with too little completedOutputTokens when the usage event was only backfilled).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1ac6027 — and you are right that my previous gate over-corrected. Root cause: `countTrailingOutputChars` skipped a trailing tool-call part and counted the text before it, which is already folded into the next snapshot's `messageTokens`. It now counts only the contiguous output at the very END of the aggregated content (0 when the latest part is a tool call), and the resume path always seeds it again. So in-flight output the pre-invoke snapshot does not yet cover is restored, while a snapshot's budget is never double-counted. The only residual is a tiny window (resumed after a call emitted a tool_call but before the next snapshot) — bounded to one call's text and corrected at the next snapshot/finalize.

Comment on lines +920 to +922
/** Resolved endpoint token/pricing config so spending and cost reflect
* configured rates for custom-endpoint agents instead of defaults. */
endpointTokenConfig: primaryConfig.endpointTokenConfig,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Price secondary agents with their own token config

In a multi-agent run where a connected or added agent uses a different custom endpoint/tokenConfig than the primary agent, only primaryConfig.endpointTokenConfig is passed into AgentClient. The collected usage events for secondary agents still flow through recordCollectedUsage and the subagent cost emitter with this single config, so those calls are billed/displayed with the primary endpoint's rates or default rates instead of the secondary agent's configured rates. Thread the resolved config per usage-producing agent rather than using only the primary config.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Holding this as a deferred follow-up (chip filed), consistent with the prior round. To be clear on scope: before this PR the agents path never passed `endpointTokenConfig` to `AgentClient` at all, so all usage was billed at default rates. Threading `primaryConfig.endpointTokenConfig` fixes the common single-custom-endpoint-agent case with no regression for distinct-model-id secondaries (a model absent from the primary config falls back to the built-in rate map exactly as before). The only affected case is a secondary agent on a different endpoint sharing the same model id as a primary `tokenConfig` entry. Fully per-agent pricing needs each collected usage item to carry its agent's resolved config and a per-item resolver threaded through the shared `recordCollectedUsage` (also used by responses.js/openai.js) — a billing-pipeline change I'd rather not fold into this PR.

Comment on lines +94 to +95
if (isSubmittingRef.current || event.type !== 'updated') {
return;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Rebuild the token index for streaming fallback

When the backend does not emit context snapshots (for example the documented pre-@librechat/agents@3.2.35 fallback path, or non-agent streams), skipping all message-query updates while isSubmitting leaves the module token index on the pre-submit branch. After the created event changes the active tail to the placeholder response, sumBranch cannot find that tail and the gauge shows only live output tokens, dropping the existing history and current prompt until the final event calls finalizeUsage.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1ac6027. `useTokenUsage` now re-indexes from the messages cache on tail change while `isSubmitting`. The cache subscriber is intentionally muted during streaming (no per-chunk O(n) rebuilds), but the `created` event still moves the tail to the placeholder response — so in the no-snapshot fallback (non-agent streams / pre-3.2.35) `sumBranch` missed that tail and dropped history + prompt until `finalizeUsage`. The re-index is bounded: `tailId` only shifts on created/finalize/branch-switch, never per chunk, so streaming stays O(1) per delta.

/** Lets the host stream the billed child usage to the client (tagged
* `subagent`, so it folds into session cost/totals but not the live
* gauge) — child runs never reach ModelEndHandler's emit path. */
onUsage?.(usage);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Await subagent usage emissions before continuing

For resumable streams, buildSubagentUsageEmitter is async because it awaits GenerationJobManager.emitChunk, but the sink fires it without awaiting or retaining the promise. In subagent-heavy runs this lets the graph proceed to the final event and job cleanup while the on_token_usage emit/persist is still pending, so the UI/resume state can miss billed subagent usage even though collectedUsage was charged. Make the sink/host await these emissions or flush them before finalizing.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 1ac6027. `AgentClient` now retains each subagent emit promise (`pendingSubagentEmits`) and flushes them with `Promise.allSettled` in `chatCompletion`'s finally, before the response returns and the job is cleaned up. You're right that the emitter being async was not enough — the sink fires it without awaiting, and resume reads the usage that `emitChunk` persists (HSET before publish via `trackTokenUsage`), so cleanup racing the persist would drop billed subagent usage from resumed clients. (Live billing was always safe — `collectedUsage` is pushed synchronously in the sink.)

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

… Flush

Codex round on the prior commit:

- countTrailingOutputChars now counts only output at the very END of the
  aggregated content (0 when the model paused at a tool call), and the resume
  path always seeds it. The earlier skip-trailing-tool-parts behavior plus the
  skip-seed-when-snapshot gate together over- or under-counted in-flight
  output on resume; one rule fixes both — pre-invoke snapshot budget is never
  double-counted, and genuine in-flight output is no longer dropped.
- useTokenUsage re-indexes from the messages cache on tail change while
  submitting. The cache subscriber is muted during streaming, so without a
  context snapshot (non-agent streams) sumBranch missed the created tail and
  dropped history + prompt until finalize. Bounded — tailId only shifts on
  created/finalize/branch-switch.
- AgentClient tracks subagent usage emit promises and flushes them in
  chatCompletion's finally. The sink fires the emitter without awaiting, and
  resume reads the usage emitChunk persists (HSET), so cleanup must not race
  it or resumed clients miss billed subagent usage.
@danny-avila

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Breezy!

Reviewed commit: 1ac6027565

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@github-actions

Copy link
Copy Markdown
Contributor

GitNexus: ❌ deploy failed

The deploy failed — the previous index (if any) continues to be served.
Deploy run

@danny-avila
danny-avila merged commit db7011d into dev Jun 13, 2026
33 checks passed
@danny-avila
danny-avila deleted the feat/context-usage-tracking branch June 13, 2026 23:38
fuuuzzy pushed a commit to fuuuzzy/LibreChat that referenced this pull request Jun 18, 2026
…13670)

* 📊 feat: Real-Time Context Window & Token Usage Tracking

* 🧪 fix: Align Pricing Spec Dep Signatures with TxDeps

* 🩹 fix: Resolve Codex Findings for Context Usage Tracking

* 📊 feat: Granular Tool Token Breakdown with Deferred Splits

* 🧪 test: Cover Session Cost in Mock E2E and Scope Usage Selectors

* 🧪 test: Live Host-Pipeline Usage Verification (Env-Gated)

* 🧪 test: Local Real-Provider Multi-Turn E2E Harness

* 🪙 fix: Keep Tagged Usage Buckets Out of the Live Context Estimate

* 🩹 fix: Scoped Token-Config Fallback and Sequential Visibility for Usage Events

* 🩹 fix: Address Usage Review Findings — Cost Timing, Scoped Caches, Finalized Output

- carry the post-snapshot output estimate into the context snapshot at
  finalize so the gauge keeps the last response after live resets
- accumulate per-rate billable units and price the session cost at
  render, so usage events arriving before the token-config load still
  count once it resolves
- pass user-scoped token-config cache keys through loadConfigModels
  fetches and drop the controller's unscoped fallback to prevent serving
  another user's resolved config
- tag emitted usage events with a per-run seq so resume dedupe never
  drops a distinct call with an identical payload
- admit the static tokenConfig override in the custom endpoint schema so
  it survives zod parsing into req.config

* 🩹 fix: Align Client Usage Accounting with Backend Cost Semantics

- classify cache tokens by provider (shared inputTokensIncludesCache from
  data-provider, consumed by both the backend billing path and the client)
  instead of a magnitude heuristic, so Anthropic/Bedrock turns where cache
  is smaller than uncached input no longer under-bill input
- mirror resolveCompletionTokens on the client so Vertex-style hidden
  thinking tokens are reflected in the Output row and session cost
- prefer endpoint pricing over adapter-provider pricing so a custom
  endpoint can price a known model name without built-in rates shadowing it
- carry static cacheRead/cacheWrite overrides through the tokenConfig
  schema and buildTokenConfigMap

* 🩹 fix: Honor Static Token Config in Billing; Tighten Usage Freshness

- initializeCustom now uses a static endpoint tokenConfig as the agent's
  endpointTokenConfig (billing + balance checks), not just the advertised
  UI config — previously the gauge showed admin rates while the agent
  billed against built-in tables
- invalidate the token-config query alongside models on user-key add/
  revoke so context windows and pricing refresh without a reload
- include maxContextTokens in ChatForm's stabilized conversation memo so
  the gauge reflects a changed context-window setting immediately
- feed the live output estimate from the legacy content path (direct and
  assistants streams), setting from cumulative part text rather than
  accumulating deltas

* 🩹 fix: Resume Usage Dedup, Agent Pricing, and Partial Override Billing

- fold usage events idempotently by (runId, seq) so resume backfill no
  longer resets the conversation totals — a mid-stream reconnect keeps the
  usage of prompts already completed earlier in the session
- tap replayed pending message/reasoning/content events so output streamed
  past the resume snapshot reaches the live estimate, not just the message
- resolve cost against the agent's backing endpoint (Agents conversations
  report endpoint `agents` / provider `openAI`, neither of which keys a
  custom endpoint's tokenConfig)
- getMultiplier/getCacheMultiplier fall back to the standard tables for
  models absent from a partial endpointTokenConfig, so a partial static
  override no longer bills non-listed models at defaultRate while the UI
  shows the correct pattern rate

* 🩹 fix: Repaired Output in Gauge, Cache-Rate Keys, Config Gate, Usage Cleanup

- live/completed gauge counts the repaired completion (normalized output),
  so under-reporting providers don't drop the response from used context
- translate static tokenConfig cacheWrite/cacheRead onto the write/read
  keys getCacheMultiplier reads, so cache tokens bill at the configured
  rate instead of the prompt-rate fallback
- clear the token index and usage atoms when leaving a conversation, so
  visited histories don't accumulate in memory for the tab's lifetime
- wait for startupConfig before mounting the gauge, so a deployment with
  contextUsage disabled never briefly mounts it or fires the token-config
  query on first load

* 🩹 fix: Move Token-Config Resolution to TS; Key Live Usage by Created Convo

- extract the token-config resolution (override gathering + cache lookup +
  buildTokenConfigMap) into resolveTokenConfigMap in packages/api, leaving
  the /api controller a thin request-scoped wrapper (CLAUDE.md TS rule)
- getConvoKey prefers the user message's real conversationId once the
  `created` event stamps it, so a new chat's first-response live gauge and
  totals land under the id TokenUsage subscribes to instead of NEW_CONVO

* 🩹 fix: Clear Stale Redis Job Usage; Live-Tap Legacy Streams; Share Fetched Config

- DEL the Redis job hash before re-creating it so a reused streamId can't
  inherit a prior run's contextUsage/tokenUsage and backfill stale usage
- tap the legacy {message,text} stream branch (non-agent OpenAI/Anthropic
  streams) into the live estimate, not just the content path
- copy a deduped fetch's token config to every sibling endpoint sharing the
  baseURL/key/headers, so /token-config resolves each by its own name

* ⏪ revert: Don't DEL Redis job hash in createJob (breaks cross-replica resume)

createJob is an idempotent join — a second replica calls it for the same
streamId to share an in-flight stream's state. DELeting the hash wiped the
prior replica's persisted created/usage state, so a joining replica missed
the created event (GenerationJobManager cross-replica integration test).
Reverts the F1 change from 2bfce0c; the stale-usage concern doesn't
arise in practice (streamId is unique per generation).

* 🩹 fix: Best-Effort Usage Emit; Tag Hidden Sequential-Agent Usage

- wrap the ModelEndHandler usage emit in try/catch so a failed telemetry
  delivery (closed SSE / Redis publish error) can't abort the handler
  before thought-signature capture, which would break resumed tool calls
- tag hidden sequential-agent usage as 'sequential' (non-primary) so the
  client folds it into session cost/totals but not the live context gauge,
  instead of letting an undefined usage_type inflate the visible gauge

* 🩹 fix: Refetch Stale Token Config on Mount; Normalize Vertex for Lookup

- useTokenConfigQuery refetches on mount when stale, so a user-key change
  that invalidates tokenConfig while the gauge is unmounted takes effect on
  return instead of serving the prior key's resolved config
- normalize a Vertex-backed agent's provider (vertexai) to the google
  token-config key, so Gemini context windows and rates resolve instead of
  showing unknown context / $0 cost

* ✨ feat: Server-Side Per-Event Cost (Authoritative Pricing for the Gauge)

Move usage-cost pricing to the single source of truth. The backend prices
each model call with the same billing functions (premium tiers via
getMultiplier(inputTokenCount), cache rates) and emits the USD cost on
on_token_usage when interface.contextCost is enabled; the client sums
emitted costs instead of re-deriving from base token-config rates.

- computeUsageCostUSD reuses prepareTokenSpend/prepareStructuredTokenSpend
  so the emitted cost matches what is billed (incl. premium thresholds)
- getDefaultHandlers gains a usageCost pricing context; initialize.js wires
  db.getMultiplier/getCacheMultiplier gated on contextCost (agents path)
- client UsageTotals carries a summed costUSD; retire the client-side rate
  lookups (costFromUnits/calcUsageCost) that drifted from backend pricing
  and produced the provider-keying / cache-key / Vertex / premium findings
- keep normalizeUsageUnits for the displayed token counts; token-config is
  still used for the context-window meter

Fixes the premium-tier session-cost under-report (gpt-5.x / gemini-3.1
above their input thresholds).

* 🩹 fix: Branch-Accurate Usage Snapshot + Clearer Gauge Track Contrast

- re-anchor the context snapshot from the user message to the response
  message at finalize. Regenerating a response branches off a shared user
  message, so anchoring on it made the snapshot read as "active" on both
  branches — switching to the sibling branch showed the wrong (other
  branch's) context. The response message is branch-unique, so sibling
  branches now correctly fall back to their own per-branch totals.
- raise the gauge ring's track/fill contrast (muted track, prominent fill)
  so the used portion reads clearly as a fill-level indicator

* 🩹 fix: Tag Sequential Usage in Billing; Emit Subagent Cost; Reset Live on Resume Errors

- tag hidden sequential-agent usage `usage_type: 'sequential'` on the
  COLLECTED usage (not just the emit), and treat it as non-primary in
  recordCollectedUsage (billed, excluded from the reported output total) so
  hidden intermediate output stops inflating the parent's tokenCount/pruning
- emit on_token_usage from the subagent usage sink (tagged `subagent`, with
  authoritative cost when contextCost is on) so the gauge's session
  cost/totals include billed subagent usage; it stays out of the live meter
- call resetLive on the resumable 404 and max-retry terminal branches so the
  gauge doesn't keep counting stale in-flight tokens after the stream ends

* 🎨 fix: Contrast the Popup Context Bar; Revert Ring Restyle

- raise the popup breakdown's context progressbar contrast (muted
  surface-tertiary track, prominent text-primary fill) — that's the bar the
  contrast feedback was about
- revert the gauge ring restyle (kept its original border-heavy track /
  text-secondary fill); the ring wasn't the element in question

* 🩹 fix: Stop Snapshot Granularity Leaking Across Branches; Revert Tree Memo

- a null-anchor context snapshot was treated as active on every branch,
  leaking one generation's granular breakdown onto sibling branches. Require
  a non-null (response-message) anchor on the viewed branch instead, so
  siblings without a matching snapshot fall back to their own totals.
- revert the buildTree WeakMap memo in messages.ts. buildTree is pure (builds
  from shallow copies) so the memo was behaviorally identical, but it was the
  feature's only change to core branch-navigation selectors — removing it
  matches upstream and rules it out of branch-navigation debugging.

* 🪙 fix: Thread Endpoint Token Config to Agent Billing, Cost, and Context Limits

Custom-endpoint agents resolve an endpointTokenConfig during agent init but
it never reached the AgentClient, so spending, emitted cost, and runtime
max-token resolution all fell back to default rates for those agents.

- Surface options.endpointTokenConfig on the returned InitializedAgent.
- Pass it to the AgentClient (this.options.endpointTokenConfig) so the
  spending path bills at configured rates.
- Thread it through usageCost to computeUsageCostUSD so emitted per-event
  cost matches billing.
- getModelMaxTokens/getModelMaxOutputTokens fall back to the built-in map
  for models absent from a partial override (matches buildTokenConfigMap);
  consolidates the duplicated fallback in pricing.ts.

* 🪙 fix: Preserve Granular Breakdown Across Branch Switches

The granular context breakdown lives only in the live on_context_usage
snapshot — a single per-conversation slot, anchored to the latest response
and overwritten by each generation. Switching to a branch generated earlier
this session lost its tool/skill/system rows and fell back to coarse totals.

Retain each generation's finalized snapshot in a per-conversation map keyed
by its branch-unique response id (snapshotsByAnchorFamily). When the live
snapshot is off the viewed branch, walk the branch tail for its deepest
stored anchor and render that breakdown. Bounded by generation count and
cleared on conversation switch; the live/just-generated path is unchanged.

* 🪙 fix: Harden Resume Seeding and Subagent Usage Emission

- useResumableSSE: skip the trailing-output live seed when the resume
  carries a context snapshot; the snapshot's messageTokens already counts
  produced output, so seeding it again inflated usage until the next reset.
- AgentClient subagent emitter: await GenerationJobManager.emitChunk like
  every other caller (it persists before publishing), so a floating promise
  can't race job cleanup and a Redis/publish failure is caught by the
  emitter's try/catch instead of surfacing as an unhandled rejection.

* 🧪 test: Playwright Coverage for Context Breakdown Granularity

Add a test-only data-testid distinguishing the granular snapshot breakdown
(context-breakdown) from the coarse message-history estimate
(context-estimate), then assert granularity in the mock e2e harness:

- renders the granular breakdown from the live on_context_usage snapshot
  (guards that the snapshot event actually reaches the popover, not just the
  usage totals).
- preserves the granular breakdown after switching branches — regenerate to
  overwrite the single live snapshot, switch back, and confirm the rows
  survive via the per-anchor snapshot history map.

Branch regenerate/sibling selectors mirror the existing chat.spec branch test.
All three usage specs pass against the mock pipeline.

* 🪙 fix: Correct Resume Live-Seed, Fallback Re-index, and Subagent Emit Flush

Codex round on the prior commit:

- countTrailingOutputChars now counts only output at the very END of the
  aggregated content (0 when the model paused at a tool call), and the resume
  path always seeds it. The earlier skip-trailing-tool-parts behavior plus the
  skip-seed-when-snapshot gate together over- or under-counted in-flight
  output on resume; one rule fixes both — pre-invoke snapshot budget is never
  double-counted, and genuine in-flight output is no longer dropped.
- useTokenUsage re-indexes from the messages cache on tail change while
  submitting. The cache subscriber is muted during streaming, so without a
  context snapshot (non-agent streams) sumBranch missed the created tail and
  dropped history + prompt until finalize. Bounded — tailId only shifts on
  created/finalize/branch-switch.
- AgentClient tracks subagent usage emit promises and flushes them in
  chatCompletion's finally. The sink fires the emitter without awaiting, and
  resume reads the usage emitChunk persists (HSET), so cleanup must not race
  it or resumed clients miss billed subagent usage.
ThomasVuNguyen pushed a commit to ThomasVuNguyen/LibreChat that referenced this pull request Jul 15, 2026
…13670)

* 📊 feat: Real-Time Context Window & Token Usage Tracking

* 🧪 fix: Align Pricing Spec Dep Signatures with TxDeps

* 🩹 fix: Resolve Codex Findings for Context Usage Tracking

* 📊 feat: Granular Tool Token Breakdown with Deferred Splits

* 🧪 test: Cover Session Cost in Mock E2E and Scope Usage Selectors

* 🧪 test: Live Host-Pipeline Usage Verification (Env-Gated)

* 🧪 test: Local Real-Provider Multi-Turn E2E Harness

* 🪙 fix: Keep Tagged Usage Buckets Out of the Live Context Estimate

* 🩹 fix: Scoped Token-Config Fallback and Sequential Visibility for Usage Events

* 🩹 fix: Address Usage Review Findings — Cost Timing, Scoped Caches, Finalized Output

- carry the post-snapshot output estimate into the context snapshot at
  finalize so the gauge keeps the last response after live resets
- accumulate per-rate billable units and price the session cost at
  render, so usage events arriving before the token-config load still
  count once it resolves
- pass user-scoped token-config cache keys through loadConfigModels
  fetches and drop the controller's unscoped fallback to prevent serving
  another user's resolved config
- tag emitted usage events with a per-run seq so resume dedupe never
  drops a distinct call with an identical payload
- admit the static tokenConfig override in the custom endpoint schema so
  it survives zod parsing into req.config

* 🩹 fix: Align Client Usage Accounting with Backend Cost Semantics

- classify cache tokens by provider (shared inputTokensIncludesCache from
  data-provider, consumed by both the backend billing path and the client)
  instead of a magnitude heuristic, so Anthropic/Bedrock turns where cache
  is smaller than uncached input no longer under-bill input
- mirror resolveCompletionTokens on the client so Vertex-style hidden
  thinking tokens are reflected in the Output row and session cost
- prefer endpoint pricing over adapter-provider pricing so a custom
  endpoint can price a known model name without built-in rates shadowing it
- carry static cacheRead/cacheWrite overrides through the tokenConfig
  schema and buildTokenConfigMap

* 🩹 fix: Honor Static Token Config in Billing; Tighten Usage Freshness

- initializeCustom now uses a static endpoint tokenConfig as the agent's
  endpointTokenConfig (billing + balance checks), not just the advertised
  UI config — previously the gauge showed admin rates while the agent
  billed against built-in tables
- invalidate the token-config query alongside models on user-key add/
  revoke so context windows and pricing refresh without a reload
- include maxContextTokens in ChatForm's stabilized conversation memo so
  the gauge reflects a changed context-window setting immediately
- feed the live output estimate from the legacy content path (direct and
  assistants streams), setting from cumulative part text rather than
  accumulating deltas

* 🩹 fix: Resume Usage Dedup, Agent Pricing, and Partial Override Billing

- fold usage events idempotently by (runId, seq) so resume backfill no
  longer resets the conversation totals — a mid-stream reconnect keeps the
  usage of prompts already completed earlier in the session
- tap replayed pending message/reasoning/content events so output streamed
  past the resume snapshot reaches the live estimate, not just the message
- resolve cost against the agent's backing endpoint (Agents conversations
  report endpoint `agents` / provider `openAI`, neither of which keys a
  custom endpoint's tokenConfig)
- getMultiplier/getCacheMultiplier fall back to the standard tables for
  models absent from a partial endpointTokenConfig, so a partial static
  override no longer bills non-listed models at defaultRate while the UI
  shows the correct pattern rate

* 🩹 fix: Repaired Output in Gauge, Cache-Rate Keys, Config Gate, Usage Cleanup

- live/completed gauge counts the repaired completion (normalized output),
  so under-reporting providers don't drop the response from used context
- translate static tokenConfig cacheWrite/cacheRead onto the write/read
  keys getCacheMultiplier reads, so cache tokens bill at the configured
  rate instead of the prompt-rate fallback
- clear the token index and usage atoms when leaving a conversation, so
  visited histories don't accumulate in memory for the tab's lifetime
- wait for startupConfig before mounting the gauge, so a deployment with
  contextUsage disabled never briefly mounts it or fires the token-config
  query on first load

* 🩹 fix: Move Token-Config Resolution to TS; Key Live Usage by Created Convo

- extract the token-config resolution (override gathering + cache lookup +
  buildTokenConfigMap) into resolveTokenConfigMap in packages/api, leaving
  the /api controller a thin request-scoped wrapper (CLAUDE.md TS rule)
- getConvoKey prefers the user message's real conversationId once the
  `created` event stamps it, so a new chat's first-response live gauge and
  totals land under the id TokenUsage subscribes to instead of NEW_CONVO

* 🩹 fix: Clear Stale Redis Job Usage; Live-Tap Legacy Streams; Share Fetched Config

- DEL the Redis job hash before re-creating it so a reused streamId can't
  inherit a prior run's contextUsage/tokenUsage and backfill stale usage
- tap the legacy {message,text} stream branch (non-agent OpenAI/Anthropic
  streams) into the live estimate, not just the content path
- copy a deduped fetch's token config to every sibling endpoint sharing the
  baseURL/key/headers, so /token-config resolves each by its own name

* ⏪ revert: Don't DEL Redis job hash in createJob (breaks cross-replica resume)

createJob is an idempotent join — a second replica calls it for the same
streamId to share an in-flight stream's state. DELeting the hash wiped the
prior replica's persisted created/usage state, so a joining replica missed
the created event (GenerationJobManager cross-replica integration test).
Reverts the F1 change from 2bfce0c; the stale-usage concern doesn't
arise in practice (streamId is unique per generation).

* 🩹 fix: Best-Effort Usage Emit; Tag Hidden Sequential-Agent Usage

- wrap the ModelEndHandler usage emit in try/catch so a failed telemetry
  delivery (closed SSE / Redis publish error) can't abort the handler
  before thought-signature capture, which would break resumed tool calls
- tag hidden sequential-agent usage as 'sequential' (non-primary) so the
  client folds it into session cost/totals but not the live context gauge,
  instead of letting an undefined usage_type inflate the visible gauge

* 🩹 fix: Refetch Stale Token Config on Mount; Normalize Vertex for Lookup

- useTokenConfigQuery refetches on mount when stale, so a user-key change
  that invalidates tokenConfig while the gauge is unmounted takes effect on
  return instead of serving the prior key's resolved config
- normalize a Vertex-backed agent's provider (vertexai) to the google
  token-config key, so Gemini context windows and rates resolve instead of
  showing unknown context / $0 cost

* ✨ feat: Server-Side Per-Event Cost (Authoritative Pricing for the Gauge)

Move usage-cost pricing to the single source of truth. The backend prices
each model call with the same billing functions (premium tiers via
getMultiplier(inputTokenCount), cache rates) and emits the USD cost on
on_token_usage when interface.contextCost is enabled; the client sums
emitted costs instead of re-deriving from base token-config rates.

- computeUsageCostUSD reuses prepareTokenSpend/prepareStructuredTokenSpend
  so the emitted cost matches what is billed (incl. premium thresholds)
- getDefaultHandlers gains a usageCost pricing context; initialize.js wires
  db.getMultiplier/getCacheMultiplier gated on contextCost (agents path)
- client UsageTotals carries a summed costUSD; retire the client-side rate
  lookups (costFromUnits/calcUsageCost) that drifted from backend pricing
  and produced the provider-keying / cache-key / Vertex / premium findings
- keep normalizeUsageUnits for the displayed token counts; token-config is
  still used for the context-window meter

Fixes the premium-tier session-cost under-report (gpt-5.x / gemini-3.1
above their input thresholds).

* 🩹 fix: Branch-Accurate Usage Snapshot + Clearer Gauge Track Contrast

- re-anchor the context snapshot from the user message to the response
  message at finalize. Regenerating a response branches off a shared user
  message, so anchoring on it made the snapshot read as "active" on both
  branches — switching to the sibling branch showed the wrong (other
  branch's) context. The response message is branch-unique, so sibling
  branches now correctly fall back to their own per-branch totals.
- raise the gauge ring's track/fill contrast (muted track, prominent fill)
  so the used portion reads clearly as a fill-level indicator

* 🩹 fix: Tag Sequential Usage in Billing; Emit Subagent Cost; Reset Live on Resume Errors

- tag hidden sequential-agent usage `usage_type: 'sequential'` on the
  COLLECTED usage (not just the emit), and treat it as non-primary in
  recordCollectedUsage (billed, excluded from the reported output total) so
  hidden intermediate output stops inflating the parent's tokenCount/pruning
- emit on_token_usage from the subagent usage sink (tagged `subagent`, with
  authoritative cost when contextCost is on) so the gauge's session
  cost/totals include billed subagent usage; it stays out of the live meter
- call resetLive on the resumable 404 and max-retry terminal branches so the
  gauge doesn't keep counting stale in-flight tokens after the stream ends

* 🎨 fix: Contrast the Popup Context Bar; Revert Ring Restyle

- raise the popup breakdown's context progressbar contrast (muted
  surface-tertiary track, prominent text-primary fill) — that's the bar the
  contrast feedback was about
- revert the gauge ring restyle (kept its original border-heavy track /
  text-secondary fill); the ring wasn't the element in question

* 🩹 fix: Stop Snapshot Granularity Leaking Across Branches; Revert Tree Memo

- a null-anchor context snapshot was treated as active on every branch,
  leaking one generation's granular breakdown onto sibling branches. Require
  a non-null (response-message) anchor on the viewed branch instead, so
  siblings without a matching snapshot fall back to their own totals.
- revert the buildTree WeakMap memo in messages.ts. buildTree is pure (builds
  from shallow copies) so the memo was behaviorally identical, but it was the
  feature's only change to core branch-navigation selectors — removing it
  matches upstream and rules it out of branch-navigation debugging.

* 🪙 fix: Thread Endpoint Token Config to Agent Billing, Cost, and Context Limits

Custom-endpoint agents resolve an endpointTokenConfig during agent init but
it never reached the AgentClient, so spending, emitted cost, and runtime
max-token resolution all fell back to default rates for those agents.

- Surface options.endpointTokenConfig on the returned InitializedAgent.
- Pass it to the AgentClient (this.options.endpointTokenConfig) so the
  spending path bills at configured rates.
- Thread it through usageCost to computeUsageCostUSD so emitted per-event
  cost matches billing.
- getModelMaxTokens/getModelMaxOutputTokens fall back to the built-in map
  for models absent from a partial override (matches buildTokenConfigMap);
  consolidates the duplicated fallback in pricing.ts.

* 🪙 fix: Preserve Granular Breakdown Across Branch Switches

The granular context breakdown lives only in the live on_context_usage
snapshot — a single per-conversation slot, anchored to the latest response
and overwritten by each generation. Switching to a branch generated earlier
this session lost its tool/skill/system rows and fell back to coarse totals.

Retain each generation's finalized snapshot in a per-conversation map keyed
by its branch-unique response id (snapshotsByAnchorFamily). When the live
snapshot is off the viewed branch, walk the branch tail for its deepest
stored anchor and render that breakdown. Bounded by generation count and
cleared on conversation switch; the live/just-generated path is unchanged.

* 🪙 fix: Harden Resume Seeding and Subagent Usage Emission

- useResumableSSE: skip the trailing-output live seed when the resume
  carries a context snapshot; the snapshot's messageTokens already counts
  produced output, so seeding it again inflated usage until the next reset.
- AgentClient subagent emitter: await GenerationJobManager.emitChunk like
  every other caller (it persists before publishing), so a floating promise
  can't race job cleanup and a Redis/publish failure is caught by the
  emitter's try/catch instead of surfacing as an unhandled rejection.

* 🧪 test: Playwright Coverage for Context Breakdown Granularity

Add a test-only data-testid distinguishing the granular snapshot breakdown
(context-breakdown) from the coarse message-history estimate
(context-estimate), then assert granularity in the mock e2e harness:

- renders the granular breakdown from the live on_context_usage snapshot
  (guards that the snapshot event actually reaches the popover, not just the
  usage totals).
- preserves the granular breakdown after switching branches — regenerate to
  overwrite the single live snapshot, switch back, and confirm the rows
  survive via the per-anchor snapshot history map.

Branch regenerate/sibling selectors mirror the existing chat.spec branch test.
All three usage specs pass against the mock pipeline.

* 🪙 fix: Correct Resume Live-Seed, Fallback Re-index, and Subagent Emit Flush

Codex round on the prior commit:

- countTrailingOutputChars now counts only output at the very END of the
  aggregated content (0 when the model paused at a tool call), and the resume
  path always seeds it. The earlier skip-trailing-tool-parts behavior plus the
  skip-seed-when-snapshot gate together over- or under-counted in-flight
  output on resume; one rule fixes both — pre-invoke snapshot budget is never
  double-counted, and genuine in-flight output is no longer dropped.
- useTokenUsage re-indexes from the messages cache on tail change while
  submitting. The cache subscriber is muted during streaming, so without a
  context snapshot (non-agent streams) sumBranch missed the created tail and
  dropped history + prompt until finalize. Bounded — tailId only shifts on
  created/finalize/branch-switch.
- AgentClient tracks subagent usage emit promises and flushes them in
  chatCompletion's finally. The sink fires the emitter without awaiting, and
  resume reads the usage emitChunk persists (HSET), so cleanup must not race
  it or resumed clients miss billed subagent usage.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants