// Streaming client for Inkling via the Hugging Face Inference Providers router. export const MODEL_ID = "thinkingmachines/Inkling"; const ENDPOINT = "https://router.huggingface.co/together/v1/chat/completions"; /** Build the single multimodal user message. */ export function buildMessage(prompt, imageUri, audioB64) { const parts = []; if (imageUri) { parts.push({ type: "image_url", image_url: { url: imageUri } }); } if (audioB64) { parts.push({ type: "input_audio", input_audio: { data: audioB64, format: "wav" } }); } parts.push({ type: "text", text: prompt }); return { role: "user", content: parts }; } /** Pull a human-readable message out of a provider error body. */ function describeError(status, body) { try { const parsed = JSON.parse(body); const inner = parsed?.error?.message ?? parsed?.message; if (typeof inner === "string") return inner; if (inner?.message) return inner.message; } catch { /* fall through to the raw body */ } return body?.slice(0, 400) || `Request failed with status ${status}.`; } /** * Stream a completion, invoking callbacks as tokens arrive. * * Inkling is a reasoning model: it emits `reasoning` deltas before committing * to `content`, so both are surfaced separately. */ export async function streamCompletion({ token, prompt, imageUri = null, audioB64 = null, maxTokens = 2048, temperature = 0.7, signal, onReasoning = () => {}, onContent = () => {}, }) { const response = await fetch(ENDPOINT, { method: "POST", signal, headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", }, body: JSON.stringify({ model: MODEL_ID, messages: [buildMessage(prompt, imageUri, audioB64)], max_tokens: maxTokens, temperature, stream: true, }), }); if (!response.ok) { throw new Error(describeError(response.status, await response.text())); } const reader = response.body.getReader(); const decoder = new TextDecoder(); let buffer = ""; for (;;) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split("\n"); // Keep the trailing fragment; it may be half an event. buffer = lines.pop() ?? ""; for (const line of lines) { const trimmed = line.trim(); if (!trimmed.startsWith("data:")) continue; const payload = trimmed.slice(5).trim(); if (payload === "[DONE]") return; let parsed; try { parsed = JSON.parse(payload); } catch { continue; // Ignore keep-alives and partial frames. } const delta = parsed?.choices?.[0]?.delta; if (!delta) continue; const thought = delta.reasoning ?? delta.reasoning_content; if (thought) onReasoning(thought); if (delta.content) onContent(delta.content); } } }