Skip to content

feat: add chat token usage indicator#9162

Merged
Soulter merged 1 commit into
masterfrom
codex/chat-input-token-usage-indicator
Jul 6, 2026
Merged

feat: add chat token usage indicator#9162
Soulter merged 1 commit into
masterfrom
codex/chat-input-token-usage-indicator

Conversation

@Soulter

@Soulter Soulter commented Jul 6, 2026

Copy link
Copy Markdown
Member

Summary

  • show a neutral token-usage progress ring next to the ChatUI microphone action
  • derive usage from the latest assistant token stats and model context metadata
  • hide the indicator when usage is zero or no context window is available

Tests

  • pnpm typecheck
  • uv run ruff format . && uv run ruff check .

Summary by Sourcery

Add a chat token usage indicator derived from assistant token stats and provider context metadata, surfaced next to the Chat UI microphone action.

New Features:

  • Display a neutral token-usage progress ring in the chat input when context and usage data are available.

Enhancements:

  • Centralize provider context limit and token count formatting utilities for reuse across chat features.

Chores:

  • Load and track enabled chat completion providers in the chat view to support token usage calculations.

@dosubot dosubot Bot added size:L This PR changes 100-499 lines, ignoring generated files. area:webui The bug / feature is about webui(dashboard) of astrbot. feature:chatui The bug / feature is about astrbot's chatui, webchat labels Jul 6, 2026

@sourcery-ai sourcery-ai Bot 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.

Hey - I've found 1 issue, and left some high level feedback:

  • The loadTokenProviders response is force-cast to TokenProviderConfig[] via as unknown as, which bypasses type safety; consider validating the shape or narrowing the type instead of double-casting.
  • There is overlapping logic for clamping/normalizing usage percentages between formatUsagePercent in Chat.vue and tokenUsagePercent in ChatInput.vue; consolidating this into a single helper would reduce duplication and potential drift.
  • The readTokenCount helper currently ignores zero and negative values; if the API can report zero or negative token deltas, you may want to treat them explicitly rather than silently coercing to 0.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `loadTokenProviders` response is force-cast to `TokenProviderConfig[]` via `as unknown as`, which bypasses type safety; consider validating the shape or narrowing the type instead of double-casting.
- There is overlapping logic for clamping/normalizing usage percentages between `formatUsagePercent` in `Chat.vue` and `tokenUsagePercent` in `ChatInput.vue`; consolidating this into a single helper would reduce duplication and potential drift.
- The `readTokenCount` helper currently ignores zero and negative values; if the API can report zero or negative token deltas, you may want to treat them explicitly rather than silently coercing to 0.

## Individual Comments

### Comment 1
<location path="dashboard/src/components/chat/Chat.vue" line_range="858-862" />
<code_context>
         selectedText: replyPreview(replyTarget.value.id, ""),
       },
 );
+const currentTokenProvider = computed(() => {
+  const selectedProvider = tokenProviderConfigs.value.find(
+    (provider) => provider.id === selectedTokenProviderId.value,
+  );
+  return selectedProvider || tokenProviderConfigs.value[0] || null;
+});
+const latestTokenUsageTotal = computed(() => {
</code_context>
<issue_to_address>
**suggestion:** Keep `selectedTokenProviderId` in sync when falling back to the first provider.

In `currentTokenProvider`, when `selectedTokenProviderId` doesn't match any loaded provider, you fall back to `tokenProviderConfigs[0]` but leave `selectedTokenProviderId` pointing to a non-existent provider. This desynchronizes the persisted selection (e.g. localStorage) from the actual provider used for `contextLimit`. Please either update `selectedTokenProviderId` to the fallback provider’s id when no match is found, or introduce a derived `effectiveSelectedProviderId` that always reflects the provider actually used.

Suggested implementation:

```
const tokenProviderConfigs = ref<TokenProviderConfig[]>([]);
const selectedTokenProviderId = ref("");

const effectiveSelectedTokenProviderId = computed(() => {
  const hasSelectedProvider = tokenProviderConfigs.value.some(
    (provider) => provider.id === selectedTokenProviderId.value,
  );

  if (hasSelectedProvider) {
    return selectedTokenProviderId.value;
  }

  return tokenProviderConfigs.value[0]?.id ?? "";
});

const messagesContainer = ref<HTMLElement | null>(null);

```

```
const currentTokenProvider = computed(() => {
  const selectedProvider = tokenProviderConfigs.value.find(
    (provider) => provider.id === effectiveSelectedTokenProviderId.value,
  );

  return selectedProvider || tokenProviderConfigs.value[0] || null;
});

```

Any logic that depends on the “actually used” token provider (e.g. for `contextLimit`, pricing display, or usage calculation) should use `effectiveSelectedTokenProviderId` or `currentTokenProvider` rather than `selectedTokenProviderId`. Search for usages of `selectedTokenProviderId` elsewhere in `Chat.vue` (and related components) and update them where they should reflect the effective provider, not just the persisted/raw selection.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +858 to +862
const currentTokenProvider = computed(() => {
const selectedProvider = tokenProviderConfigs.value.find(
(provider) => provider.id === selectedTokenProviderId.value,
);
return selectedProvider || tokenProviderConfigs.value[0] || null;

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.

suggestion: Keep selectedTokenProviderId in sync when falling back to the first provider.

In currentTokenProvider, when selectedTokenProviderId doesn't match any loaded provider, you fall back to tokenProviderConfigs[0] but leave selectedTokenProviderId pointing to a non-existent provider. This desynchronizes the persisted selection (e.g. localStorage) from the actual provider used for contextLimit. Please either update selectedTokenProviderId to the fallback provider’s id when no match is found, or introduce a derived effectiveSelectedProviderId that always reflects the provider actually used.

Suggested implementation:

const tokenProviderConfigs = ref<TokenProviderConfig[]>([]);
const selectedTokenProviderId = ref("");

const effectiveSelectedTokenProviderId = computed(() => {
  const hasSelectedProvider = tokenProviderConfigs.value.some(
    (provider) => provider.id === selectedTokenProviderId.value,
  );

  if (hasSelectedProvider) {
    return selectedTokenProviderId.value;
  }

  return tokenProviderConfigs.value[0]?.id ?? "";
});

const messagesContainer = ref<HTMLElement | null>(null);

const currentTokenProvider = computed(() => {
  const selectedProvider = tokenProviderConfigs.value.find(
    (provider) => provider.id === effectiveSelectedTokenProviderId.value,
  );

  return selectedProvider || tokenProviderConfigs.value[0] || null;
});

Any logic that depends on the “actually used” token provider (e.g. for contextLimit, pricing display, or usage calculation) should use effectiveSelectedTokenProviderId or currentTokenProvider rather than selectedTokenProviderId. Search for usages of selectedTokenProviderId elsewhere in Chat.vue (and related components) and update them where they should reflect the effective provider, not just the persisted/raw selection.

@gemini-code-assist gemini-code-assist Bot 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.

Code Review

This pull request introduces a token usage indicator to the chat interface, displaying a progress ring and tooltip in the chat input based on the current provider's context limit and the latest message's token usage. The feedback highlights a reactivity issue where the selected provider ID is not updated when changed in the dropdown menu until a message is sent, and suggests checking both enable and enabled properties when filtering active providers.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +858 to +863
const currentTokenProvider = computed(() => {
const selectedProvider = tokenProviderConfigs.value.find(
(provider) => provider.id === selectedTokenProviderId.value,
);
return selectedProvider || tokenProviderConfigs.value[0] || null;
});

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.

medium

The selectedTokenProviderId is only updated when getSelectedProviderSelection() is called (which happens during message sending or editing) or on initial mount. If a user changes the active provider/model in the dropdown menu, selectedTokenProviderId will not update reactively. As a result, the tokenUsageIndicator (and the progress ring) will continue to use the context limit of the old provider until a message is sent.

To fix this, consider having ProviderModelMenu emit a change event when a new provider is selected, propagating it up through ChatInput to Chat.vue to update selectedTokenProviderId reactively.

Comment on lines +1020 to +1022
tokenProviderConfigs.value = (
(response.data.data || []) as unknown as TokenProviderConfig[]
).filter((provider) => provider.enable !== false);

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.

medium

Some providers might use the enabled property instead of enable to indicate their status (as seen in other parts of the API and configuration helpers). To ensure disabled providers are correctly filtered out, check both properties.

      tokenProviderConfigs.value = (
        (response.data.data || []) as unknown as TokenProviderConfig[]
      ).filter((provider) => provider.enable !== false && provider.enabled !== false);

@Soulter Soulter force-pushed the codex/chat-input-token-usage-indicator branch from 7ef89cc to 8eec231 Compare July 6, 2026 13:14
@Soulter Soulter merged commit ab2502c into master Jul 6, 2026
20 checks passed
@Soulter Soulter deleted the codex/chat-input-token-usage-indicator branch July 6, 2026 13:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:webui The bug / feature is about webui(dashboard) of astrbot. feature:chatui The bug / feature is about astrbot's chatui, webchat size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant