Spaces:
Running
Running
display latency, tokens and prvider
Browse files
src/lib/components/chat/Assistant.svelte
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
import { MessageCirclePlus, Star } from '@lucide/svelte';
|
| 13 |
import { mode } from 'mode-watcher';
|
| 14 |
|
| 15 |
-
import type { ChatModel, ChatMessage } from '$lib/helpers/types';
|
| 16 |
import { Button } from '$lib/components/ui/button';
|
| 17 |
import Message from './Message.svelte';
|
| 18 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
|
@@ -27,6 +27,7 @@
|
|
| 27 |
let selectedModel = $derived((nodeData.current?.data.selectedModel as ChatModel) ?? null);
|
| 28 |
let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
|
| 29 |
let loading = $derived((nodeData.current?.data.loading as boolean) ?? false);
|
|
|
|
| 30 |
let message = $derived(
|
| 31 |
nodeData.current?.data.content
|
| 32 |
? ({
|
|
@@ -134,9 +135,34 @@
|
|
| 134 |
{/if}
|
| 135 |
{#if message}
|
| 136 |
<div bind:this={containerRef}>
|
| 137 |
-
<Message {message}
|
| 138 |
</div>
|
| 139 |
{/if}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</div>
|
| 141 |
<Button
|
| 142 |
variant={mode.current === 'dark' ? 'default' : 'outline'}
|
|
|
|
| 12 |
import { MessageCirclePlus, Star } from '@lucide/svelte';
|
| 13 |
import { mode } from 'mode-watcher';
|
| 14 |
|
| 15 |
+
import type { ChatModel, ChatMessage, TokenUsage } from '$lib/helpers/types';
|
| 16 |
import { Button } from '$lib/components/ui/button';
|
| 17 |
import Message from './Message.svelte';
|
| 18 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
|
|
|
| 27 |
let selectedModel = $derived((nodeData.current?.data.selectedModel as ChatModel) ?? null);
|
| 28 |
let messages = $derived((nodeData.current?.data.messages as ChatMessage[]) ?? []);
|
| 29 |
let loading = $derived((nodeData.current?.data.loading as boolean) ?? false);
|
| 30 |
+
let usage = $derived((nodeData.current?.data.usage as TokenUsage) ?? null);
|
| 31 |
let message = $derived(
|
| 32 |
nodeData.current?.data.content
|
| 33 |
? ({
|
|
|
|
| 135 |
{/if}
|
| 136 |
{#if message}
|
| 137 |
<div bind:this={containerRef}>
|
| 138 |
+
<Message {message} />
|
| 139 |
</div>
|
| 140 |
{/if}
|
| 141 |
+
{#if usage && !loading && message}
|
| 142 |
+
{@const provider = selectedModel.provider}
|
| 143 |
+
<p class="mt-3 border-t border-border pt-3 text-xs text-muted-foreground">
|
| 144 |
+
{usage.total_tokens} tokens
|
| 145 |
+
{#if message.timestamp}
|
| 146 |
+
<span class="mx-0.5">·</span> Latency {message.timestamp}ms
|
| 147 |
+
{/if}
|
| 148 |
+
<span class="inline-flex items-center gap-0.5">
|
| 149 |
+
<span>·</span> Using
|
| 150 |
+
<span class="inline-flex items-center gap-0.5 rounded-full bg-muted px-1 py-0.5">
|
| 151 |
+
{#if provider === 'auto'}
|
| 152 |
+
<Star class="size-3.5 fill-yellow-500 text-yellow-500" />
|
| 153 |
+
{:else}
|
| 154 |
+
<img
|
| 155 |
+
src={`https://huggingface.co/api/avatars/${provider}`}
|
| 156 |
+
alt={provider}
|
| 157 |
+
class="size-4 rounded"
|
| 158 |
+
/>
|
| 159 |
+
{/if}
|
| 160 |
+
{provider}
|
| 161 |
+
</span>
|
| 162 |
+
provider
|
| 163 |
+
</span>
|
| 164 |
+
</p>
|
| 165 |
+
{/if}
|
| 166 |
</div>
|
| 167 |
<Button
|
| 168 |
variant={mode.current === 'dark' ? 'default' : 'outline'}
|
src/lib/components/chat/Message.svelte
CHANGED
|
@@ -11,9 +11,8 @@
|
|
| 11 |
import ListItem from './markdown/ListItem.svelte';
|
| 12 |
import Link from './markdown/Link.svelte';
|
| 13 |
import Hr from './markdown/Hr.svelte';
|
| 14 |
-
import { Star } from '@lucide/svelte';
|
| 15 |
|
| 16 |
-
let { message
|
| 17 |
|
| 18 |
const renderers = {
|
| 19 |
paragraph: Paragraph,
|
|
@@ -37,24 +36,5 @@
|
|
| 37 |
</p>
|
| 38 |
{:else}
|
| 39 |
<SvelteMarkdown source={message.content} renderers={renderers as any} />
|
| 40 |
-
{#if message.timestamp}
|
| 41 |
-
<p class="flex items-center gap-1 text-xs text-muted-foreground/70 select-none">
|
| 42 |
-
Generated in
|
| 43 |
-
{message.timestamp / 1000}s using
|
| 44 |
-
<span class="flex items-center gap-1 rounded bg-muted py-0.5 pr-1 pl-0.5">
|
| 45 |
-
{#if provider === 'auto'}
|
| 46 |
-
<Star class="size-4 fill-yellow-500 text-yellow-500" />
|
| 47 |
-
{:else}
|
| 48 |
-
<img
|
| 49 |
-
src={`https://huggingface.co/api/avatars/${provider}`}
|
| 50 |
-
alt={provider}
|
| 51 |
-
class="size-4 rounded"
|
| 52 |
-
/>
|
| 53 |
-
{/if}
|
| 54 |
-
{provider}
|
| 55 |
-
</span>
|
| 56 |
-
provider
|
| 57 |
-
</p>
|
| 58 |
-
{/if}
|
| 59 |
{/if}
|
| 60 |
</main>
|
|
|
|
| 11 |
import ListItem from './markdown/ListItem.svelte';
|
| 12 |
import Link from './markdown/Link.svelte';
|
| 13 |
import Hr from './markdown/Hr.svelte';
|
|
|
|
| 14 |
|
| 15 |
+
let { message }: { message: ChatMessage } = $props();
|
| 16 |
|
| 17 |
const renderers = {
|
| 18 |
paragraph: Paragraph,
|
|
|
|
| 36 |
</p>
|
| 37 |
{:else}
|
| 38 |
<SvelteMarkdown source={message.content} renderers={renderers as any} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 39 |
{/if}
|
| 40 |
</main>
|
src/lib/components/chat/User.svelte
CHANGED
|
@@ -12,7 +12,7 @@
|
|
| 12 |
useSvelteFlow
|
| 13 |
} from '@xyflow/svelte';
|
| 14 |
|
| 15 |
-
import type { ChatModel, ChatMessage } from '$lib/helpers/types';
|
| 16 |
import { Button } from '$lib/components/ui/button';
|
| 17 |
import ComboBoxModels from '$lib/components/model/ComboBoxModels.svelte';
|
| 18 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
|
@@ -143,6 +143,7 @@
|
|
| 143 |
if (!response.body) throw new Error('No response body');
|
| 144 |
|
| 145 |
let content = '';
|
|
|
|
| 146 |
|
| 147 |
const reader = response.body.getReader();
|
| 148 |
const decoder = new TextDecoder();
|
|
@@ -154,6 +155,16 @@
|
|
| 154 |
const errorMessage = content.split('__ERROR__').pop() ?? 'Unknown error';
|
| 155 |
throw new Error(errorMessage);
|
| 156 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
const newNodeId = `user-${crypto.randomUUID()}`;
|
| 158 |
const newNode: Node = {
|
| 159 |
id: newNodeId,
|
|
@@ -178,7 +189,14 @@
|
|
| 178 |
const end = Date.now();
|
| 179 |
updateNodeData(
|
| 180 |
node.id,
|
| 181 |
-
{
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 182 |
{ replace: true }
|
| 183 |
);
|
| 184 |
break;
|
|
|
|
| 12 |
useSvelteFlow
|
| 13 |
} from '@xyflow/svelte';
|
| 14 |
|
| 15 |
+
import type { ChatModel, ChatMessage, TokenUsage } from '$lib/helpers/types';
|
| 16 |
import { Button } from '$lib/components/ui/button';
|
| 17 |
import ComboBoxModels from '$lib/components/model/ComboBoxModels.svelte';
|
| 18 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
|
|
|
| 143 |
if (!response.body) throw new Error('No response body');
|
| 144 |
|
| 145 |
let content = '';
|
| 146 |
+
let usage: TokenUsage | null = null;
|
| 147 |
|
| 148 |
const reader = response.body.getReader();
|
| 149 |
const decoder = new TextDecoder();
|
|
|
|
| 155 |
const errorMessage = content.split('__ERROR__').pop() ?? 'Unknown error';
|
| 156 |
throw new Error(errorMessage);
|
| 157 |
}
|
| 158 |
+
if (content.includes('__USAGE__')) {
|
| 159 |
+
const usageParts = content.split('__USAGE__');
|
| 160 |
+
const usageJson = usageParts.pop() ?? '';
|
| 161 |
+
content = usageParts.join('').trimEnd();
|
| 162 |
+
try {
|
| 163 |
+
usage = JSON.parse(usageJson) as TokenUsage;
|
| 164 |
+
} catch {
|
| 165 |
+
// ignore malformed usage JSON
|
| 166 |
+
}
|
| 167 |
+
}
|
| 168 |
const newNodeId = `user-${crypto.randomUUID()}`;
|
| 169 |
const newNode: Node = {
|
| 170 |
id: newNodeId,
|
|
|
|
| 189 |
const end = Date.now();
|
| 190 |
updateNodeData(
|
| 191 |
node.id,
|
| 192 |
+
{
|
| 193 |
+
...node.data,
|
| 194 |
+
content,
|
| 195 |
+
timestamp: end - start,
|
| 196 |
+
loading: false,
|
| 197 |
+
messages,
|
| 198 |
+
usage
|
| 199 |
+
},
|
| 200 |
{ replace: true }
|
| 201 |
);
|
| 202 |
break;
|
src/lib/helpers/types.ts
CHANGED
|
@@ -19,6 +19,13 @@ export interface ChatMessage {
|
|
| 19 |
isHidden?: boolean;
|
| 20 |
}
|
| 21 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
export interface HFUser {
|
| 23 |
id: string;
|
| 24 |
name: string;
|
|
|
|
| 19 |
isHidden?: boolean;
|
| 20 |
}
|
| 21 |
|
| 22 |
+
export interface TokenUsage {
|
| 23 |
+
prompt_tokens: number;
|
| 24 |
+
completion_tokens: number;
|
| 25 |
+
total_tokens: number;
|
| 26 |
+
reasoning_tokens: number;
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
export interface HFUser {
|
| 30 |
id: string;
|
| 31 |
name: string;
|
src/routes/api/+server.ts
CHANGED
|
@@ -33,6 +33,20 @@ export async function POST({ request }: RequestEvent) {
|
|
| 33 |
try {
|
| 34 |
for await (const chunk of stream) {
|
| 35 |
const content = chunk.choices?.[0]?.delta?.content ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
if (content) {
|
| 37 |
controller.enqueue(encoder.encode(content));
|
| 38 |
}
|
|
|
|
| 33 |
try {
|
| 34 |
for await (const chunk of stream) {
|
| 35 |
const content = chunk.choices?.[0]?.delta?.content ?? '';
|
| 36 |
+
const usage = chunk.usage;
|
| 37 |
+
if (usage) {
|
| 38 |
+
const usageData = {
|
| 39 |
+
prompt_tokens: usage.prompt_tokens ?? 0,
|
| 40 |
+
completion_tokens: usage.completion_tokens ?? 0,
|
| 41 |
+
total_tokens: usage.total_tokens ?? 0,
|
| 42 |
+
reasoning_tokens:
|
| 43 |
+
(usage.completion_tokens_details as Record<string, number>)
|
| 44 |
+
?.reasoning_tokens ?? 0
|
| 45 |
+
};
|
| 46 |
+
controller.enqueue(
|
| 47 |
+
encoder.encode(`\n\n__USAGE__${JSON.stringify(usageData)}`)
|
| 48 |
+
);
|
| 49 |
+
}
|
| 50 |
if (content) {
|
| 51 |
controller.enqueue(encoder.encode(content));
|
| 52 |
}
|