managarten/services/llm-playground/src/lib/api/llm.ts
Till-JS f880ef2b7f feat(llm-playground): add SvelteKit LLM playground UI
- Chat interface with message history
- Model selector for available LLM models
- Parameter panel (temperature, max tokens, etc.)
- System prompt editor
- Svelte 5 runes-based stores

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 17:52:10 +01:00

112 lines
2.9 KiB
TypeScript

import type {
ChatCompletionRequest,
ChatCompletionResponse,
HealthResponse,
ModelsResponse,
StreamChunk,
} from '$lib/types';
import { env } from '$env/dynamic/public';
const API_BASE = env.PUBLIC_MANA_LLM_URL || 'http://localhost:3025';
export async function getHealth(): Promise<HealthResponse> {
const response = await fetch(`${API_BASE}/health`);
if (!response.ok) {
throw new Error(`Health check failed: ${response.statusText}`);
}
return response.json();
}
export async function getModels(): Promise<ModelsResponse> {
const response = await fetch(`${API_BASE}/v1/models`);
if (!response.ok) {
throw new Error(`Failed to fetch models: ${response.statusText}`);
}
return response.json();
}
export async function sendCompletion(
request: ChatCompletionRequest
): Promise<ChatCompletionResponse> {
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, stream: false }),
});
if (!response.ok) {
throw new Error(`Completion failed: ${response.statusText}`);
}
return response.json();
}
export async function* streamCompletion(
request: ChatCompletionRequest
): AsyncGenerator<string, void, unknown> {
const response = await fetch(`${API_BASE}/v1/chat/completions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ...request, stream: true }),
});
if (!response.ok) {
const error = await response.text();
throw new Error(`Completion failed: ${response.statusText} - ${error}`);
}
if (!response.body) {
throw new Error('Response body is null');
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
try {
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() || '';
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const data = trimmed.slice(6);
if (data === '[DONE]') return;
try {
const parsed: StreamChunk = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) yield content;
} catch {
// Ignore malformed JSON chunks
}
}
}
// Process remaining buffer
if (buffer.trim()) {
const lines = buffer.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || !trimmed.startsWith('data: ')) continue;
const data = trimmed.slice(6);
if (data === '[DONE]') return;
try {
const parsed: StreamChunk = JSON.parse(data);
const content = parsed.choices[0]?.delta?.content;
if (content) yield content;
} catch {
// Ignore malformed JSON chunks
}
}
}
} finally {
reader.releaseLock();
}
}