Show total number of tokens by using tokenizer

This commit is contained in:
Leszek Hanusz 2026-02-03 01:50:52 +01:00
parent 3657a8a7ad
commit fb2095e815
4 changed files with 88 additions and 11 deletions

Binary file not shown.

View File

@ -2,7 +2,7 @@
import { notebookStore } from '$lib/stores/notebook.svelte';
import Button from '$lib/components/ui/button/button.svelte';
import Textarea from '$lib/components/ui/textarea/textarea.svelte';
import { Play, Square, Settings, Undo, Redo } from '@lucide/svelte';
import { Play, Square, Settings, Undo, Redo, RulerDimensionLine } from '@lucide/svelte';
import { config } from '$lib/stores/settings.svelte';
import DialogChatSettings from '$lib/components/app/dialogs/DialogChatSettings.svelte';
import { ChatMessageStatistics, DialogChatError, KeyboardShortcutInfo, ModelsSelector } from '$lib/components/app';
@ -41,6 +41,7 @@
// Sync local input with store content
$effect(() => {
inputContent = notebookStore.content;
notebookStore.updateTokenCount();
});
function handleInput(e: Event) {
@ -335,16 +336,32 @@
/>
</div>
{#if showMessageStats && (notebookStore.promptTokens > 0 || notebookStore.predictedTokens > 0)}
<div class="flex w-full justify-end md:w-auto">
<ChatMessageStatistics
promptTokens={notebookStore.promptTokens}
promptMs={notebookStore.promptMs}
predictedTokens={notebookStore.predictedTokens}
predictedMs={notebookStore.predictedMs}
isLive={notebookStore.isGenerating}
isProcessingPrompt={notebookStore.isGenerating && notebookStore.predictedTokens === 0}
/>
{#if showMessageStats}
<div class="flex w-full flex-col items-end justify-center gap-0.5 md:w-auto">
{#if notebookStore.totalTokens > 0}
<Tooltip.Root>
<Tooltip.Trigger>
<div class="pr-3.5 flex items-center gap-1.5 text-xs text-muted-foreground">
<RulerDimensionLine class="h-3.5 w-3.5" />
<span>{notebookStore.totalTokens} tokens</span>
</div>
</Tooltip.Trigger>
<Tooltip.Content>
<p>Total tokens</p>
</Tooltip.Content>
</Tooltip.Root>
{/if}
{#if notebookStore.promptTokens > 0 || notebookStore.predictedTokens > 0}
<ChatMessageStatistics
promptTokens={notebookStore.promptTokens}
promptMs={notebookStore.promptMs}
predictedTokens={notebookStore.predictedTokens}
predictedMs={notebookStore.predictedMs}
isLive={notebookStore.isGenerating}
isProcessingPrompt={notebookStore.isGenerating && notebookStore.predictedTokens === 0}
/>
{/if}
</div>
{/if}
</div>

View File

@ -1,5 +1,17 @@
import { getJsonHeaders } from '$lib/utils';
import { AttachmentType } from '$lib/enums';
import type {
ApiChatCompletionRequest,
ApiChatCompletionResponse,
ApiChatCompletionStreamChunk,
ApiChatCompletionToolCall,
ApiChatCompletionToolCallDelta,
ApiChatMessageContentPart,
ApiChatMessageData,
ApiCompletionRequest,
ApiCompletionResponse,
ApiCompletionStreamChunk
} from '$lib/types/api';
/**
* ChatService - Low-level API communication layer for Chat Completions
@ -1116,4 +1128,31 @@ export class ChatService {
throw err;
}
}
/**
* Tokenizes the provided text using the server's tokenizer.
*
* @param content - The text content to tokenize
* @param signal - Optional AbortSignal
* @returns {Promise<number[]>} Promise that resolves to an array of token IDs
*/
static async tokenize(content: string, signal?: AbortSignal): Promise<number[]> {
try {
const response = await fetch('./tokenize', {
method: 'POST',
headers: getJsonHeaders(),
body: JSON.stringify({ content }),
signal
});
if (!response.ok) {
throw new Error(`Tokenize failed: ${response.statusText}`);
}
const data = await response.json();
return data.tokens;
} catch (error) {
console.error('Tokenize error:', error);
return [];
}
}
}

View File

@ -11,6 +11,9 @@ export class NotebookStore {
promptMs = $state(0);
predictedTokens = $state(0);
predictedMs = $state(0);
totalTokens = $state(0);
generationStartTokens = $state(0);
tokenizeTimeout: ReturnType<typeof setTimeout> | undefined;
error = $state<{
message: string;
@ -36,6 +39,9 @@ export class NotebookStore {
this.predictedTokens = 0;
this.predictedMs = 0;
// Snapshot the current total tokens as the baseline for this generation
this.generationStartTokens = this.totalTokens;
try {
const currentConfig = config();
await ChatService.sendCompletion(
@ -62,9 +68,13 @@ export class NotebookStore {
if (processed > 0) this.promptTokens = processed;
if (time_ms > 0) this.promptMs = time_ms;
}
// Update totalTokens live
this.totalTokens = this.generationStartTokens + this.predictedTokens;
},
onComplete: () => {
this.isGenerating = false;
this.totalTokens = this.generationStartTokens + this.predictedTokens;
},
onError: (error: unknown) => {
if (error instanceof Error && error.name === 'AbortError') {
@ -123,6 +133,17 @@ export class NotebookStore {
}
this.isGenerating = false;
}
updateTokenCount() {
if (this.tokenizeTimeout) {
clearTimeout(this.tokenizeTimeout);
}
this.tokenizeTimeout = setTimeout(async () => {
const tokens = await ChatService.tokenize(this.content);
this.totalTokens = tokens.length;
}, 500);
}
}
export const notebookStore = new NotebookStore();