Spaces:
Running
Running
show what provider has been used
Browse files- src/lib/chat/triggerAiCall.ts +4 -1
- src/lib/components/chat/Assistant.svelte +11 -18
- src/lib/index.ts +2 -1
- src/routes/api/+server.ts +65 -37
src/lib/chat/triggerAiCall.ts
CHANGED
|
@@ -89,6 +89,8 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
|
|
| 89 |
}
|
| 90 |
if (!response.body) throw new Error('No response body');
|
| 91 |
|
|
|
|
|
|
|
| 92 |
let content = '';
|
| 93 |
let reasoning = '';
|
| 94 |
let usage: TokenUsage | null = null;
|
|
@@ -124,7 +126,8 @@ export async function triggerAiCall(ctx: TriggerAiCallContext): Promise<void> {
|
|
| 124 |
timestamp: end - start,
|
| 125 |
loading: false,
|
| 126 |
messages,
|
| 127 |
-
usage
|
|
|
|
| 128 |
} as Record<string, unknown>,
|
| 129 |
{ replace: true }
|
| 130 |
);
|
|
|
|
| 89 |
}
|
| 90 |
if (!response.body) throw new Error('No response body');
|
| 91 |
|
| 92 |
+
const inferenceProvider = response.headers.get('x-inference-provider');
|
| 93 |
+
|
| 94 |
let content = '';
|
| 95 |
let reasoning = '';
|
| 96 |
let usage: TokenUsage | null = null;
|
|
|
|
| 126 |
timestamp: end - start,
|
| 127 |
loading: false,
|
| 128 |
messages,
|
| 129 |
+
usage,
|
| 130 |
+
inferenceProvider
|
| 131 |
} as Record<string, unknown>,
|
| 132 |
{ replace: true }
|
| 133 |
);
|
src/lib/components/chat/Assistant.svelte
CHANGED
|
@@ -18,9 +18,8 @@
|
|
| 18 |
import { Button } from '$lib/components/ui/button';
|
| 19 |
import Message from './Message.svelte';
|
| 20 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
| 21 |
-
import { formatUsageCost } from '$lib';
|
| 22 |
import { modelsState } from '$lib/state/models.svelte';
|
| 23 |
-
import { PROVIDER_SELECTION_MODES } from '$lib/consts';
|
| 24 |
import ListModels from '$lib/components/model/ListModels.svelte';
|
| 25 |
|
| 26 |
let { id }: NodeProps = $props();
|
|
@@ -46,6 +45,9 @@
|
|
| 46 |
} as unknown as ChatMessage)
|
| 47 |
: null
|
| 48 |
);
|
|
|
|
|
|
|
|
|
|
| 49 |
let containerRef: HTMLDivElement | null = $state(null);
|
| 50 |
let articleRef: HTMLElement | null = $state(null);
|
| 51 |
let selectedText = $state<string | null>(null);
|
|
@@ -155,7 +157,7 @@
|
|
| 155 |
{/if}
|
| 156 |
{#if message}
|
| 157 |
<div bind:this={containerRef}>
|
| 158 |
-
<Message {message} />
|
| 159 |
</div>
|
| 160 |
{/if}
|
| 161 |
</div>
|
|
@@ -180,21 +182,12 @@
|
|
| 180 |
<span class="inline-flex items-center gap-0.5">
|
| 181 |
Using
|
| 182 |
<span class="inline-flex items-center gap-1 rounded-full bg-muted py-0.5 pr-2 pl-1">
|
| 183 |
-
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
</span>
|
| 190 |
-
{:else}
|
| 191 |
-
<img
|
| 192 |
-
src={`https://huggingface.co/api/avatars/${provider}`}
|
| 193 |
-
alt={provider}
|
| 194 |
-
class="size-4 rounded-full"
|
| 195 |
-
/>
|
| 196 |
-
{/if}
|
| 197 |
-
{provider}
|
| 198 |
</span>
|
| 199 |
provider
|
| 200 |
</span>
|
|
|
|
| 18 |
import { Button } from '$lib/components/ui/button';
|
| 19 |
import Message from './Message.svelte';
|
| 20 |
import Spinner from '$lib/components/loading/Spinner.svelte';
|
| 21 |
+
import { formatUsageCost, getProviderName } from '$lib';
|
| 22 |
import { modelsState } from '$lib/state/models.svelte';
|
|
|
|
| 23 |
import ListModels from '$lib/components/model/ListModels.svelte';
|
| 24 |
|
| 25 |
let { id }: NodeProps = $props();
|
|
|
|
| 45 |
} as unknown as ChatMessage)
|
| 46 |
: null
|
| 47 |
);
|
| 48 |
+
let inferenceProvider = $derived(
|
| 49 |
+
getProviderName((nodeData.current?.data.inferenceProvider as string) ?? null)
|
| 50 |
+
);
|
| 51 |
let containerRef: HTMLDivElement | null = $state(null);
|
| 52 |
let articleRef: HTMLElement | null = $state(null);
|
| 53 |
let selectedText = $state<string | null>(null);
|
|
|
|
| 157 |
{/if}
|
| 158 |
{#if message}
|
| 159 |
<div bind:this={containerRef}>
|
| 160 |
+
<Message {message} nodeId={id} />
|
| 161 |
</div>
|
| 162 |
{/if}
|
| 163 |
</div>
|
|
|
|
| 182 |
<span class="inline-flex items-center gap-0.5">
|
| 183 |
Using
|
| 184 |
<span class="inline-flex items-center gap-1 rounded-full bg-muted py-0.5 pr-2 pl-1">
|
| 185 |
+
<img
|
| 186 |
+
src={`https://huggingface.co/api/avatars/${inferenceProvider}`}
|
| 187 |
+
alt={inferenceProvider}
|
| 188 |
+
class="size-4 rounded-full"
|
| 189 |
+
/>
|
| 190 |
+
{inferenceProvider}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 191 |
</span>
|
| 192 |
provider
|
| 193 |
</span>
|
src/lib/index.ts
CHANGED
|
@@ -28,7 +28,8 @@ export function formatUsageCost(pricing: ChatModelProvider['pricing'], usage: To
|
|
| 28 |
|
| 29 |
export function getProviderName(provider: string) {
|
| 30 |
const providersMap = {
|
| 31 |
-
together: 'togethercomputer'
|
|
|
|
| 32 |
};
|
| 33 |
return providersMap[provider as keyof typeof providersMap] ?? provider;
|
| 34 |
}
|
|
|
|
| 28 |
|
| 29 |
export function getProviderName(provider: string) {
|
| 30 |
const providersMap = {
|
| 31 |
+
together: 'togethercomputer',
|
| 32 |
+
sambanova: 'sambanovasystems'
|
| 33 |
};
|
| 34 |
return providersMap[provider as keyof typeof providersMap] ?? provider;
|
| 35 |
}
|
src/routes/api/+server.ts
CHANGED
|
@@ -21,31 +21,43 @@ export async function POST({ request }: RequestEvent) {
|
|
| 21 |
|
| 22 |
const client = new InferenceClient(token);
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
|
|
|
|
|
|
|
|
|
| 49 |
const content = chunk.choices?.[0]?.delta?.content ?? '';
|
| 50 |
const reasoningContent = chunk.choices?.[0]?.delta?.reasoning_content ?? '';
|
| 51 |
if (chunk.usage) {
|
|
@@ -70,24 +82,40 @@ export async function POST({ request }: RequestEvent) {
|
|
| 70 |
}
|
| 71 |
controller.enqueue(encoder.encode(content));
|
| 72 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 73 |
}
|
| 74 |
-
if (lastUsage) {
|
| 75 |
-
controller.enqueue(encoder.encode(`\n\n__USAGE__${JSON.stringify(lastUsage)}`));
|
| 76 |
-
}
|
| 77 |
-
} catch (err) {
|
| 78 |
-
const message = err instanceof Error ? err.message : 'An unknown error occurred';
|
| 79 |
-
controller.enqueue(encoder.encode(`\n\n__ERROR__${message}`));
|
| 80 |
-
} finally {
|
| 81 |
-
controller.close();
|
| 82 |
}
|
| 83 |
-
}
|
| 84 |
-
});
|
| 85 |
|
| 86 |
-
|
| 87 |
-
headers: {
|
| 88 |
'Content-Type': 'text/plain; charset=utf-8',
|
| 89 |
'Cache-Control': 'no-cache',
|
| 90 |
Connection: 'keep-alive'
|
|
|
|
|
|
|
|
|
|
| 91 |
}
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
|
|
|
| 21 |
|
| 22 |
const client = new InferenceClient(token);
|
| 23 |
|
| 24 |
+
let resolvedProvider: string | null = null;
|
| 25 |
+
const interceptFetch: typeof fetch = async (...args) => {
|
| 26 |
+
const response = await fetch(...args);
|
| 27 |
+
resolvedProvider = response.headers.get('x-inference-provider');
|
| 28 |
+
return response;
|
| 29 |
+
};
|
| 30 |
+
|
| 31 |
+
try {
|
| 32 |
+
const stream = client.chatCompletionStream(
|
| 33 |
+
{
|
| 34 |
+
model: model + (provider !== 'auto' ? `:${provider}` : ''),
|
| 35 |
+
...(options ?? {}),
|
| 36 |
+
messages: [
|
| 37 |
+
{
|
| 38 |
+
role: 'system',
|
| 39 |
+
content:
|
| 40 |
+
"You are a helpful assistant. You are very helpful and friendly. Use markdown to format your responses, but don't include array start and end markers."
|
| 41 |
+
},
|
| 42 |
+
...(messages ?? [])
|
| 43 |
+
]
|
| 44 |
+
},
|
| 45 |
+
{
|
| 46 |
+
...(billingTo === 'personal' ? {} : { billTo: billingTo }),
|
| 47 |
+
fetch: interceptFetch
|
| 48 |
+
}
|
| 49 |
+
);
|
| 50 |
|
| 51 |
+
const firstResult = await stream.next();
|
| 52 |
+
|
| 53 |
+
const readable = new ReadableStream({
|
| 54 |
+
async start(controller) {
|
| 55 |
+
const encoder = new TextEncoder();
|
| 56 |
+
let lastUsage: Record<string, unknown> | null = null;
|
| 57 |
+
let reasoning: string | null = '';
|
| 58 |
+
|
| 59 |
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
| 60 |
+
const processChunk = (chunk: any) => {
|
| 61 |
const content = chunk.choices?.[0]?.delta?.content ?? '';
|
| 62 |
const reasoningContent = chunk.choices?.[0]?.delta?.reasoning_content ?? '';
|
| 63 |
if (chunk.usage) {
|
|
|
|
| 82 |
}
|
| 83 |
controller.enqueue(encoder.encode(content));
|
| 84 |
}
|
| 85 |
+
};
|
| 86 |
+
|
| 87 |
+
try {
|
| 88 |
+
if (!firstResult.done && firstResult.value) {
|
| 89 |
+
processChunk(firstResult.value);
|
| 90 |
+
}
|
| 91 |
+
for await (const chunk of stream) {
|
| 92 |
+
processChunk(chunk);
|
| 93 |
+
}
|
| 94 |
+
if (lastUsage) {
|
| 95 |
+
controller.enqueue(encoder.encode(`\n\n__USAGE__${JSON.stringify(lastUsage)}`));
|
| 96 |
+
}
|
| 97 |
+
} catch (err) {
|
| 98 |
+
const message = err instanceof Error ? err.message : 'An unknown error occurred';
|
| 99 |
+
controller.enqueue(encoder.encode(`\n\n__ERROR__${message}`));
|
| 100 |
+
} finally {
|
| 101 |
+
controller.close();
|
| 102 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 103 |
}
|
| 104 |
+
});
|
|
|
|
| 105 |
|
| 106 |
+
const responseHeaders: Record<string, string> = {
|
|
|
|
| 107 |
'Content-Type': 'text/plain; charset=utf-8',
|
| 108 |
'Cache-Control': 'no-cache',
|
| 109 |
Connection: 'keep-alive'
|
| 110 |
+
};
|
| 111 |
+
if (resolvedProvider) {
|
| 112 |
+
responseHeaders['x-inference-provider'] = resolvedProvider;
|
| 113 |
}
|
| 114 |
+
return new Response(readable, { headers: responseHeaders });
|
| 115 |
+
} catch (error) {
|
| 116 |
+
return json(
|
| 117 |
+
{ error: error instanceof Error ? error.message : 'An unknown error occurred' },
|
| 118 |
+
{ status: 500 }
|
| 119 |
+
);
|
| 120 |
+
}
|
| 121 |
}
|