Spaces:
Running
Running
| // 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); | |
| } | |
| } | |
| } | |