@tanstack/ai-grok 0.14.11 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"adapter.js","names":[],"sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeAdapter,\n RealtimeConnection,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { GrokRealtimeOptions } from './types'\n\nconst GROK_REALTIME_URL = 'https://api.x.ai/v1/realtime'\n\n/**\n * Runtime-checked field readers for untyped server events. Replace the\n * drive-by `event.X as string` / `event.X as Record<string, unknown>` casts\n * with readers that return `undefined` when the shape doesn't match, so a\n * malformed frame can't throw a TypeError inside `handleServerEvent`.\n */\nfunction readString(\n obj: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = obj[key]\n return typeof value === 'string' ? value : undefined\n}\n\nfunction readObject(\n obj: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined {\n const value = obj[key]\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\n\nfunction readObjectArray(\n obj: Record<string, unknown>,\n key: string,\n): Array<Record<string, unknown>> | undefined {\n const value = obj[key]\n if (!Array.isArray(value)) return undefined\n return value.filter(\n (item): item is Record<string, unknown> =>\n item !== null && typeof item === 'object' && !Array.isArray(item),\n )\n}\n\ntype RealtimeServerError = Error & {\n code?: string\n type?: string\n param?: string\n}\n\n/**\n * Creates a Grok realtime adapter for client-side use.\n *\n * Uses WebRTC for browser connections (default). Mirrors the OpenAI realtime\n * adapter because xAI's Voice Agent API is OpenAI-realtime-compatible — the\n * only differences are the endpoint URL and default model.\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { grokRealtime } from '@tanstack/ai-grok'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: grokRealtime(),\n * })\n * ```\n */\nexport function grokRealtime(\n options: GrokRealtimeOptions = {},\n): RealtimeAdapter {\n const connectionMode = options.connectionMode ?? 'webrtc'\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'grok',\n\n async connect(\n token: RealtimeToken,\n _clientTools?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n const model = token.config.model ?? 'grok-voice-fast-1.0'\n logger.request(`activity=realtime provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n if (connectionMode === 'webrtc') {\n return createWebRTCConnection(token, logger)\n }\n const error = new Error('WebSocket connection mode not yet implemented')\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime',\n })\n throw error\n },\n }\n}\n\n/**\n * Creates a WebRTC connection to xAI's realtime API.\n */\nasync function createWebRTCConnection(\n token: RealtimeToken,\n logger: InternalLogger,\n): Promise<RealtimeConnection> {\n const model = token.config.model ?? 'grok-voice-fast-1.0'\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n\n const pc = new RTCPeerConnection()\n\n let audioContext: AudioContext | null = null\n let inputAnalyser: AnalyserNode | null = null\n let outputAnalyser: AnalyserNode | null = null\n let inputSource: MediaStreamAudioSourceNode | null = null\n let outputSource: MediaStreamAudioSourceNode | null = null\n let localStream: MediaStream | null = null\n\n let audioElement: HTMLAudioElement | null = null\n\n // Captured into a const so closures see a non-nullable reference (teardown\n // re-points the outer `dataChannel` to null, but in-flight closures still\n // need to close their own channel).\n const channel = pc.createDataChannel('oai-events')\n let dataChannel: RTCDataChannel | null = channel\n\n let currentMode: RealtimeMode = 'idle'\n let currentMessageId: string | null = null\n\n // Flipped by `teardownConnection`. Guards `sendEvent` so post-disconnect\n // calls (e.g. a React `useEffect` cleanup flushing queued events) are\n // logged and skipped instead of silently piling up in `pendingEvents`.\n let isTornDown = false\n\n // Outbound events queued while the data channel isn't yet open. Declared\n // here (rather than next to `sendEvent`) so `teardownConnection` — which\n // lives higher up and can run from the SDP-path catch before `sendEvent`\n // is defined — can drain it without hitting the TDZ.\n const pendingEvents: Array<Record<string, unknown>> = []\n\n // Tracks whether we've sent the first session.update. On the first update\n // we attach a default input_audio_transcription so the server will emit\n // user transcripts unless the caller opts out via\n // `providerOptions.inputAudioTranscription = null | false`.\n let hasSentInitialSessionUpdate = false\n\n // Size hints for the fallback buffers returned when an analyser isn't yet\n // populated. We return a *fresh* `Uint8Array` on each call so a caller\n // that draws into it (e.g. a canvas visualiser zeroing the buffer) can't\n // mutate a shared module-level instance for every other consumer.\n const FALLBACK_FREQUENCY_BIN_COUNT = 1024\n const FALLBACK_TIME_DOMAIN_SIZE = 2048\n const FALLBACK_TIME_DOMAIN_FILL = 128\n\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n let dataChannelOpened = false\n let rejectDataChannelReady: ((reason: unknown) => void) | null = null\n let dataChannelReadyTimeout: ReturnType<typeof setTimeout> | null = null\n\n const dataChannelReady = new Promise<void>((resolve, reject) => {\n rejectDataChannelReady = (reason) => {\n if (dataChannelReadyTimeout !== null) {\n clearTimeout(dataChannelReadyTimeout)\n dataChannelReadyTimeout = null\n }\n // One-shot: null out so later state transitions don't reject twice.\n rejectDataChannelReady = null\n reject(reason)\n }\n\n dataChannelReadyTimeout = setTimeout(() => {\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(\n new Error(\n 'Data channel did not open within 15000ms — aborting connection',\n ),\n )\n }\n }, 15000)\n\n channel.onopen = () => {\n dataChannelOpened = true\n if (dataChannelReadyTimeout !== null) {\n clearTimeout(dataChannelReadyTimeout)\n dataChannelReadyTimeout = null\n }\n // Once resolved, rejecting is a no-op — null out so teardown paths\n // don't attempt a redundant reject on an already-settled promise.\n rejectDataChannelReady = null\n flushPendingEvents()\n emit('status_change', { status: 'connected' })\n resolve()\n }\n })\n\n channel.onmessage = (event) => {\n try {\n const message = JSON.parse(event.data)\n const messageRecord: Record<string, unknown> =\n message !== null && typeof message === 'object' ? message : {}\n logger.provider(\n `provider=grok direction=in type=${readString(messageRecord, 'type') ?? '<unknown>'}`,\n { frame: messageRecord },\n )\n handleServerEvent(messageRecord)\n } catch (parseErr) {\n logger.errors('grok.realtime fatal', {\n error: parseErr,\n source: 'grok.realtime',\n })\n emit('error', {\n error:\n parseErr instanceof Error ? parseErr : new Error(String(parseErr)),\n })\n }\n }\n\n channel.onerror = (error) => {\n // Closing the peer connection cascades into `onerror`/`onclose` on the\n // data channel. Once teardown has started, re-surfacing those as\n // `emit('error')` is noise that confuses consumers (they just called\n // `disconnect()` — they don't want an error toast for it).\n if (isTornDown) return\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime',\n })\n // RTCErrorEvent exposes a typed `.error`; fall back to the event type\n // name, then to a string representation, so the emitted error message\n // doesn't end up as \"[object Event]\".\n // `onerror` always fires with an Event (often an RTCErrorEvent), so we\n // can read it via the untyped helpers without first proving object-ness.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- RTCErrorEvent is a typed DOM class that does not structurally overlap Record<string, unknown>; we duck-type it via readObject/readString\n const errorRecord = error as unknown as Record<string, unknown>\n const rtcError = readObject(errorRecord, 'error')\n const msg =\n (rtcError && readString(rtcError, 'message')) ?? (error.type || 'unknown')\n const dcErr = new Error(`Data channel error: ${msg}`)\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(dcErr)\n }\n emit('error', { error: dcErr })\n }\n\n channel.onclose = () => {\n // Same rationale as `onerror` above: `pc.close()` during teardown\n // cascades to the data channel's `onclose`. If we've already started\n // teardown, there's nothing to do here.\n if (isTornDown) return\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(new Error('Data channel closed before opening'))\n }\n }\n\n pc.ontrack = (event) => {\n if (event.track.kind === 'audio' && event.streams[0]) {\n setupOutputAudioAnalysis(event.streams[0])\n }\n }\n\n // `status_change` has a single source of truth: `onconnectionstatechange`\n // (the higher-level aggregate state). `oniceconnectionstatechange` is\n // responsible only for rejecting `dataChannelReady` on ICE failures so we\n // surface them without waiting for the 15s timeout.\n pc.onconnectionstatechange = () => {\n const state = pc.connectionState\n logger.provider(`provider=grok pc.connectionState=${state}`, {\n state,\n })\n if (state === 'failed' || state === 'disconnected' || state === 'closed') {\n // Suppress the `status_change` emission when teardown is in progress:\n // the user-facing `disconnect()` already emits `status_change: 'idle'`\n // and then calls `teardownConnection()` → `pc.close()`, which fires\n // `onconnectionstatechange` with state === 'closed'. Without this\n // guard listeners would see two `idle` events per disconnect.\n if (!isTornDown) {\n emit('status_change', {\n status: state === 'failed' ? 'error' : 'idle',\n })\n }\n if (!dataChannelOpened) {\n // Reject on any terminal-ish pre-open state so callers don't hang\n // for the full 15s timeout. The reject is one-shot — subsequent\n // state changes become no-ops via the null-out in\n // `rejectDataChannelReady`.\n const message =\n state === 'failed'\n ? `PeerConnection failed before data channel opened`\n : `PeerConnection entered state '${state}' before data channel opened`\n rejectDataChannelReady?.(new Error(message))\n }\n // Auto-teardown on `failed`: without this the mic track, pc, and\n // AudioContext stay allocated after a fatal connection failure, so the\n // browser's mic indicator stays on and the user sees a broken\n // \"connected mic\" state. `closed` already means pc was torn down\n // (usually by teardownConnection itself) so nothing extra to do.\n // `disconnected` is transient per the WebRTC spec and may recover, so\n // we leave resources in place. `teardownConnection` is idempotent so\n // a subsequent consumer `disconnect()` remains safe.\n if (state === 'failed' && !isTornDown) {\n void teardownConnection()\n }\n }\n }\n\n pc.oniceconnectionstatechange = () => {\n const state = pc.iceConnectionState\n logger.provider(`provider=grok pc.iceConnectionState=${state}`, {\n state,\n })\n if (\n !dataChannelOpened &&\n (state === 'failed' || state === 'closed' || state === 'disconnected')\n ) {\n const message =\n state === 'failed'\n ? `ICE connection failed before data channel opened`\n : `ICE connection entered state '${state}' before data channel opened`\n rejectDataChannelReady?.(new Error(message))\n }\n }\n\n /**\n * Tear down every resource we may have allocated so the mic/pc/audio\n * nodes/audio element don't leak on a failed connect. Safe to call from\n * any point after `new RTCPeerConnection()`; each branch null-guards and\n * swallows errors because cascading closes (e.g. `pc.close()` closing the\n * data channel implicitly) are expected.\n *\n * Shared between the SDP-path catch, the post-SDP catch, and (implicitly\n * via idempotency) the `disconnect()` entry point.\n */\n async function teardownConnection() {\n // Flip the teardown flag BEFORE any awaits so handlers that fire during\n // `await audioContext.close()` (or any other async step below) can guard\n // on it — otherwise a late `pc.onconnectionstatechange` or `pc.ontrack`\n // can allocate new resources or re-emit `status_change: idle` after the\n // user-facing `disconnect()` already emitted one.\n isTornDown = true\n\n // Drop any queued events the caller sent before the data channel opened\n // up front. Without this they'd accumulate across reconnect attempts\n // (each connect allocates a fresh closure, but a caller holding the old\n // `connection` reference could otherwise keep appending forever). Done\n // at the top — before the awaits below — so `sendEvent` calls racing\n // with teardown don't push into a list we're about to drain.\n pendingEvents.length = 0\n\n // Clear the data-channel-open timeout / reject the readiness promise\n // if it's still pending. `rejectDataChannelReady` is one-shot and nulls\n // itself on first call, so calling it from `disconnect()` after a\n // successful open is a no-op.\n rejectDataChannelReady?.(\n new Error('Connection torn down before data channel opened'),\n )\n\n if (localStream) {\n for (const track of localStream.getTracks()) {\n track.stop()\n }\n localStream = null\n }\n\n // Output audio (populated by `pc.ontrack` → setupOutputAudioAnalysis,\n // which may have fired during SDP negotiation before we threw).\n if (audioElement) {\n try {\n audioElement.pause()\n } catch {\n // ignore — element may already be unloaded\n }\n audioElement.srcObject = null\n audioElement = null\n }\n if (outputSource) {\n try {\n outputSource.disconnect()\n } catch {\n // ignore\n }\n outputSource = null\n }\n if (outputAnalyser) {\n try {\n outputAnalyser.disconnect()\n } catch {\n // ignore\n }\n outputAnalyser = null\n }\n\n // Input audio (populated by setupInputAudioAnalysis after SDP).\n if (inputSource) {\n try {\n inputSource.disconnect()\n } catch {\n // ignore\n }\n inputSource = null\n }\n if (inputAnalyser) {\n try {\n inputAnalyser.disconnect()\n } catch {\n // ignore\n }\n inputAnalyser = null\n }\n\n if (dataChannel) {\n try {\n dataChannel.close()\n } catch {\n // ignore — channel may already be closed by pc.close()\n }\n dataChannel = null\n }\n\n try {\n pc.close()\n } catch {\n // ignore — pc may already be closed\n }\n\n if (audioContext) {\n try {\n await audioContext.close()\n } catch {\n // ignore — context may already be closed\n }\n audioContext = null\n }\n }\n\n // xAI requires an audio track in the SDP offer, same as OpenAI realtime.\n //\n // This try/catch also covers `getUserMedia` failure (e.g. the user denies\n // microphone permission). `pc` + `dataChannel` are already allocated above\n // and the 15s `dataChannelReady` timeout is already armed, so we MUST\n // teardown on failure here — otherwise they leak until the tab closes.\n // `teardownConnection` is idempotent and null-safe (runs fine even if the\n // mic was never acquired).\n try {\n try {\n localStream = await navigator.mediaDevices.getUserMedia({\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n sampleRate: 24000,\n },\n })\n } catch (error) {\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime.getUserMedia',\n })\n // Re-throw with the descriptive message callers rely on. Teardown runs\n // in the outer catch below.\n throw new Error(\n `Microphone access required for realtime voice: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n for (const track of localStream.getAudioTracks()) {\n pc.addTrack(track, localStream)\n }\n\n const offer = await pc.createOffer()\n await pc.setLocalDescription(offer)\n\n const sdpResponse = await fetch(`${GROK_REALTIME_URL}?model=${model}`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token.token}`,\n 'Content-Type': 'application/sdp',\n },\n body: offer.sdp,\n })\n\n if (!sdpResponse.ok) {\n const errorText = await sdpResponse.text()\n const error = new Error(\n `Failed to establish WebRTC connection: ${sdpResponse.status} - ${errorText}`,\n )\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime.sdp',\n status: sdpResponse.status,\n })\n throw error\n }\n\n const answerSdp = await sdpResponse.text()\n await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp })\n } catch (err) {\n await teardownConnection()\n throw err\n }\n\n // Second cleanup scope: after SDP succeeds we still have to set up input\n // audio analysis and wait for the data channel to open. Both can fail\n // (AudioContext allocation, 15s timeout, ICE failure, pc.close from the\n // other end, etc.) and those failures must NOT leave the mic/pc/audio\n // nodes running.\n try {\n setupInputAudioAnalysis(localStream)\n await dataChannelReady\n } catch (err) {\n await teardownConnection()\n throw err\n }\n\n function handleServerEvent(event: Record<string, unknown>) {\n const type = readString(event, 'type')\n\n switch (type) {\n case 'session.created':\n case 'session.updated':\n break\n\n case 'input_audio_buffer.speech_started':\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n break\n\n case 'input_audio_buffer.speech_stopped':\n currentMode = 'thinking'\n emit('mode_change', { mode: 'thinking' })\n break\n\n case 'input_audio_buffer.committed':\n break\n\n case 'conversation.item.input_audio_transcription.completed': {\n const transcript = readString(event, 'transcript')\n if (transcript === undefined) break\n emit('transcript', { role: 'user', transcript, isFinal: true })\n break\n }\n\n case 'response.created':\n // Reset message id so a tool-only response (which never emits\n // response.output_item.added for a message) can't reuse the previous\n // turn's id when `response.done` later inspects this flag.\n currentMessageId = null\n currentMode = 'thinking'\n emit('mode_change', { mode: 'thinking' })\n break\n\n case 'response.output_item.added': {\n const item = readObject(event, 'item')\n if (item && readString(item, 'type') === 'message') {\n const id = readString(item, 'id')\n if (id !== undefined) currentMessageId = id\n }\n break\n }\n\n // xAI realtime per docs uses `response.output_audio_transcript.*`;\n // accept the legacy OpenAI-realtime `response.audio_transcript.*` as\n // an alias so this adapter stays compatible across protocol versions.\n case 'response.output_audio_transcript.delta':\n case 'response.audio_transcript.delta': {\n const delta = readString(event, 'delta')\n if (delta === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: delta,\n isFinal: false,\n })\n break\n }\n\n case 'response.output_audio_transcript.done':\n case 'response.audio_transcript.done': {\n const transcript = readString(event, 'transcript')\n if (transcript === undefined) break\n emit('transcript', { role: 'assistant', transcript, isFinal: true })\n break\n }\n\n // xAI realtime per docs uses `response.text.*`; accept the legacy\n // OpenAI-realtime `response.output_text.*` as an alias.\n case 'response.text.delta':\n case 'response.output_text.delta': {\n const delta = readString(event, 'delta')\n if (delta === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: delta,\n isFinal: false,\n })\n break\n }\n\n case 'response.text.done':\n case 'response.output_text.done': {\n const text = readString(event, 'text')\n if (text === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: text,\n isFinal: true,\n })\n break\n }\n\n // xAI realtime per docs uses `response.output_audio.*`; accept the\n // legacy OpenAI-realtime `response.audio.*` as an alias.\n case 'response.output_audio.delta':\n case 'response.audio.delta':\n if (currentMode !== 'speaking') {\n currentMode = 'speaking'\n emit('mode_change', { mode: 'speaking' })\n }\n break\n\n case 'response.output_audio.done':\n case 'response.audio.done':\n break\n\n case 'response.function_call_arguments.done': {\n // Only `call_id` is valid for `sendToolResult` correlation. Falling\n // back to `item_id` would produce a tool-call id the server doesn't\n // recognise when the result is posted back, silently dropping the\n // tool execution. If `call_id` is missing we surface an error event\n // so the UI can react instead of pretending the tool call succeeded.\n const callId = readString(event, 'call_id')\n const name = readString(event, 'name') ?? ''\n const args = readString(event, 'arguments') ?? ''\n if (!callId) {\n logger.errors(\n 'grok.realtime tool_call missing call_id — dropping tool_call',\n {\n source: 'grok.realtime',\n event_type: 'response.function_call_arguments.done',\n item_id: event.item_id,\n },\n )\n emit('error', {\n error: new Error(\n 'Realtime tool call missing call_id; tool will not execute',\n ),\n })\n break\n }\n try {\n const input = JSON.parse(args)\n emit('tool_call', { toolCallId: callId, toolName: name, input })\n } catch {\n emit('tool_call', { toolCallId: callId, toolName: name, input: args })\n }\n break\n }\n\n case 'response.done': {\n const response = readObject(event, 'response') ?? {}\n const output = readObjectArray(response, 'output')\n\n // Only transition back to `listening` if the user hasn't already\n // stopped capture — otherwise we'd override their explicit `idle`\n // state and re-arm the mic visualisation.\n if (currentMode !== 'idle') {\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n }\n\n if (currentMessageId) {\n const message: RealtimeMessage = {\n id: currentMessageId,\n role: 'assistant',\n timestamp: Date.now(),\n parts: [],\n }\n\n for (const item of output ?? []) {\n if (readString(item, 'type') !== 'message') continue\n const content = readObjectArray(item, 'content')\n if (!content) continue\n for (const part of content) {\n const partType = readString(part, 'type')\n if (partType === 'audio') {\n const transcript = readString(part, 'transcript')\n if (transcript) {\n message.parts.push({ type: 'audio', transcript })\n }\n } else if (partType === 'text') {\n const content = readString(part, 'text')\n if (content) {\n message.parts.push({ type: 'text', content })\n }\n }\n }\n }\n\n emit('message_complete', { message })\n currentMessageId = null\n }\n break\n }\n\n case 'conversation.item.truncated':\n // Assistant playback was interrupted — flip mode back to `listening`\n // unless the user already called `stopAudioCapture()` (idle). Without\n // this the visualisation would stay stuck on `speaking` even though\n // no audio is playing.\n if (currentMode !== 'idle') {\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n }\n emit('interrupted', {\n ...(currentMessageId !== null && { messageId: currentMessageId }),\n })\n break\n\n case 'error': {\n // The realtime server's `error` envelope isn't guaranteed to carry\n // an `error` object at all (network-layer corruption, protocol\n // drift, etc.). Validate shape before dereferencing so a malformed\n // payload can't throw a TypeError inside this handler and stop the\n // switch from running for the rest of the session.\n const errorObj = readObject(event, 'error') ?? {}\n const message =\n readString(errorObj, 'message') ?? 'Unknown realtime server error'\n const err: RealtimeServerError = new Error(message)\n // Preserve `code` / `type` / `param` on the Error as extra props so\n // consumers can branch on them without re-parsing the raw event.\n const code = readString(errorObj, 'code')\n if (code !== undefined) err.code = code\n const errType = readString(errorObj, 'type')\n if (errType !== undefined) err.type = errType\n const param = readString(errorObj, 'param')\n if (param !== undefined) err.param = param\n logger.errors('grok.realtime server error', {\n ...errorObj,\n source: 'grok.realtime server',\n })\n emit('error', { error: err })\n break\n }\n\n case undefined:\n default:\n // The xAI realtime protocol is a moving target; log unhandled event\n // types at provider level so they're visible during debugging without\n // emitting a user-visible error. `undefined` shares the bucket because\n // a malformed event without a `type` field is just as unhandleable.\n logger.provider('grok.realtime unhandled server event', {\n type: event.type,\n })\n break\n }\n }\n\n function setupOutputAudioAnalysis(stream: MediaStream) {\n // Bail out if teardown has already started. `pc.ontrack` can fire\n // asynchronously after `teardownConnection()` has flipped `isTornDown`\n // (e.g. a remote track arriving mid-close); without this guard we'd\n // allocate a fresh AudioContext / audio element that nothing would ever\n // clean up.\n if (isTornDown) return\n\n // Tear down any prior output audio before allocating new resources.\n // `pc.ontrack` can fire multiple times over the lifetime of a session\n // (e.g. after renegotiation), and without this we'd leak audio elements\n // and analyser nodes.\n if (audioElement) {\n try {\n audioElement.pause()\n } catch {\n // ignore — element may already be unloaded\n }\n audioElement.srcObject = null\n audioElement = null\n }\n if (outputSource) {\n try {\n outputSource.disconnect()\n } catch {\n // ignore — may already be disconnected\n }\n outputSource = null\n }\n if (outputAnalyser) {\n try {\n outputAnalyser.disconnect()\n } catch {\n // ignore\n }\n outputAnalyser = null\n }\n\n audioElement = new Audio()\n audioElement.srcObject = stream\n audioElement.autoplay = true\n audioElement.play().catch((e) => {\n // Autoplay is commonly blocked until the user interacts with the page\n // (browser gesture requirement). Surfacing this as a fatal `error`\n // event makes the UI render a red/error state even though the\n // connection is healthy — the page just needs a click. Log at a\n // dedicated source tag so it's debuggable, but don't emit `error`.\n logger.errors('grok.realtime audio autoplay blocked', {\n error: e,\n source: 'grok.realtime.audio_permission_required',\n })\n })\n\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n\n if (audioContext.state === 'suspended') {\n audioContext.resume().catch((err) => {\n // Same rationale as the autoplay catch: `resume()` failure usually\n // means the user hasn't interacted yet. Logging only — no error\n // emit — so the UI doesn't go into a fatal state for a recoverable\n // condition.\n logger.errors('grok.realtime audioContext.resume failed', {\n error: err,\n source: 'grok.realtime',\n })\n })\n }\n\n outputAnalyser = audioContext.createAnalyser()\n outputAnalyser.fftSize = 2048\n outputAnalyser.smoothingTimeConstant = 0.3\n\n outputSource = audioContext.createMediaStreamSource(stream)\n outputSource.connect(outputAnalyser)\n }\n\n function setupInputAudioAnalysis(stream: MediaStream) {\n // Defensive symmetry with `setupOutputAudioAnalysis`. Today this is\n // only called inline after SDP negotiation, but keeping the guard\n // means any future caller path (e.g. renegotiation) won't leak a fresh\n // AudioContext after teardown.\n if (isTornDown) return\n\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n\n if (audioContext.state === 'suspended') {\n audioContext.resume().catch((err) => {\n // Same rationale as in setupOutputAudioAnalysis: a suspended\n // AudioContext usually resumes after a user gesture. Log only —\n // surfacing this as a fatal error makes the UI look broken for a\n // recoverable condition.\n logger.errors('grok.realtime audioContext.resume failed', {\n error: err,\n source: 'grok.realtime',\n })\n })\n }\n\n inputAnalyser = audioContext.createAnalyser()\n inputAnalyser.fftSize = 2048\n inputAnalyser.smoothingTimeConstant = 0.3\n\n inputSource = audioContext.createMediaStreamSource(stream)\n inputSource.connect(inputAnalyser)\n }\n\n function sendEvent(event: Record<string, unknown>) {\n if (isTornDown) {\n // The caller is holding onto a `connection` object after `disconnect()`\n // (or a failed connect). Silently queueing would leak memory and the\n // events would never flush. Log + drop so the misuse is visible in\n // debug mode without escalating to a throw — throwing from a React\n // useEffect cleanup path can break teardown ordering in the UI.\n logger.errors('grok.realtime sendEvent after disconnect', {\n eventType: readString(event, 'type') ?? '<unknown>',\n source: 'grok.realtime',\n })\n return\n }\n if (dataChannel?.readyState === 'open') {\n logger.provider(\n `provider=grok direction=out type=${readString(event, 'type') ?? '<unknown>'}`,\n { frame: event },\n )\n // Mirror the try/catch in `flushPendingEvents` — `dataChannel.send`\n // can synchronously throw if the channel flipped to `closing` between\n // our readyState check and this call, or if `JSON.stringify` chokes\n // on a caller-supplied payload. Log + emit error instead of letting\n // the exception propagate up through public `sendText` / `sendImage`\n // / `updateSession` call sites.\n try {\n dataChannel.send(JSON.stringify(event))\n } catch (error) {\n logger.errors('grok.realtime sendEvent failed', {\n error,\n eventType: readString(event, 'type') ?? '<unknown>',\n source: 'grok.realtime',\n })\n emit('error', {\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n } else {\n pendingEvents.push(event)\n }\n }\n\n function flushPendingEvents() {\n try {\n for (const event of pendingEvents) {\n logger.provider(\n `provider=grok direction=out type=${readString(event, 'type') ?? '<unknown>'}`,\n { frame: event },\n )\n channel.send(JSON.stringify(event))\n }\n pendingEvents.length = 0\n } catch (error) {\n // A send failure here (e.g. dataChannel went from 'open' back to\n // 'closing' mid-flush, or JSON.stringify on a caller-provided event\n // threw) would otherwise be silently swallowed. By the time we're\n // called, `onopen` has already resolved `dataChannelReady`, so the\n // consumer-facing signal is `emit('error')` — try rejectDataChannelReady\n // as a defensive belt-and-braces in case this ever runs pre-resolve.\n logger.errors('grok.realtime flushPendingEvents failed', {\n error,\n source: 'grok.realtime',\n })\n const err = error instanceof Error ? error : new Error(String(error))\n rejectDataChannelReady?.(err)\n emit('error', { error: err })\n }\n }\n\n const connection: RealtimeConnection = {\n async disconnect() {\n // Reuse the same teardown path as the failed-connect branches so\n // every cleanup site stays in sync (input analyser, output analyser,\n // output source, audio element, etc.).\n await teardownConnection()\n emit('status_change', { status: 'idle' })\n },\n\n async startAudioCapture() {\n if (localStream) {\n for (const track of localStream.getAudioTracks()) {\n track.enabled = true\n }\n }\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n if (localStream) {\n for (const track of localStream.getAudioTracks()) {\n track.enabled = false\n }\n }\n currentMode = 'idle'\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'message',\n role: 'user',\n content: [{ type: 'input_text', text }],\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n sendImage(imageData: string, mimeType: string) {\n // Accept:\n // - http(s):// URLs → forward as-is\n // - data: URIs (e.g. from FileReader.readAsDataURL) → forward as-is\n // so we don't double-wrap into `data:image/png;base64,data:image/png;base64,…`\n // - bare base64 → wrap in `data:${mimeType};base64,…`\n const isAlreadyUrlOrDataUri =\n imageData.startsWith('http://') ||\n imageData.startsWith('https://') ||\n imageData.startsWith('data:')\n const imageContent = {\n type: 'input_image',\n // The OpenAI-realtime content part (which this adapter mirrors) nests\n // the URL under an `image_url: { url: ... }` object, not a bare\n // string.\n image_url: {\n url: isAlreadyUrlOrDataUri\n ? imageData\n : `data:${mimeType};base64,${imageData}`,\n },\n }\n\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'message',\n role: 'user',\n content: [imageContent],\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n sendToolResult(callId: string, result: string) {\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'function_call_output',\n call_id: callId,\n output: result,\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n updateSession(config: Partial<RealtimeSessionConfig>) {\n const sessionUpdate: Record<string, unknown> = {}\n\n if (config.instructions) {\n sessionUpdate.instructions = config.instructions\n }\n\n if (config.voice) {\n sessionUpdate.voice = config.voice\n }\n\n if (config.vadMode) {\n if (config.vadMode === 'semantic') {\n sessionUpdate.turn_detection = {\n type: 'semantic_vad',\n eagerness: config.semanticEagerness ?? 'medium',\n }\n } else if (config.vadMode === 'server') {\n sessionUpdate.turn_detection = {\n type: 'server_vad',\n threshold: config.vadConfig?.threshold ?? 0.5,\n prefix_padding_ms: config.vadConfig?.prefixPaddingMs ?? 300,\n silence_duration_ms: config.vadConfig?.silenceDurationMs ?? 500,\n }\n } else {\n sessionUpdate.turn_detection = null\n }\n }\n\n if (config.tools !== undefined) {\n sessionUpdate.tools = config.tools.map((t) => ({\n type: 'function',\n name: t.name,\n description: t.description,\n parameters: t.inputSchema ?? { type: 'object', properties: {} },\n }))\n sessionUpdate.tool_choice = 'auto'\n }\n\n if (config.outputModalities) {\n sessionUpdate.modalities = config.outputModalities\n }\n\n if (config.temperature !== undefined) {\n sessionUpdate.temperature = config.temperature\n }\n\n if (config.maxOutputTokens !== undefined) {\n sessionUpdate.max_response_output_tokens = config.maxOutputTokens\n }\n\n // Let callers forward an explicit `input_audio_transcription` value\n // through `providerOptions` — including `null` / `false` to disable\n // the feature. Only apply our `grok-stt` default on the first\n // session.update and only if the caller hasn't set it themselves.\n const providerOptions: Record<string, unknown> =\n config.providerOptions ?? {}\n const callerTranscription =\n 'inputAudioTranscription' in providerOptions\n ? providerOptions.inputAudioTranscription\n : 'input_audio_transcription' in providerOptions\n ? providerOptions.input_audio_transcription\n : undefined\n if (callerTranscription !== undefined) {\n sessionUpdate.input_audio_transcription =\n callerTranscription === false ? null : callerTranscription\n } else if (!hasSentInitialSessionUpdate) {\n sessionUpdate.input_audio_transcription = { model: 'grok-stt' }\n }\n\n if (Object.keys(sessionUpdate).length > 0) {\n sendEvent({\n type: 'session.update',\n session: sessionUpdate,\n })\n hasSentInitialSessionUpdate = true\n }\n },\n\n interrupt() {\n sendEvent({ type: 'response.cancel' })\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {\n ...(currentMessageId !== null && { messageId: currentMessageId }),\n })\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n function calculateLevel(analyser: AnalyserNode): number {\n const data = new Uint8Array(analyser.fftSize)\n analyser.getByteTimeDomainData(data)\n\n let maxDeviation = 0\n for (const sample of data) {\n const deviation = Math.abs(sample - 128)\n if (deviation > maxDeviation) {\n maxDeviation = deviation\n }\n }\n\n const normalized = maxDeviation / 128\n return Math.min(1, normalized * 1.5)\n }\n\n return {\n get inputLevel() {\n if (!inputAnalyser) return 0\n return calculateLevel(inputAnalyser)\n },\n\n get outputLevel() {\n if (!outputAnalyser) return 0\n return calculateLevel(outputAnalyser)\n },\n\n getInputFrequencyData() {\n if (!inputAnalyser)\n return new Uint8Array(FALLBACK_FREQUENCY_BIN_COUNT)\n const data = new Uint8Array(inputAnalyser.frequencyBinCount)\n inputAnalyser.getByteFrequencyData(data)\n return data\n },\n\n getOutputFrequencyData() {\n if (!outputAnalyser)\n return new Uint8Array(FALLBACK_FREQUENCY_BIN_COUNT)\n const data = new Uint8Array(outputAnalyser.frequencyBinCount)\n outputAnalyser.getByteFrequencyData(data)\n return data\n },\n\n getInputTimeDomainData() {\n if (!inputAnalyser)\n return new Uint8Array(FALLBACK_TIME_DOMAIN_SIZE).fill(\n FALLBACK_TIME_DOMAIN_FILL,\n )\n const data = new Uint8Array(inputAnalyser.fftSize)\n inputAnalyser.getByteTimeDomainData(data)\n return data\n },\n\n getOutputTimeDomainData() {\n if (!outputAnalyser)\n return new Uint8Array(FALLBACK_TIME_DOMAIN_SIZE).fill(\n FALLBACK_TIME_DOMAIN_FILL,\n )\n const data = new Uint8Array(outputAnalyser.fftSize)\n outputAnalyser.getByteTimeDomainData(data)\n return data\n },\n\n get inputSampleRate() {\n return 24000\n },\n\n get outputSampleRate() {\n return 24000\n },\n }\n },\n }\n\n // `dataChannelReady` was already awaited inside the post-SDP try/catch\n // above so we can short-circuit on failures with full teardown.\n return connection\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"adapter.js","names":[],"sources":["../../../src/realtime/adapter.ts"],"sourcesContent":["import { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport { GROK_DEFAULT_REALTIME_MODEL } from '../model-meta'\nimport type {\n AnyClientTool,\n AudioVisualization,\n RealtimeAdapter,\n RealtimeConnection,\n RealtimeEvent,\n RealtimeEventHandler,\n RealtimeMessage,\n RealtimeMode,\n RealtimeSessionConfig,\n RealtimeToken,\n} from '@tanstack/ai'\nimport type { InternalLogger } from '@tanstack/ai/adapter-internals'\nimport type { GrokRealtimeOptions } from './types'\n\nconst GROK_REALTIME_URL = 'https://api.x.ai/v1/realtime'\n\n/**\n * Runtime-checked field readers for untyped server events. Replace the\n * drive-by `event.X as string` / `event.X as Record<string, unknown>` casts\n * with readers that return `undefined` when the shape doesn't match, so a\n * malformed frame can't throw a TypeError inside `handleServerEvent`.\n */\nfunction readString(\n obj: Record<string, unknown>,\n key: string,\n): string | undefined {\n const value = obj[key]\n return typeof value === 'string' ? value : undefined\n}\n\nfunction readObject(\n obj: Record<string, unknown>,\n key: string,\n): Record<string, unknown> | undefined {\n const value = obj[key]\n return value && typeof value === 'object' && !Array.isArray(value)\n ? (value as Record<string, unknown>)\n : undefined\n}\n\nfunction readObjectArray(\n obj: Record<string, unknown>,\n key: string,\n): Array<Record<string, unknown>> | undefined {\n const value = obj[key]\n if (!Array.isArray(value)) return undefined\n return value.filter(\n (item): item is Record<string, unknown> =>\n item !== null && typeof item === 'object' && !Array.isArray(item),\n )\n}\n\ntype RealtimeServerError = Error & {\n code?: string\n type?: string\n param?: string\n}\n\n/**\n * Creates a Grok realtime adapter for client-side use.\n *\n * Uses WebRTC for browser connections (default). Mirrors the OpenAI realtime\n * adapter because xAI's Voice Agent API is OpenAI-realtime-compatible — the\n * only differences are the endpoint URL and default model.\n *\n * @example\n * ```typescript\n * import { RealtimeClient } from '@tanstack/ai-client'\n * import { grokRealtime } from '@tanstack/ai-grok'\n *\n * const client = new RealtimeClient({\n * getToken: () => fetch('/api/realtime-token').then(r => r.json()),\n * adapter: grokRealtime(),\n * })\n * ```\n */\nexport function grokRealtime(\n options: GrokRealtimeOptions = {},\n): RealtimeAdapter {\n const connectionMode = options.connectionMode ?? 'webrtc'\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'grok',\n\n async connect(\n token: RealtimeToken,\n _clientTools?: ReadonlyArray<AnyClientTool>,\n ): Promise<RealtimeConnection> {\n const model = token.config.model ?? GROK_DEFAULT_REALTIME_MODEL\n logger.request(`activity=realtime provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n if (connectionMode === 'webrtc') {\n return createWebRTCConnection(token, logger)\n }\n const error = new Error('WebSocket connection mode not yet implemented')\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime',\n })\n throw error\n },\n }\n}\n\n/**\n * Creates a WebRTC connection to xAI's realtime API.\n */\nasync function createWebRTCConnection(\n token: RealtimeToken,\n logger: InternalLogger,\n): Promise<RealtimeConnection> {\n const model = token.config.model ?? GROK_DEFAULT_REALTIME_MODEL\n const eventHandlers = new Map<RealtimeEvent, Set<RealtimeEventHandler<any>>>()\n\n const pc = new RTCPeerConnection()\n\n let audioContext: AudioContext | null = null\n let inputAnalyser: AnalyserNode | null = null\n let outputAnalyser: AnalyserNode | null = null\n let inputSource: MediaStreamAudioSourceNode | null = null\n let outputSource: MediaStreamAudioSourceNode | null = null\n let localStream: MediaStream | null = null\n\n let audioElement: HTMLAudioElement | null = null\n\n // Captured into a const so closures see a non-nullable reference (teardown\n // re-points the outer `dataChannel` to null, but in-flight closures still\n // need to close their own channel).\n const channel = pc.createDataChannel('oai-events')\n let dataChannel: RTCDataChannel | null = channel\n\n let currentMode: RealtimeMode = 'idle'\n let currentMessageId: string | null = null\n\n // Flipped by `teardownConnection`. Guards `sendEvent` so post-disconnect\n // calls (e.g. a React `useEffect` cleanup flushing queued events) are\n // logged and skipped instead of silently piling up in `pendingEvents`.\n let isTornDown = false\n\n // Outbound events queued while the data channel isn't yet open. Declared\n // here (rather than next to `sendEvent`) so `teardownConnection` — which\n // lives higher up and can run from the SDP-path catch before `sendEvent`\n // is defined — can drain it without hitting the TDZ.\n const pendingEvents: Array<Record<string, unknown>> = []\n\n // Tracks whether we've sent the first session.update. On the first update\n // we attach a default input_audio_transcription so the server will emit\n // user transcripts unless the caller opts out via\n // `providerOptions.inputAudioTranscription = null | false`.\n let hasSentInitialSessionUpdate = false\n\n // Size hints for the fallback buffers returned when an analyser isn't yet\n // populated. We return a *fresh* `Uint8Array` on each call so a caller\n // that draws into it (e.g. a canvas visualiser zeroing the buffer) can't\n // mutate a shared module-level instance for every other consumer.\n const FALLBACK_FREQUENCY_BIN_COUNT = 1024\n const FALLBACK_TIME_DOMAIN_SIZE = 2048\n const FALLBACK_TIME_DOMAIN_FILL = 128\n\n function emit<TEvent extends RealtimeEvent>(\n event: TEvent,\n payload: Parameters<RealtimeEventHandler<TEvent>>[0],\n ) {\n const handlers = eventHandlers.get(event)\n if (handlers) {\n for (const handler of handlers) {\n handler(payload)\n }\n }\n }\n\n let dataChannelOpened = false\n let rejectDataChannelReady: ((reason: unknown) => void) | null = null\n let dataChannelReadyTimeout: ReturnType<typeof setTimeout> | null = null\n\n const dataChannelReady = new Promise<void>((resolve, reject) => {\n rejectDataChannelReady = (reason) => {\n if (dataChannelReadyTimeout !== null) {\n clearTimeout(dataChannelReadyTimeout)\n dataChannelReadyTimeout = null\n }\n // One-shot: null out so later state transitions don't reject twice.\n rejectDataChannelReady = null\n reject(reason)\n }\n\n dataChannelReadyTimeout = setTimeout(() => {\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(\n new Error(\n 'Data channel did not open within 15000ms — aborting connection',\n ),\n )\n }\n }, 15000)\n\n channel.onopen = () => {\n dataChannelOpened = true\n if (dataChannelReadyTimeout !== null) {\n clearTimeout(dataChannelReadyTimeout)\n dataChannelReadyTimeout = null\n }\n // Once resolved, rejecting is a no-op — null out so teardown paths\n // don't attempt a redundant reject on an already-settled promise.\n rejectDataChannelReady = null\n flushPendingEvents()\n emit('status_change', { status: 'connected' })\n resolve()\n }\n })\n\n channel.onmessage = (event) => {\n try {\n const message = JSON.parse(event.data)\n const messageRecord: Record<string, unknown> =\n message !== null && typeof message === 'object' ? message : {}\n logger.provider(\n `provider=grok direction=in type=${readString(messageRecord, 'type') ?? '<unknown>'}`,\n { frame: messageRecord },\n )\n handleServerEvent(messageRecord)\n } catch (parseErr) {\n logger.errors('grok.realtime fatal', {\n error: parseErr,\n source: 'grok.realtime',\n })\n emit('error', {\n error:\n parseErr instanceof Error ? parseErr : new Error(String(parseErr)),\n })\n }\n }\n\n channel.onerror = (error) => {\n // Closing the peer connection cascades into `onerror`/`onclose` on the\n // data channel. Once teardown has started, re-surfacing those as\n // `emit('error')` is noise that confuses consumers (they just called\n // `disconnect()` — they don't want an error toast for it).\n if (isTornDown) return\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime',\n })\n // RTCErrorEvent exposes a typed `.error`; fall back to the event type\n // name, then to a string representation, so the emitted error message\n // doesn't end up as \"[object Event]\".\n // `onerror` always fires with an Event (often an RTCErrorEvent), so we\n // can read it via the untyped helpers without first proving object-ness.\n // oxlint-disable-next-line eslint-js/no-restricted-syntax -- RTCErrorEvent is a typed DOM class that does not structurally overlap Record<string, unknown>; we duck-type it via readObject/readString\n const errorRecord = error as unknown as Record<string, unknown>\n const rtcError = readObject(errorRecord, 'error')\n const msg =\n (rtcError && readString(rtcError, 'message')) ?? (error.type || 'unknown')\n const dcErr = new Error(`Data channel error: ${msg}`)\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(dcErr)\n }\n emit('error', { error: dcErr })\n }\n\n channel.onclose = () => {\n // Same rationale as `onerror` above: `pc.close()` during teardown\n // cascades to the data channel's `onclose`. If we've already started\n // teardown, there's nothing to do here.\n if (isTornDown) return\n if (!dataChannelOpened) {\n rejectDataChannelReady?.(new Error('Data channel closed before opening'))\n }\n }\n\n pc.ontrack = (event) => {\n if (event.track.kind === 'audio' && event.streams[0]) {\n setupOutputAudioAnalysis(event.streams[0])\n }\n }\n\n // `status_change` has a single source of truth: `onconnectionstatechange`\n // (the higher-level aggregate state). `oniceconnectionstatechange` is\n // responsible only for rejecting `dataChannelReady` on ICE failures so we\n // surface them without waiting for the 15s timeout.\n pc.onconnectionstatechange = () => {\n const state = pc.connectionState\n logger.provider(`provider=grok pc.connectionState=${state}`, {\n state,\n })\n if (state === 'failed' || state === 'disconnected' || state === 'closed') {\n // Suppress the `status_change` emission when teardown is in progress:\n // the user-facing `disconnect()` already emits `status_change: 'idle'`\n // and then calls `teardownConnection()` → `pc.close()`, which fires\n // `onconnectionstatechange` with state === 'closed'. Without this\n // guard listeners would see two `idle` events per disconnect.\n if (!isTornDown) {\n emit('status_change', {\n status: state === 'failed' ? 'error' : 'idle',\n })\n }\n if (!dataChannelOpened) {\n // Reject on any terminal-ish pre-open state so callers don't hang\n // for the full 15s timeout. The reject is one-shot — subsequent\n // state changes become no-ops via the null-out in\n // `rejectDataChannelReady`.\n const message =\n state === 'failed'\n ? `PeerConnection failed before data channel opened`\n : `PeerConnection entered state '${state}' before data channel opened`\n rejectDataChannelReady?.(new Error(message))\n }\n // Auto-teardown on `failed`: without this the mic track, pc, and\n // AudioContext stay allocated after a fatal connection failure, so the\n // browser's mic indicator stays on and the user sees a broken\n // \"connected mic\" state. `closed` already means pc was torn down\n // (usually by teardownConnection itself) so nothing extra to do.\n // `disconnected` is transient per the WebRTC spec and may recover, so\n // we leave resources in place. `teardownConnection` is idempotent so\n // a subsequent consumer `disconnect()` remains safe.\n if (state === 'failed' && !isTornDown) {\n void teardownConnection()\n }\n }\n }\n\n pc.oniceconnectionstatechange = () => {\n const state = pc.iceConnectionState\n logger.provider(`provider=grok pc.iceConnectionState=${state}`, {\n state,\n })\n if (\n !dataChannelOpened &&\n (state === 'failed' || state === 'closed' || state === 'disconnected')\n ) {\n const message =\n state === 'failed'\n ? `ICE connection failed before data channel opened`\n : `ICE connection entered state '${state}' before data channel opened`\n rejectDataChannelReady?.(new Error(message))\n }\n }\n\n /**\n * Tear down every resource we may have allocated so the mic/pc/audio\n * nodes/audio element don't leak on a failed connect. Safe to call from\n * any point after `new RTCPeerConnection()`; each branch null-guards and\n * swallows errors because cascading closes (e.g. `pc.close()` closing the\n * data channel implicitly) are expected.\n *\n * Shared between the SDP-path catch, the post-SDP catch, and (implicitly\n * via idempotency) the `disconnect()` entry point.\n */\n async function teardownConnection() {\n // Flip the teardown flag BEFORE any awaits so handlers that fire during\n // `await audioContext.close()` (or any other async step below) can guard\n // on it — otherwise a late `pc.onconnectionstatechange` or `pc.ontrack`\n // can allocate new resources or re-emit `status_change: idle` after the\n // user-facing `disconnect()` already emitted one.\n isTornDown = true\n\n // Drop any queued events the caller sent before the data channel opened\n // up front. Without this they'd accumulate across reconnect attempts\n // (each connect allocates a fresh closure, but a caller holding the old\n // `connection` reference could otherwise keep appending forever). Done\n // at the top — before the awaits below — so `sendEvent` calls racing\n // with teardown don't push into a list we're about to drain.\n pendingEvents.length = 0\n\n // Clear the data-channel-open timeout / reject the readiness promise\n // if it's still pending. `rejectDataChannelReady` is one-shot and nulls\n // itself on first call, so calling it from `disconnect()` after a\n // successful open is a no-op.\n rejectDataChannelReady?.(\n new Error('Connection torn down before data channel opened'),\n )\n\n if (localStream) {\n for (const track of localStream.getTracks()) {\n track.stop()\n }\n localStream = null\n }\n\n // Output audio (populated by `pc.ontrack` → setupOutputAudioAnalysis,\n // which may have fired during SDP negotiation before we threw).\n if (audioElement) {\n try {\n audioElement.pause()\n } catch {\n // ignore — element may already be unloaded\n }\n audioElement.srcObject = null\n audioElement = null\n }\n if (outputSource) {\n try {\n outputSource.disconnect()\n } catch {\n // ignore\n }\n outputSource = null\n }\n if (outputAnalyser) {\n try {\n outputAnalyser.disconnect()\n } catch {\n // ignore\n }\n outputAnalyser = null\n }\n\n // Input audio (populated by setupInputAudioAnalysis after SDP).\n if (inputSource) {\n try {\n inputSource.disconnect()\n } catch {\n // ignore\n }\n inputSource = null\n }\n if (inputAnalyser) {\n try {\n inputAnalyser.disconnect()\n } catch {\n // ignore\n }\n inputAnalyser = null\n }\n\n if (dataChannel) {\n try {\n dataChannel.close()\n } catch {\n // ignore — channel may already be closed by pc.close()\n }\n dataChannel = null\n }\n\n try {\n pc.close()\n } catch {\n // ignore — pc may already be closed\n }\n\n if (audioContext) {\n try {\n await audioContext.close()\n } catch {\n // ignore — context may already be closed\n }\n audioContext = null\n }\n }\n\n // xAI requires an audio track in the SDP offer, same as OpenAI realtime.\n //\n // This try/catch also covers `getUserMedia` failure (e.g. the user denies\n // microphone permission). `pc` + `dataChannel` are already allocated above\n // and the 15s `dataChannelReady` timeout is already armed, so we MUST\n // teardown on failure here — otherwise they leak until the tab closes.\n // `teardownConnection` is idempotent and null-safe (runs fine even if the\n // mic was never acquired).\n try {\n try {\n localStream = await navigator.mediaDevices.getUserMedia({\n audio: {\n echoCancellation: true,\n noiseSuppression: true,\n sampleRate: 24000,\n },\n })\n } catch (error) {\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime.getUserMedia',\n })\n // Re-throw with the descriptive message callers rely on. Teardown runs\n // in the outer catch below.\n throw new Error(\n `Microphone access required for realtime voice: ${error instanceof Error ? error.message : error}`,\n )\n }\n\n for (const track of localStream.getAudioTracks()) {\n pc.addTrack(track, localStream)\n }\n\n const offer = await pc.createOffer()\n await pc.setLocalDescription(offer)\n\n const sdpResponse = await fetch(`${GROK_REALTIME_URL}?model=${model}`, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${token.token}`,\n 'Content-Type': 'application/sdp',\n },\n body: offer.sdp,\n })\n\n if (!sdpResponse.ok) {\n const errorText = await sdpResponse.text()\n const error = new Error(\n `Failed to establish WebRTC connection: ${sdpResponse.status} - ${errorText}`,\n )\n logger.errors('grok.realtime fatal', {\n error,\n source: 'grok.realtime.sdp',\n status: sdpResponse.status,\n })\n throw error\n }\n\n const answerSdp = await sdpResponse.text()\n await pc.setRemoteDescription({ type: 'answer', sdp: answerSdp })\n } catch (err) {\n await teardownConnection()\n throw err\n }\n\n // Second cleanup scope: after SDP succeeds we still have to set up input\n // audio analysis and wait for the data channel to open. Both can fail\n // (AudioContext allocation, 15s timeout, ICE failure, pc.close from the\n // other end, etc.) and those failures must NOT leave the mic/pc/audio\n // nodes running.\n try {\n setupInputAudioAnalysis(localStream)\n await dataChannelReady\n } catch (err) {\n await teardownConnection()\n throw err\n }\n\n function handleServerEvent(event: Record<string, unknown>) {\n const type = readString(event, 'type')\n\n switch (type) {\n case 'session.created':\n case 'session.updated':\n break\n\n case 'input_audio_buffer.speech_started':\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n break\n\n case 'input_audio_buffer.speech_stopped':\n currentMode = 'thinking'\n emit('mode_change', { mode: 'thinking' })\n break\n\n case 'input_audio_buffer.committed':\n break\n\n case 'conversation.item.input_audio_transcription.completed': {\n const transcript = readString(event, 'transcript')\n if (transcript === undefined) break\n emit('transcript', { role: 'user', transcript, isFinal: true })\n break\n }\n\n case 'response.created':\n // Reset message id so a tool-only response (which never emits\n // response.output_item.added for a message) can't reuse the previous\n // turn's id when `response.done` later inspects this flag.\n currentMessageId = null\n currentMode = 'thinking'\n emit('mode_change', { mode: 'thinking' })\n break\n\n case 'response.output_item.added': {\n const item = readObject(event, 'item')\n if (item && readString(item, 'type') === 'message') {\n const id = readString(item, 'id')\n if (id !== undefined) currentMessageId = id\n }\n break\n }\n\n // xAI realtime per docs uses `response.output_audio_transcript.*`;\n // accept the legacy OpenAI-realtime `response.audio_transcript.*` as\n // an alias so this adapter stays compatible across protocol versions.\n case 'response.output_audio_transcript.delta':\n case 'response.audio_transcript.delta': {\n const delta = readString(event, 'delta')\n if (delta === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: delta,\n isFinal: false,\n })\n break\n }\n\n case 'response.output_audio_transcript.done':\n case 'response.audio_transcript.done': {\n const transcript = readString(event, 'transcript')\n if (transcript === undefined) break\n emit('transcript', { role: 'assistant', transcript, isFinal: true })\n break\n }\n\n // xAI realtime per docs uses `response.text.*`; accept the legacy\n // OpenAI-realtime `response.output_text.*` as an alias.\n case 'response.text.delta':\n case 'response.output_text.delta': {\n const delta = readString(event, 'delta')\n if (delta === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: delta,\n isFinal: false,\n })\n break\n }\n\n case 'response.text.done':\n case 'response.output_text.done': {\n const text = readString(event, 'text')\n if (text === undefined) break\n emit('transcript', {\n role: 'assistant',\n transcript: text,\n isFinal: true,\n })\n break\n }\n\n // xAI realtime per docs uses `response.output_audio.*`; accept the\n // legacy OpenAI-realtime `response.audio.*` as an alias.\n case 'response.output_audio.delta':\n case 'response.audio.delta':\n if (currentMode !== 'speaking') {\n currentMode = 'speaking'\n emit('mode_change', { mode: 'speaking' })\n }\n break\n\n case 'response.output_audio.done':\n case 'response.audio.done':\n break\n\n case 'response.function_call_arguments.done': {\n // Only `call_id` is valid for `sendToolResult` correlation. Falling\n // back to `item_id` would produce a tool-call id the server doesn't\n // recognise when the result is posted back, silently dropping the\n // tool execution. If `call_id` is missing we surface an error event\n // so the UI can react instead of pretending the tool call succeeded.\n const callId = readString(event, 'call_id')\n const name = readString(event, 'name') ?? ''\n const args = readString(event, 'arguments') ?? ''\n if (!callId) {\n logger.errors(\n 'grok.realtime tool_call missing call_id — dropping tool_call',\n {\n source: 'grok.realtime',\n event_type: 'response.function_call_arguments.done',\n item_id: event.item_id,\n },\n )\n emit('error', {\n error: new Error(\n 'Realtime tool call missing call_id; tool will not execute',\n ),\n })\n break\n }\n try {\n const input = JSON.parse(args)\n emit('tool_call', { toolCallId: callId, toolName: name, input })\n } catch {\n emit('tool_call', { toolCallId: callId, toolName: name, input: args })\n }\n break\n }\n\n case 'response.done': {\n const response = readObject(event, 'response') ?? {}\n const output = readObjectArray(response, 'output')\n\n // Only transition back to `listening` if the user hasn't already\n // stopped capture — otherwise we'd override their explicit `idle`\n // state and re-arm the mic visualisation.\n if (currentMode !== 'idle') {\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n }\n\n if (currentMessageId) {\n const message: RealtimeMessage = {\n id: currentMessageId,\n role: 'assistant',\n timestamp: Date.now(),\n parts: [],\n }\n\n for (const item of output ?? []) {\n if (readString(item, 'type') !== 'message') continue\n const content = readObjectArray(item, 'content')\n if (!content) continue\n for (const part of content) {\n const partType = readString(part, 'type')\n if (partType === 'audio') {\n const transcript = readString(part, 'transcript')\n if (transcript) {\n message.parts.push({ type: 'audio', transcript })\n }\n } else if (partType === 'text') {\n const content = readString(part, 'text')\n if (content) {\n message.parts.push({ type: 'text', content })\n }\n }\n }\n }\n\n emit('message_complete', { message })\n currentMessageId = null\n }\n break\n }\n\n case 'conversation.item.truncated':\n // Assistant playback was interrupted — flip mode back to `listening`\n // unless the user already called `stopAudioCapture()` (idle). Without\n // this the visualisation would stay stuck on `speaking` even though\n // no audio is playing.\n if (currentMode !== 'idle') {\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n }\n emit('interrupted', {\n ...(currentMessageId !== null && { messageId: currentMessageId }),\n })\n break\n\n case 'error': {\n // The realtime server's `error` envelope isn't guaranteed to carry\n // an `error` object at all (network-layer corruption, protocol\n // drift, etc.). Validate shape before dereferencing so a malformed\n // payload can't throw a TypeError inside this handler and stop the\n // switch from running for the rest of the session.\n const errorObj = readObject(event, 'error') ?? {}\n const message =\n readString(errorObj, 'message') ?? 'Unknown realtime server error'\n const err: RealtimeServerError = new Error(message)\n // Preserve `code` / `type` / `param` on the Error as extra props so\n // consumers can branch on them without re-parsing the raw event.\n const code = readString(errorObj, 'code')\n if (code !== undefined) err.code = code\n const errType = readString(errorObj, 'type')\n if (errType !== undefined) err.type = errType\n const param = readString(errorObj, 'param')\n if (param !== undefined) err.param = param\n logger.errors('grok.realtime server error', {\n ...errorObj,\n source: 'grok.realtime server',\n })\n emit('error', { error: err })\n break\n }\n\n case undefined:\n default:\n // The xAI realtime protocol is a moving target; log unhandled event\n // types at provider level so they're visible during debugging without\n // emitting a user-visible error. `undefined` shares the bucket because\n // a malformed event without a `type` field is just as unhandleable.\n logger.provider('grok.realtime unhandled server event', {\n type: event.type,\n })\n break\n }\n }\n\n function setupOutputAudioAnalysis(stream: MediaStream) {\n // Bail out if teardown has already started. `pc.ontrack` can fire\n // asynchronously after `teardownConnection()` has flipped `isTornDown`\n // (e.g. a remote track arriving mid-close); without this guard we'd\n // allocate a fresh AudioContext / audio element that nothing would ever\n // clean up.\n if (isTornDown) return\n\n // Tear down any prior output audio before allocating new resources.\n // `pc.ontrack` can fire multiple times over the lifetime of a session\n // (e.g. after renegotiation), and without this we'd leak audio elements\n // and analyser nodes.\n if (audioElement) {\n try {\n audioElement.pause()\n } catch {\n // ignore — element may already be unloaded\n }\n audioElement.srcObject = null\n audioElement = null\n }\n if (outputSource) {\n try {\n outputSource.disconnect()\n } catch {\n // ignore — may already be disconnected\n }\n outputSource = null\n }\n if (outputAnalyser) {\n try {\n outputAnalyser.disconnect()\n } catch {\n // ignore\n }\n outputAnalyser = null\n }\n\n audioElement = new Audio()\n audioElement.srcObject = stream\n audioElement.autoplay = true\n audioElement.play().catch((e) => {\n // Autoplay is commonly blocked until the user interacts with the page\n // (browser gesture requirement). Surfacing this as a fatal `error`\n // event makes the UI render a red/error state even though the\n // connection is healthy — the page just needs a click. Log at a\n // dedicated source tag so it's debuggable, but don't emit `error`.\n logger.errors('grok.realtime audio autoplay blocked', {\n error: e,\n source: 'grok.realtime.audio_permission_required',\n })\n })\n\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n\n if (audioContext.state === 'suspended') {\n audioContext.resume().catch((err) => {\n // Same rationale as the autoplay catch: `resume()` failure usually\n // means the user hasn't interacted yet. Logging only — no error\n // emit — so the UI doesn't go into a fatal state for a recoverable\n // condition.\n logger.errors('grok.realtime audioContext.resume failed', {\n error: err,\n source: 'grok.realtime',\n })\n })\n }\n\n outputAnalyser = audioContext.createAnalyser()\n outputAnalyser.fftSize = 2048\n outputAnalyser.smoothingTimeConstant = 0.3\n\n outputSource = audioContext.createMediaStreamSource(stream)\n outputSource.connect(outputAnalyser)\n }\n\n function setupInputAudioAnalysis(stream: MediaStream) {\n // Defensive symmetry with `setupOutputAudioAnalysis`. Today this is\n // only called inline after SDP negotiation, but keeping the guard\n // means any future caller path (e.g. renegotiation) won't leak a fresh\n // AudioContext after teardown.\n if (isTornDown) return\n\n if (!audioContext) {\n audioContext = new AudioContext()\n }\n\n if (audioContext.state === 'suspended') {\n audioContext.resume().catch((err) => {\n // Same rationale as in setupOutputAudioAnalysis: a suspended\n // AudioContext usually resumes after a user gesture. Log only —\n // surfacing this as a fatal error makes the UI look broken for a\n // recoverable condition.\n logger.errors('grok.realtime audioContext.resume failed', {\n error: err,\n source: 'grok.realtime',\n })\n })\n }\n\n inputAnalyser = audioContext.createAnalyser()\n inputAnalyser.fftSize = 2048\n inputAnalyser.smoothingTimeConstant = 0.3\n\n inputSource = audioContext.createMediaStreamSource(stream)\n inputSource.connect(inputAnalyser)\n }\n\n function sendEvent(event: Record<string, unknown>) {\n if (isTornDown) {\n // The caller is holding onto a `connection` object after `disconnect()`\n // (or a failed connect). Silently queueing would leak memory and the\n // events would never flush. Log + drop so the misuse is visible in\n // debug mode without escalating to a throw — throwing from a React\n // useEffect cleanup path can break teardown ordering in the UI.\n logger.errors('grok.realtime sendEvent after disconnect', {\n eventType: readString(event, 'type') ?? '<unknown>',\n source: 'grok.realtime',\n })\n return\n }\n if (dataChannel?.readyState === 'open') {\n logger.provider(\n `provider=grok direction=out type=${readString(event, 'type') ?? '<unknown>'}`,\n { frame: event },\n )\n // Mirror the try/catch in `flushPendingEvents` — `dataChannel.send`\n // can synchronously throw if the channel flipped to `closing` between\n // our readyState check and this call, or if `JSON.stringify` chokes\n // on a caller-supplied payload. Log + emit error instead of letting\n // the exception propagate up through public `sendText` / `sendImage`\n // / `updateSession` call sites.\n try {\n dataChannel.send(JSON.stringify(event))\n } catch (error) {\n logger.errors('grok.realtime sendEvent failed', {\n error,\n eventType: readString(event, 'type') ?? '<unknown>',\n source: 'grok.realtime',\n })\n emit('error', {\n error: error instanceof Error ? error : new Error(String(error)),\n })\n }\n } else {\n pendingEvents.push(event)\n }\n }\n\n function flushPendingEvents() {\n try {\n for (const event of pendingEvents) {\n logger.provider(\n `provider=grok direction=out type=${readString(event, 'type') ?? '<unknown>'}`,\n { frame: event },\n )\n channel.send(JSON.stringify(event))\n }\n pendingEvents.length = 0\n } catch (error) {\n // A send failure here (e.g. dataChannel went from 'open' back to\n // 'closing' mid-flush, or JSON.stringify on a caller-provided event\n // threw) would otherwise be silently swallowed. By the time we're\n // called, `onopen` has already resolved `dataChannelReady`, so the\n // consumer-facing signal is `emit('error')` — try rejectDataChannelReady\n // as a defensive belt-and-braces in case this ever runs pre-resolve.\n logger.errors('grok.realtime flushPendingEvents failed', {\n error,\n source: 'grok.realtime',\n })\n const err = error instanceof Error ? error : new Error(String(error))\n rejectDataChannelReady?.(err)\n emit('error', { error: err })\n }\n }\n\n const connection: RealtimeConnection = {\n async disconnect() {\n // Reuse the same teardown path as the failed-connect branches so\n // every cleanup site stays in sync (input analyser, output analyser,\n // output source, audio element, etc.).\n await teardownConnection()\n emit('status_change', { status: 'idle' })\n },\n\n async startAudioCapture() {\n if (localStream) {\n for (const track of localStream.getAudioTracks()) {\n track.enabled = true\n }\n }\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n },\n\n stopAudioCapture() {\n if (localStream) {\n for (const track of localStream.getAudioTracks()) {\n track.enabled = false\n }\n }\n currentMode = 'idle'\n emit('mode_change', { mode: 'idle' })\n },\n\n sendText(text: string) {\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'message',\n role: 'user',\n content: [{ type: 'input_text', text }],\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n sendImage(imageData: string, mimeType: string) {\n // Accept:\n // - http(s):// URLs → forward as-is\n // - data: URIs (e.g. from FileReader.readAsDataURL) → forward as-is\n // so we don't double-wrap into `data:image/png;base64,data:image/png;base64,…`\n // - bare base64 → wrap in `data:${mimeType};base64,…`\n const isAlreadyUrlOrDataUri =\n imageData.startsWith('http://') ||\n imageData.startsWith('https://') ||\n imageData.startsWith('data:')\n const imageContent = {\n type: 'input_image',\n // The OpenAI-realtime content part (which this adapter mirrors) nests\n // the URL under an `image_url: { url: ... }` object, not a bare\n // string.\n image_url: {\n url: isAlreadyUrlOrDataUri\n ? imageData\n : `data:${mimeType};base64,${imageData}`,\n },\n }\n\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'message',\n role: 'user',\n content: [imageContent],\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n sendToolResult(callId: string, result: string) {\n sendEvent({\n type: 'conversation.item.create',\n item: {\n type: 'function_call_output',\n call_id: callId,\n output: result,\n },\n })\n sendEvent({ type: 'response.create' })\n },\n\n updateSession(config: Partial<RealtimeSessionConfig>) {\n const sessionUpdate: Record<string, unknown> = {}\n\n if (config.instructions) {\n sessionUpdate.instructions = config.instructions\n }\n\n if (config.voice) {\n sessionUpdate.voice = config.voice\n }\n\n if (config.vadMode) {\n if (config.vadMode === 'semantic') {\n sessionUpdate.turn_detection = {\n type: 'semantic_vad',\n eagerness: config.semanticEagerness ?? 'medium',\n }\n } else if (config.vadMode === 'server') {\n sessionUpdate.turn_detection = {\n type: 'server_vad',\n threshold: config.vadConfig?.threshold ?? 0.5,\n prefix_padding_ms: config.vadConfig?.prefixPaddingMs ?? 300,\n silence_duration_ms: config.vadConfig?.silenceDurationMs ?? 500,\n }\n } else {\n sessionUpdate.turn_detection = null\n }\n }\n\n if (config.tools !== undefined) {\n sessionUpdate.tools = config.tools.map((t) => ({\n type: 'function',\n name: t.name,\n description: t.description,\n parameters: t.inputSchema ?? { type: 'object', properties: {} },\n }))\n sessionUpdate.tool_choice = 'auto'\n }\n\n if (config.outputModalities) {\n sessionUpdate.modalities = config.outputModalities\n }\n\n if (config.temperature !== undefined) {\n sessionUpdate.temperature = config.temperature\n }\n\n if (config.maxOutputTokens !== undefined) {\n sessionUpdate.max_response_output_tokens = config.maxOutputTokens\n }\n\n // Let callers forward an explicit `input_audio_transcription` value\n // through `providerOptions` — including `null` / `false` to disable\n // the feature. Only apply our `grok-stt` default on the first\n // session.update and only if the caller hasn't set it themselves.\n const providerOptions: Record<string, unknown> =\n config.providerOptions ?? {}\n const callerTranscription =\n 'inputAudioTranscription' in providerOptions\n ? providerOptions.inputAudioTranscription\n : 'input_audio_transcription' in providerOptions\n ? providerOptions.input_audio_transcription\n : undefined\n if (callerTranscription !== undefined) {\n sessionUpdate.input_audio_transcription =\n callerTranscription === false ? null : callerTranscription\n } else if (!hasSentInitialSessionUpdate) {\n sessionUpdate.input_audio_transcription = { model: 'grok-stt' }\n }\n\n if (Object.keys(sessionUpdate).length > 0) {\n sendEvent({\n type: 'session.update',\n session: sessionUpdate,\n })\n hasSentInitialSessionUpdate = true\n }\n },\n\n interrupt() {\n sendEvent({ type: 'response.cancel' })\n currentMode = 'listening'\n emit('mode_change', { mode: 'listening' })\n emit('interrupted', {\n ...(currentMessageId !== null && { messageId: currentMessageId }),\n })\n },\n\n on<TEvent extends RealtimeEvent>(\n event: TEvent,\n handler: RealtimeEventHandler<TEvent>,\n ): () => void {\n let handlers = eventHandlers.get(event)\n if (!handlers) {\n handlers = new Set()\n eventHandlers.set(event, handlers)\n }\n handlers.add(handler)\n\n return () => {\n eventHandlers.get(event)?.delete(handler)\n }\n },\n\n getAudioVisualization(): AudioVisualization {\n function calculateLevel(analyser: AnalyserNode): number {\n const data = new Uint8Array(analyser.fftSize)\n analyser.getByteTimeDomainData(data)\n\n let maxDeviation = 0\n for (const sample of data) {\n const deviation = Math.abs(sample - 128)\n if (deviation > maxDeviation) {\n maxDeviation = deviation\n }\n }\n\n const normalized = maxDeviation / 128\n return Math.min(1, normalized * 1.5)\n }\n\n return {\n get inputLevel() {\n if (!inputAnalyser) return 0\n return calculateLevel(inputAnalyser)\n },\n\n get outputLevel() {\n if (!outputAnalyser) return 0\n return calculateLevel(outputAnalyser)\n },\n\n getInputFrequencyData() {\n if (!inputAnalyser)\n return new Uint8Array(FALLBACK_FREQUENCY_BIN_COUNT)\n const data = new Uint8Array(inputAnalyser.frequencyBinCount)\n inputAnalyser.getByteFrequencyData(data)\n return data\n },\n\n getOutputFrequencyData() {\n if (!outputAnalyser)\n return new Uint8Array(FALLBACK_FREQUENCY_BIN_COUNT)\n const data = new Uint8Array(outputAnalyser.frequencyBinCount)\n outputAnalyser.getByteFrequencyData(data)\n return data\n },\n\n getInputTimeDomainData() {\n if (!inputAnalyser)\n return new Uint8Array(FALLBACK_TIME_DOMAIN_SIZE).fill(\n FALLBACK_TIME_DOMAIN_FILL,\n )\n const data = new Uint8Array(inputAnalyser.fftSize)\n inputAnalyser.getByteTimeDomainData(data)\n return data\n },\n\n getOutputTimeDomainData() {\n if (!outputAnalyser)\n return new Uint8Array(FALLBACK_TIME_DOMAIN_SIZE).fill(\n FALLBACK_TIME_DOMAIN_FILL,\n )\n const data = new Uint8Array(outputAnalyser.fftSize)\n outputAnalyser.getByteTimeDomainData(data)\n return data\n },\n\n get inputSampleRate() {\n return 24000\n },\n\n get outputSampleRate() {\n return 24000\n },\n }\n },\n }\n\n // `dataChannelReady` was already awaited inside the post-SDP try/catch\n // above so we can short-circuit on failures with full teardown.\n return connection\n}\n"],"mappings":"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"}
@@ -15,7 +15,7 @@ import { GrokRealtimeTokenOptions } from './types.js';
15
15
  * import { grokRealtimeToken } from '@tanstack/ai-grok'
16
16
  *
17
17
  * const token = await realtimeToken({
18
- * adapter: grokRealtimeToken({ model: 'grok-voice-fast-1.0' }),
18
+ * adapter: grokRealtimeToken({ model: 'grok-voice-think-fast-2.0' }),
19
19
  * })
20
20
  * ```
21
21
  */
@@ -1,5 +1,6 @@
1
1
  import { getGrokApiKeyFromEnv } from "../utils/client.js";
2
2
  import "../utils/index.js";
3
+ import "../model-meta.js";
3
4
  import { resolveDebugOption } from "@tanstack/ai/adapter-internals";
4
5
  //#region src/realtime/token.ts
5
6
  var GROK_REALTIME_CLIENT_SECRETS_URL = "https://api.x.ai/v1/realtime/client_secrets";
@@ -19,7 +20,7 @@ var DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 15e3;
19
20
  * import { grokRealtimeToken } from '@tanstack/ai-grok'
20
21
  *
21
22
  * const token = await realtimeToken({
22
- * adapter: grokRealtimeToken({ model: 'grok-voice-fast-1.0' }),
23
+ * adapter: grokRealtimeToken({ model: 'grok-voice-think-fast-2.0' }),
23
24
  * })
24
25
  * ```
25
26
  */
@@ -29,7 +30,7 @@ function grokRealtimeToken(options = {}) {
29
30
  return {
30
31
  provider: "grok",
31
32
  async generateToken() {
32
- const model = options.model ?? "grok-voice-fast-1.0";
33
+ const model = options.model ?? "grok-voice-think-fast-2.0";
33
34
  logger.request(`activity=realtimeToken provider=grok model=${model}`, {
34
35
  provider: "grok",
35
36
  model
@@ -1 +1 @@
1
- {"version":3,"file":"token.js","names":[],"sources":["../../../src/realtime/token.ts"],"sourcesContent":["import { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport { getGrokApiKeyFromEnv } from '../utils'\nimport type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { GrokRealtimeModel } from '../model-meta'\nimport type {\n GrokRealtimeSessionResponse,\n GrokRealtimeTokenOptions,\n} from './types'\n\nconst GROK_REALTIME_CLIENT_SECRETS_URL =\n 'https://api.x.ai/v1/realtime/client_secrets'\n\nconst DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 15_000\n\n/**\n * Creates a Grok realtime token adapter.\n *\n * Generates ephemeral client secrets for browser-side WebRTC connections to\n * the xAI Voice Agent API.\n *\n * @param options - Configuration options for the realtime session.\n * @returns A RealtimeTokenAdapter for use with `realtimeToken()`.\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { grokRealtimeToken } from '@tanstack/ai-grok'\n *\n * const token = await realtimeToken({\n * adapter: grokRealtimeToken({ model: 'grok-voice-fast-1.0' }),\n * })\n * ```\n */\nexport function grokRealtimeToken(\n options: GrokRealtimeTokenOptions = {},\n): RealtimeTokenAdapter {\n const apiKey = getGrokApiKeyFromEnv()\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'grok',\n\n async generateToken(): Promise<RealtimeToken> {\n const model: GrokRealtimeModel = options.model ?? 'grok-voice-fast-1.0'\n\n logger.request(`activity=realtimeToken provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n // xAI docs (docs.x.ai/developers/rest-api-reference/inference/voice)\n // specify the body as `{ session: { model } }`. `expires_after` is\n // available to shorten the default 600s TTL but we don't expose it\n // yet — the caller can still call `generateToken()` more often if\n // they want a shorter-lived session.\n const requestBody: Record<string, unknown> = {\n session: { model },\n }\n\n // Abort the fetch if xAI never responds. Without this the whole\n // realtime connect flow hangs forever on a dead endpoint.\n const controller = new AbortController()\n const timeout = setTimeout(\n () =>\n controller.abort(new Error('Grok realtime token request timed out')),\n DEFAULT_TOKEN_FETCH_TIMEOUT_MS,\n )\n\n try {\n const response = await fetch(GROK_REALTIME_CLIENT_SECRETS_URL, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok realtime session creation failed: ${response.status} ${errorText}`,\n )\n }\n\n const sessionData = (await response.json()) as\n | Partial<GrokRealtimeSessionResponse>\n | undefined\n\n // Validate shape before dereferencing — xAI could return an error\n // envelope with 200 status, or a partial response under protocol drift.\n const clientSecret = sessionData?.client_secret\n if (\n !clientSecret ||\n typeof clientSecret.value !== 'string' ||\n typeof clientSecret.expires_at !== 'number' ||\n !Number.isFinite(clientSecret.expires_at)\n ) {\n throw new Error(\n 'Grok realtime session response missing or malformed `client_secret`',\n )\n }\n const sessionModel = sessionData.model ?? model\n\n // xAI docs describe `expires_at` as a unix timestamp in seconds, but\n // in practice different deployments have returned milliseconds. Treat\n // any value that already looks like ms (>1e12 ≈ Sep 2001 in ms) as ms.\n const raw = clientSecret.expires_at\n const expiresAt = raw > 1e12 ? raw : raw * 1000\n\n return {\n provider: 'grok',\n token: clientSecret.value,\n expiresAt,\n config: {\n model: sessionModel,\n },\n }\n } catch (error) {\n logger.errors('grok.realtimeToken fatal', {\n error,\n source: 'grok.realtimeToken',\n })\n throw error\n } finally {\n clearTimeout(timeout)\n }\n },\n }\n}\n"],"mappings":";;;;AASA,IAAM,mCACJ;AAEF,IAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;AAqBvC,SAAgB,kBACd,UAAoC,CAAC,GACf;CACtB,MAAM,SAAS,qBAAqB;CACpC,MAAM,SAAS,mBAAmB,QAAQ,KAAK;CAE/C,OAAO;EACL,UAAU;EAEV,MAAM,gBAAwC;GAC5C,MAAM,QAA2B,QAAQ,SAAS;GAElD,OAAO,QAAQ,8CAA8C,SAAS;IACpE,UAAU;IACV;GACF,CAAC;GAOD,MAAM,cAAuC,EAC3C,SAAS,EAAE,MAAM,EACnB;GAIA,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,UAAU,iBAEZ,WAAW,sBAAM,IAAI,MAAM,uCAAuC,CAAC,GACrE,8BACF;GAEA,IAAI;IACF,MAAM,WAAW,MAAM,MAAM,kCAAkC;KAC7D,QAAQ;KACR,SAAS;MACP,eAAe,UAAU;MACzB,gBAAgB;KAClB;KACA,MAAM,KAAK,UAAU,WAAW;KAChC,QAAQ,WAAW;IACrB,CAAC;IAED,IAAI,CAAC,SAAS,IAAI;KAChB,MAAM,YAAY,MAAM,SAAS,KAAK;KACtC,MAAM,IAAI,MACR,0CAA0C,SAAS,OAAO,GAAG,WAC/D;IACF;IAEA,MAAM,cAAe,MAAM,SAAS,KAAK;IAMzC,MAAM,eAAe,aAAa;IAClC,IACE,CAAC,gBACD,OAAO,aAAa,UAAU,YAC9B,OAAO,aAAa,eAAe,YACnC,CAAC,OAAO,SAAS,aAAa,UAAU,GAExC,MAAM,IAAI,MACR,qEACF;IAEF,MAAM,eAAe,YAAY,SAAS;IAK1C,MAAM,MAAM,aAAa;IACzB,MAAM,YAAY,MAAM,eAAO,MAAM,MAAM;IAE3C,OAAO;KACL,UAAU;KACV,OAAO,aAAa;KACpB;KACA,QAAQ,EACN,OAAO,aACT;IACF;GACF,SAAS,OAAO;IACd,OAAO,OAAO,4BAA4B;KACxC;KACA,QAAQ;IACV,CAAC;IACD,MAAM;GACR,UAAU;IACR,aAAa,OAAO;GACtB;EACF;CACF;AACF"}
1
+ {"version":3,"file":"token.js","names":[],"sources":["../../../src/realtime/token.ts"],"sourcesContent":["import { resolveDebugOption } from '@tanstack/ai/adapter-internals'\nimport { GROK_DEFAULT_REALTIME_MODEL } from '../model-meta'\nimport { getGrokApiKeyFromEnv } from '../utils'\nimport type { RealtimeToken, RealtimeTokenAdapter } from '@tanstack/ai'\nimport type { GrokRealtimeModel } from '../model-meta'\nimport type {\n GrokRealtimeSessionResponse,\n GrokRealtimeTokenOptions,\n} from './types'\n\nconst GROK_REALTIME_CLIENT_SECRETS_URL =\n 'https://api.x.ai/v1/realtime/client_secrets'\n\nconst DEFAULT_TOKEN_FETCH_TIMEOUT_MS = 15_000\n\n/**\n * Creates a Grok realtime token adapter.\n *\n * Generates ephemeral client secrets for browser-side WebRTC connections to\n * the xAI Voice Agent API.\n *\n * @param options - Configuration options for the realtime session.\n * @returns A RealtimeTokenAdapter for use with `realtimeToken()`.\n *\n * @example\n * ```typescript\n * import { realtimeToken } from '@tanstack/ai'\n * import { grokRealtimeToken } from '@tanstack/ai-grok'\n *\n * const token = await realtimeToken({\n * adapter: grokRealtimeToken({ model: 'grok-voice-think-fast-2.0' }),\n * })\n * ```\n */\nexport function grokRealtimeToken(\n options: GrokRealtimeTokenOptions = {},\n): RealtimeTokenAdapter {\n const apiKey = getGrokApiKeyFromEnv()\n const logger = resolveDebugOption(options.debug)\n\n return {\n provider: 'grok',\n\n async generateToken(): Promise<RealtimeToken> {\n const model: GrokRealtimeModel =\n options.model ?? GROK_DEFAULT_REALTIME_MODEL\n\n logger.request(`activity=realtimeToken provider=grok model=${model}`, {\n provider: 'grok',\n model,\n })\n\n // xAI docs (docs.x.ai/developers/rest-api-reference/inference/voice)\n // specify the body as `{ session: { model } }`. `expires_after` is\n // available to shorten the default 600s TTL but we don't expose it\n // yet — the caller can still call `generateToken()` more often if\n // they want a shorter-lived session.\n const requestBody: Record<string, unknown> = {\n session: { model },\n }\n\n // Abort the fetch if xAI never responds. Without this the whole\n // realtime connect flow hangs forever on a dead endpoint.\n const controller = new AbortController()\n const timeout = setTimeout(\n () =>\n controller.abort(new Error('Grok realtime token request timed out')),\n DEFAULT_TOKEN_FETCH_TIMEOUT_MS,\n )\n\n try {\n const response = await fetch(GROK_REALTIME_CLIENT_SECRETS_URL, {\n method: 'POST',\n headers: {\n Authorization: `Bearer ${apiKey}`,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(requestBody),\n signal: controller.signal,\n })\n\n if (!response.ok) {\n const errorText = await response.text()\n throw new Error(\n `Grok realtime session creation failed: ${response.status} ${errorText}`,\n )\n }\n\n const sessionData = (await response.json()) as\n | Partial<GrokRealtimeSessionResponse>\n | undefined\n\n // Validate shape before dereferencing — xAI could return an error\n // envelope with 200 status, or a partial response under protocol drift.\n const clientSecret = sessionData?.client_secret\n if (\n !clientSecret ||\n typeof clientSecret.value !== 'string' ||\n typeof clientSecret.expires_at !== 'number' ||\n !Number.isFinite(clientSecret.expires_at)\n ) {\n throw new Error(\n 'Grok realtime session response missing or malformed `client_secret`',\n )\n }\n const sessionModel = sessionData.model ?? model\n\n // xAI docs describe `expires_at` as a unix timestamp in seconds, but\n // in practice different deployments have returned milliseconds. Treat\n // any value that already looks like ms (>1e12 ≈ Sep 2001 in ms) as ms.\n const raw = clientSecret.expires_at\n const expiresAt = raw > 1e12 ? raw : raw * 1000\n\n return {\n provider: 'grok',\n token: clientSecret.value,\n expiresAt,\n config: {\n model: sessionModel,\n },\n }\n } catch (error) {\n logger.errors('grok.realtimeToken fatal', {\n error,\n source: 'grok.realtimeToken',\n })\n throw error\n } finally {\n clearTimeout(timeout)\n }\n },\n }\n}\n"],"mappings":";;;;;AAUA,IAAM,mCACJ;AAEF,IAAM,iCAAiC;;;;;;;;;;;;;;;;;;;;AAqBvC,SAAgB,kBACd,UAAoC,CAAC,GACf;CACtB,MAAM,SAAS,qBAAqB;CACpC,MAAM,SAAS,mBAAmB,QAAQ,KAAK;CAE/C,OAAO;EACL,UAAU;EAEV,MAAM,gBAAwC;GAC5C,MAAM,QACJ,QAAQ,SAAA;GAEV,OAAO,QAAQ,8CAA8C,SAAS;IACpE,UAAU;IACV;GACF,CAAC;GAOD,MAAM,cAAuC,EAC3C,SAAS,EAAE,MAAM,EACnB;GAIA,MAAM,aAAa,IAAI,gBAAgB;GACvC,MAAM,UAAU,iBAEZ,WAAW,sBAAM,IAAI,MAAM,uCAAuC,CAAC,GACrE,8BACF;GAEA,IAAI;IACF,MAAM,WAAW,MAAM,MAAM,kCAAkC;KAC7D,QAAQ;KACR,SAAS;MACP,eAAe,UAAU;MACzB,gBAAgB;KAClB;KACA,MAAM,KAAK,UAAU,WAAW;KAChC,QAAQ,WAAW;IACrB,CAAC;IAED,IAAI,CAAC,SAAS,IAAI;KAChB,MAAM,YAAY,MAAM,SAAS,KAAK;KACtC,MAAM,IAAI,MACR,0CAA0C,SAAS,OAAO,GAAG,WAC/D;IACF;IAEA,MAAM,cAAe,MAAM,SAAS,KAAK;IAMzC,MAAM,eAAe,aAAa;IAClC,IACE,CAAC,gBACD,OAAO,aAAa,UAAU,YAC9B,OAAO,aAAa,eAAe,YACnC,CAAC,OAAO,SAAS,aAAa,UAAU,GAExC,MAAM,IAAI,MACR,qEACF;IAEF,MAAM,eAAe,YAAY,SAAS;IAK1C,MAAM,MAAM,aAAa;IACzB,MAAM,YAAY,MAAM,eAAO,MAAM,MAAM;IAE3C,OAAO;KACL,UAAU;KACV,OAAO,aAAa;KACpB;KACA,QAAQ,EACN,OAAO,aACT;IACF;GACF,SAAS,OAAO;IACd,OAAO,OAAO,4BAA4B;KACxC;KACA,QAAQ;IACV,CAAC;IACD,MAAM;GACR,UAAU;IACR,aAAa,OAAO;GACtB;EACF;CACF;AACF"}
@@ -27,7 +27,7 @@ export type GrokTurnDetection = GrokSemanticVADConfig | GrokServerVADConfig | nu
27
27
  * Options for the Grok realtime token adapter.
28
28
  */
29
29
  export interface GrokRealtimeTokenOptions {
30
- /** Model to use (default: 'grok-voice-fast-1.0'). */
30
+ /** Model to use (default: 'grok-voice-think-fast-2.0'). */
31
31
  model?: GrokRealtimeModel;
32
32
  /**
33
33
  * Enable debug logging for token creation.
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/tools/index.ts"],"sourcesContent":["import { brandProviderTool } from '@tanstack/ai'\nimport { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\nimport type { ResponsesFunctionTool } from '@tanstack/openai-base'\nimport type { GrokProviderToolKind } from '../model-meta'\n\nexport type FunctionTool = ResponsesFunctionTool\n\nexport { convertFunctionToolToResponsesFormat as convertFunctionToolToAdapterFormat }\n\nexport type GrokProviderTool<TKind extends GrokProviderToolKind> = ProviderTool<\n 'grok',\n TKind\n>\n\ntype GrokToolKindMarker<TKind extends GrokProviderToolKind> = `grok.${TKind}`\n\nexport interface GrokWebSearchToolConfig {\n type: 'web_search'\n filters?: {\n allowed_domains?: Array<string>\n excluded_domains?: Array<string>\n }\n enable_image_understanding?: boolean\n enable_image_search?: boolean\n}\n\nexport interface GrokXSearchToolConfig {\n type: 'x_search'\n allowed_x_handles?: Array<string>\n excluded_x_handles?: Array<string>\n from_date?: string\n to_date?: string\n enable_image_understanding?: boolean\n enable_video_understanding?: boolean\n}\n\nexport interface GrokFileSearchToolConfig {\n type: 'file_search'\n vector_store_ids: Array<string>\n max_num_results?: number\n}\n\nexport interface GrokMCPToolConfig {\n type: 'mcp'\n server_label: string\n server_url: string\n allowed_tools?: Array<string>\n server_description?: string\n authorization?: string\n headers?: Record<string, string>\n}\n\nexport type GrokServerTool =\n | GrokWebSearchToolConfig\n | GrokXSearchToolConfig\n | GrokFileSearchToolConfig\n | GrokMCPToolConfig\n\ntype GrokProviderToolMetadata<TKind extends GrokProviderToolKind> = Extract<\n GrokServerTool,\n { type: TKind }\n> & {\n __kind: GrokToolKindMarker<TKind>\n}\n\nexport type GrokResponsesTool = GrokServerTool | ResponsesFunctionTool\n\nfunction providerTool<TKind extends GrokProviderToolKind>(\n kind: TKind,\n description: string,\n metadata: Extract<GrokServerTool, { type: TKind }>,\n): GrokProviderTool<TKind> {\n return brandProviderTool<GrokProviderTool<TKind>>({\n name: kind,\n description,\n metadata: {\n __kind: `grok.${kind}`,\n ...metadata,\n },\n })\n}\n\nexport function grokWebSearchTool(\n config: Omit<GrokWebSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'web_search'> {\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.excluded_domains !== undefined\n ) {\n throw new Error(\n 'allowed_domains and excluded_domains cannot both be provided.',\n )\n }\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.allowed_domains.length > 5\n ) {\n throw new Error('allowed_domains supports at most 5 domains.')\n }\n if (\n config.filters?.excluded_domains !== undefined &&\n config.filters.excluded_domains.length > 5\n ) {\n throw new Error('excluded_domains supports at most 5 domains.')\n }\n return providerTool('web_search', 'Search the web', {\n type: 'web_search',\n ...config,\n })\n}\n\nexport function grokXSearchTool(\n config: Omit<GrokXSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'x_search'> {\n if (\n config.allowed_x_handles !== undefined &&\n config.excluded_x_handles !== undefined\n ) {\n throw new Error(\n 'allowed_x_handles and excluded_x_handles cannot both be provided.',\n )\n }\n if (\n config.allowed_x_handles !== undefined &&\n config.allowed_x_handles.length > 20\n ) {\n throw new Error('allowed_x_handles supports at most 20 handles.')\n }\n if (\n config.excluded_x_handles !== undefined &&\n config.excluded_x_handles.length > 20\n ) {\n throw new Error('excluded_x_handles supports at most 20 handles.')\n }\n return providerTool('x_search', 'Search X posts', {\n type: 'x_search',\n ...config,\n })\n}\n\nexport function grokFileSearchTool(\n config: Omit<GrokFileSearchToolConfig, 'type'>,\n): GrokProviderTool<'file_search'> {\n if (config.vector_store_ids.length === 0) {\n throw new Error('vector_store_ids must contain at least one collection id.')\n }\n if (config.max_num_results !== undefined) {\n if (config.max_num_results < 1 || config.max_num_results > 50) {\n throw new Error('max_num_results must be between 1 and 50.')\n }\n }\n return providerTool('file_search', 'Search xAI file collections', {\n type: 'file_search',\n ...config,\n })\n}\n\nexport function grokMCPTool(\n config: Omit<GrokMCPToolConfig, 'type'>,\n): GrokProviderTool<'mcp'> {\n if (!config.server_url) {\n throw new Error('server_url must be provided.')\n }\n return providerTool('mcp', config.server_description || 'Remote MCP server', {\n type: 'mcp',\n ...config,\n })\n}\n\nfunction getGrokProviderToolKind(tool: Tool): GrokProviderToolKind | undefined {\n const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind\n switch (kind) {\n case 'grok.web_search':\n return 'web_search'\n case 'grok.x_search':\n return 'x_search'\n case 'grok.file_search':\n return 'file_search'\n case 'grok.mcp':\n return 'mcp'\n default:\n return undefined\n }\n}\n\nfunction convertGrokProviderToolToAdapterFormat(\n tool: Tool,\n kind: GrokProviderToolKind,\n): GrokServerTool {\n const metadata = tool.metadata as GrokProviderToolMetadata<typeof kind>\n if (metadata.type !== kind) {\n throw new Error(\n `convertGrokProviderToolToAdapterFormat: tool \"${tool.name}\" has mismatched Grok tool metadata.`,\n )\n }\n const { __kind: _kind, ...toolConfig } = metadata\n void _kind\n return toolConfig\n}\n\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<GrokResponsesTool> {\n return tools.map((tool) => {\n const grokProviderToolKind = getGrokProviderToolKind(tool)\n if (grokProviderToolKind) {\n return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind)\n }\n return convertFunctionToolToResponsesFormat(tool)\n })\n}\n"],"mappings":";;;AAoEA,SAAS,aACP,MACA,aACA,UACyB;CACzB,OAAO,kBAA2C;EAChD,MAAM;EACN;EACA,UAAU;GACR,QAAQ,QAAQ;GAChB,GAAG;EACL;CACF,CAAC;AACH;AAEA,SAAgB,kBACd,SAAgD,CAAC,GACjB;CAChC,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,qBAAqB,KAAA,GAEpC,MAAM,IAAI,MACR,+DACF;CAEF,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,gBAAgB,SAAS,GAExC,MAAM,IAAI,MAAM,6CAA6C;CAE/D,IACE,OAAO,SAAS,qBAAqB,KAAA,KACrC,OAAO,QAAQ,iBAAiB,SAAS,GAEzC,MAAM,IAAI,MAAM,8CAA8C;CAEhE,OAAO,aAAa,cAAc,kBAAkB;EAClD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,gBACd,SAA8C,CAAC,GACjB;CAC9B,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,uBAAuB,KAAA,GAE9B,MAAM,IAAI,MACR,mEACF;CAEF,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,kBAAkB,SAAS,IAElC,MAAM,IAAI,MAAM,gDAAgD;CAElE,IACE,OAAO,uBAAuB,KAAA,KAC9B,OAAO,mBAAmB,SAAS,IAEnC,MAAM,IAAI,MAAM,iDAAiD;CAEnE,OAAO,aAAa,YAAY,kBAAkB;EAChD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,mBACd,QACiC;CACjC,IAAI,OAAO,iBAAiB,WAAW,GACrC,MAAM,IAAI,MAAM,2DAA2D;CAE7E,IAAI,OAAO,oBAAoB,KAAA;MACzB,OAAO,kBAAkB,KAAK,OAAO,kBAAkB,IACzD,MAAM,IAAI,MAAM,2CAA2C;CAAA;CAG/D,OAAO,aAAa,eAAe,+BAA+B;EAChE,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,YACd,QACyB;CACzB,IAAI,CAAC,OAAO,YACV,MAAM,IAAI,MAAM,8BAA8B;CAEhD,OAAO,aAAa,OAAO,OAAO,sBAAsB,qBAAqB;EAC3E,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAS,wBAAwB,MAA8C;CAE7E,QADc,KAAK,UAA+C,QAClE;EACE,KAAK,mBACH,OAAO;EACT,KAAK,iBACH,OAAO;EACT,KAAK,oBACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,SACE;CACJ;AACF;AAEA,SAAS,uCACP,MACA,MACgB;CAChB,MAAM,WAAW,KAAK;CACtB,IAAI,SAAS,SAAS,MACpB,MAAM,IAAI,MACR,iDAAiD,KAAK,KAAK,qCAC7D;CAEF,MAAM,EAAE,QAAQ,OAAO,GAAG,eAAe;CAEzC,OAAO;AACT;AAEA,SAAgB,6BACd,OAC0B;CAC1B,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,uBAAuB,wBAAwB,IAAI;EACzD,IAAI,sBACF,OAAO,uCAAuC,MAAM,oBAAoB;EAE1E,OAAO,qCAAqC,IAAI;CAClD,CAAC;AACH"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/tools/index.ts"],"sourcesContent":["import { brandProviderTool } from '@tanstack/ai'\nimport { convertFunctionToolToResponsesFormat } from '@tanstack/openai-base'\nimport type { ProviderTool, Tool } from '@tanstack/ai'\nimport type { ResponsesFunctionTool } from '@tanstack/openai-base'\nimport type { GrokProviderToolKind } from '../model-meta'\n\nexport type FunctionTool = ResponsesFunctionTool\n\nexport { convertFunctionToolToResponsesFormat as convertFunctionToolToAdapterFormat }\n\nexport type GrokProviderTool<TKind extends GrokProviderToolKind> = ProviderTool<\n 'grok',\n TKind\n>\n\ntype GrokToolKindMarker<TKind extends GrokProviderToolKind> = `grok.${TKind}`\n\nexport interface GrokWebSearchToolConfig {\n type: 'web_search'\n filters?: {\n allowed_domains?: Array<string>\n excluded_domains?: Array<string>\n }\n enable_image_understanding?: boolean\n enable_image_search?: boolean\n}\n\nexport interface GrokXSearchToolConfig {\n type: 'x_search'\n allowed_x_handles?: Array<string>\n excluded_x_handles?: Array<string>\n from_date?: string\n to_date?: string\n enable_image_understanding?: boolean\n enable_video_understanding?: boolean\n}\n\nexport interface GrokFileSearchToolConfig {\n type: 'file_search'\n vector_store_ids: Array<string>\n max_num_results?: number\n}\n\nexport interface GrokMCPToolConfig {\n type: 'mcp'\n server_label: string\n server_url: string\n allowed_tools?: Array<string>\n server_description?: string\n authorization?: string\n headers?: Record<string, string>\n}\n\nexport type GrokServerTool =\n | GrokWebSearchToolConfig\n | GrokXSearchToolConfig\n | GrokFileSearchToolConfig\n | GrokMCPToolConfig\n\ntype GrokProviderToolMetadata<TKind extends GrokProviderToolKind> = Extract<\n GrokServerTool,\n { type: TKind }\n> & {\n __kind: GrokToolKindMarker<TKind>\n}\n\nexport type GrokResponsesTool = GrokServerTool | ResponsesFunctionTool\n\nfunction providerTool<TKind extends GrokProviderToolKind>(\n kind: TKind,\n description: string,\n metadata: Extract<GrokServerTool, { type: TKind }>,\n): GrokProviderTool<TKind> {\n return brandProviderTool<GrokProviderTool<TKind>>({\n name: kind,\n description,\n metadata: {\n __kind: `grok.${kind}`,\n ...metadata,\n },\n })\n}\n\nexport function grokWebSearchTool(\n config: Omit<GrokWebSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'web_search'> {\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.excluded_domains !== undefined\n ) {\n throw new Error(\n 'allowed_domains and excluded_domains cannot both be provided.',\n )\n }\n if (\n config.filters?.allowed_domains !== undefined &&\n config.filters.allowed_domains.length > 5\n ) {\n throw new Error('allowed_domains supports at most 5 domains.')\n }\n if (\n config.filters?.excluded_domains !== undefined &&\n config.filters.excluded_domains.length > 5\n ) {\n throw new Error('excluded_domains supports at most 5 domains.')\n }\n return providerTool('web_search', 'Search the web', {\n type: 'web_search',\n ...config,\n })\n}\n\nexport function grokXSearchTool(\n config: Omit<GrokXSearchToolConfig, 'type'> = {},\n): GrokProviderTool<'x_search'> {\n if (\n config.allowed_x_handles !== undefined &&\n config.excluded_x_handles !== undefined\n ) {\n throw new Error(\n 'allowed_x_handles and excluded_x_handles cannot both be provided.',\n )\n }\n if (\n config.allowed_x_handles !== undefined &&\n config.allowed_x_handles.length > 20\n ) {\n throw new Error('allowed_x_handles supports at most 20 handles.')\n }\n if (\n config.excluded_x_handles !== undefined &&\n config.excluded_x_handles.length > 20\n ) {\n throw new Error('excluded_x_handles supports at most 20 handles.')\n }\n return providerTool('x_search', 'Search X posts', {\n type: 'x_search',\n ...config,\n })\n}\n\nexport function grokFileSearchTool(\n config: Omit<GrokFileSearchToolConfig, 'type'>,\n): GrokProviderTool<'file_search'> {\n if (config.vector_store_ids.length === 0) {\n throw new Error('vector_store_ids must contain at least one collection id.')\n }\n if (config.max_num_results !== undefined) {\n if (config.max_num_results < 1 || config.max_num_results > 50) {\n throw new Error('max_num_results must be between 1 and 50.')\n }\n }\n return providerTool('file_search', 'Search xAI file collections', {\n type: 'file_search',\n ...config,\n })\n}\n\nexport function grokMCPTool(\n config: Omit<GrokMCPToolConfig, 'type'>,\n): GrokProviderTool<'mcp'> {\n if (!config.server_url) {\n throw new Error('server_url must be provided.')\n }\n return providerTool('mcp', config.server_description || 'Remote MCP server', {\n type: 'mcp',\n ...config,\n })\n}\n\nfunction getGrokProviderToolKind(tool: Tool): GrokProviderToolKind | undefined {\n const kind = (tool.metadata as { __kind?: unknown } | undefined)?.__kind\n switch (kind) {\n case 'grok.web_search':\n return 'web_search'\n case 'grok.x_search':\n return 'x_search'\n case 'grok.file_search':\n return 'file_search'\n case 'grok.mcp':\n return 'mcp'\n default:\n return undefined\n }\n}\n\nfunction convertGrokProviderToolToAdapterFormat(\n tool: Tool,\n kind: GrokProviderToolKind,\n): GrokServerTool {\n const metadata = tool.metadata as GrokProviderToolMetadata<typeof kind>\n if (metadata.type !== kind) {\n throw new Error(\n `convertGrokProviderToolToAdapterFormat: tool \"${tool.name}\" has mismatched Grok tool metadata.`,\n )\n }\n const { __kind: _kind, ...toolConfig } = metadata\n void _kind\n return toolConfig\n}\n\nexport function convertToolsToProviderFormat(\n tools: Array<Tool>,\n): Array<GrokResponsesTool> {\n return tools.map((tool) => {\n const grokProviderToolKind = getGrokProviderToolKind(tool)\n if (grokProviderToolKind) {\n return convertGrokProviderToolToAdapterFormat(tool, grokProviderToolKind)\n }\n return convertFunctionToolToResponsesFormat(tool)\n })\n}\n"],"mappings":";;;AAoEA,SAAS,aACP,MACA,aACA,UACyB;CACzB,OAAO,kBAA2C;EAChD,MAAM;EACN;EACA,UAAU;GACR,QAAQ,QAAQ;GAChB,GAAG;EACL;CACF,CAAC;AACH;AAEA,SAAgB,kBACd,SAAgD,CAAC,GACjB;CAChC,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,qBAAqB,KAAA,GAEpC,MAAM,IAAI,MACR,+DACF;CAEF,IACE,OAAO,SAAS,oBAAoB,KAAA,KACpC,OAAO,QAAQ,gBAAgB,SAAS,GAExC,MAAM,IAAI,MAAM,6CAA6C;CAE/D,IACE,OAAO,SAAS,qBAAqB,KAAA,KACrC,OAAO,QAAQ,iBAAiB,SAAS,GAEzC,MAAM,IAAI,MAAM,8CAA8C;CAEhE,OAAO,aAAa,cAAc,kBAAkB;EAClD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,gBACd,SAA8C,CAAC,GACjB;CAC9B,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,uBAAuB,KAAA,GAE9B,MAAM,IAAI,MACR,mEACF;CAEF,IACE,OAAO,sBAAsB,KAAA,KAC7B,OAAO,kBAAkB,SAAS,IAElC,MAAM,IAAI,MAAM,gDAAgD;CAElE,IACE,OAAO,uBAAuB,KAAA,KAC9B,OAAO,mBAAmB,SAAS,IAEnC,MAAM,IAAI,MAAM,iDAAiD;CAEnE,OAAO,aAAa,YAAY,kBAAkB;EAChD,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,mBACd,QACiC;CACjC,IAAI,OAAO,iBAAiB,WAAW,GACrC,MAAM,IAAI,MAAM,2DAA2D;CAE7E,IAAI,OAAO,oBAAoB,KAAA,GACzB;MAAA,OAAO,kBAAkB,KAAK,OAAO,kBAAkB,IACzD,MAAM,IAAI,MAAM,2CAA2C;CAAA;CAG/D,OAAO,aAAa,eAAe,+BAA+B;EAChE,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAgB,YACd,QACyB;CACzB,IAAI,CAAC,OAAO,YACV,MAAM,IAAI,MAAM,8BAA8B;CAEhD,OAAO,aAAa,OAAO,OAAO,sBAAsB,qBAAqB;EAC3E,MAAM;EACN,GAAG;CACL,CAAC;AACH;AAEA,SAAS,wBAAwB,MAA8C;CAE7E,QADc,KAAK,UAA+C,QAClE;EACE,KAAK,mBACH,OAAO;EACT,KAAK,iBACH,OAAO;EACT,KAAK,oBACH,OAAO;EACT,KAAK,YACH,OAAO;EACT,SACE;CACJ;AACF;AAEA,SAAS,uCACP,MACA,MACgB;CAChB,MAAM,WAAW,KAAK;CACtB,IAAI,SAAS,SAAS,MACpB,MAAM,IAAI,MACR,iDAAiD,KAAK,KAAK,qCAC7D;CAEF,MAAM,EAAE,QAAQ,OAAO,GAAG,eAAe;CAEzC,OAAO;AACT;AAEA,SAAgB,6BACd,OAC0B;CAC1B,OAAO,MAAM,KAAK,SAAS;EACzB,MAAM,uBAAuB,wBAAwB,IAAI;EACzD,IAAI,sBACF,OAAO,uCAAuC,MAAM,oBAAoB;EAE1E,OAAO,qCAAqC,IAAI;CAClD,CAAC;AACH"}
@@ -16,6 +16,13 @@ export type GrokVideoAspectRatio = '1:1' | '16:9' | '9:16' | '4:3' | '3:4' | '3:
16
16
  * @experimental Video generation is an experimental feature and may change.
17
17
  */
18
18
  export type GrokVideoResolution = '480p' | '720p' | '1080p';
19
+ /**
20
+ * Resolutions accepted by grok-imagine-video (v1.0). Native 1080p is a
21
+ * grok-imagine-video-1.5 generation feature.
22
+ *
23
+ * @experimental Video generation is an experimental feature and may change.
24
+ */
25
+ export type GrokVideoResolutionV1 = '480p' | '720p';
19
26
  /**
20
27
  * Size strings for grok-imagine video models. The Imagine API is
21
28
  * aspect-ratio based rather than pixel-size based; like the grok-imagine
@@ -26,6 +33,12 @@ export type GrokVideoResolution = '480p' | '720p' | '1080p';
26
33
  * @experimental Video generation is an experimental feature and may change.
27
34
  */
28
35
  export type GrokVideoSize = GrokVideoAspectRatio | `${GrokVideoAspectRatio}_${GrokVideoResolution}`;
36
+ /**
37
+ * Size strings for grok-imagine-video (v1.0) — 1080p is not in the type.
38
+ *
39
+ * @experimental Video generation is an experimental feature and may change.
40
+ */
41
+ export type GrokVideoSizeV1 = GrokVideoAspectRatio | `${GrokVideoAspectRatio}_${GrokVideoResolutionV1}`;
29
42
  /**
30
43
  * Video duration limits enforced by the Imagine API (seconds).
31
44
  */
@@ -41,6 +54,13 @@ export declare function parseGrokVideoSize(size: string): {
41
54
  aspectRatio: string;
42
55
  resolution?: string;
43
56
  } | undefined;
57
+ /**
58
+ * Models that accept native 1080p on text-to-video and image-to-video.
59
+ * Reference-to-video stays capped at 720p even on these models.
60
+ *
61
+ * @experimental Video generation is an experimental feature and may change.
62
+ */
63
+ export declare function isGrokVideoNative1080pModel(model: string): boolean;
44
64
  /**
45
65
  * Validate the `size` template for a given grok video model.
46
66
  *
@@ -76,33 +96,128 @@ export declare const GROK_VIDEO_DURATIONS: {
76
96
  */
77
97
  export declare function getGrokVideoDurationOptions<TModel extends GrokVideoModel>(model: TModel): DurationOptions<GrokVideoModelDurationByName[TModel]>;
78
98
  /**
79
- * Provider-specific options for grok video generation. These map directly
80
- * onto the Imagine API request body and take precedence over the generic
81
- * `size` / `duration` options when both are provided.
99
+ * Request mode for a source-video job. `'edit'` posts to `/v1/videos/edits`
100
+ * (modify the source clip in place); `'extend'` posts to
101
+ * `/v1/videos/extensions` (continue the source clip — `duration` is the
102
+ * length of the **added tail**, not the total). Both require exactly one
103
+ * video prompt part carrying the source clip, and both are
104
+ * `grok-imagine-video` (v1.0) only.
105
+ *
106
+ * Output geometry (aspect ratio / resolution) is inherited from the source
107
+ * clip in both modes, capped at 720p, and edit outputs also inherit the
108
+ * source length — the adapter rejects `size`, `aspect_ratio`, `resolution`,
109
+ * and (in edit mode) `duration` rather than sending fields the API ignores.
82
110
  *
83
111
  * @experimental Video generation is an experimental feature and may change.
84
112
  */
85
- export interface GrokVideoProviderOptions {
113
+ export type GrokVideoMode = 'edit' | 'extend';
114
+ /**
115
+ * Provider options shared by both grok-imagine video models. These map
116
+ * directly onto the Imagine API request body and take precedence over the
117
+ * generic `size` / `duration` options when both are provided.
118
+ *
119
+ * @experimental Video generation is an experimental feature and may change.
120
+ */
121
+ export interface GrokVideoBaseProviderOptions {
86
122
  /**
87
- * Output aspect ratio.
123
+ * Output aspect ratio. Generation only — edit / extend outputs inherit
124
+ * the source clip's geometry and the adapter rejects this in those modes.
88
125
  */
89
126
  aspect_ratio?: GrokVideoAspectRatio;
90
127
  /**
91
- * Output resolution tier.
128
+ * Output resolution tier. Generation only — edit / extend outputs inherit
129
+ * the source clip's geometry and the adapter rejects this in those modes.
130
+ * `1080p` is grok-imagine-video-1.5 generation only; reference-to-video
131
+ * is capped at 720p.
92
132
  */
93
133
  resolution?: GrokVideoResolution;
94
134
  /**
95
- * Video duration in integer seconds (1–15).
135
+ * Video duration in integer seconds (1–15). In `'extend'` mode this is
136
+ * the length of the added tail only, not the total output length. Not
137
+ * valid in `'edit'` mode (the output inherits the source clip's length).
96
138
  */
97
139
  duration?: number;
98
140
  }
99
141
  /**
100
- * Type-only map from model name to its specific provider options.
142
+ * Provider options for grok-imagine-video (v1.0), which is the only model
143
+ * that accepts a source-video edit / extend job.
144
+ *
145
+ * @experimental Video generation is an experimental feature and may change.
146
+ */
147
+ export interface GrokVideoSourceProviderOptions extends GrokVideoBaseProviderOptions {
148
+ /**
149
+ * Selects the request mode for a source-video prompt part: `'edit'`
150
+ * (`/v1/videos/edits`) or `'extend'` (`/v1/videos/extensions`). Required
151
+ * when the prompt carries a video part; not valid without one. Omit for
152
+ * plain generation (`/v1/videos/generations`). grok-imagine-video only.
153
+ */
154
+ mode?: GrokVideoMode;
155
+ }
156
+ /**
157
+ * Provider options for grok-imagine-video-1.5, which adds the
158
+ * reference-to-video inputs on top of the shared options.
159
+ *
160
+ * @experimental Video generation is an experimental feature and may change.
161
+ */
162
+ export interface GrokVideoProviderOptions extends GrokVideoBaseProviderOptions {
163
+ /**
164
+ * Reference images for reference-to-video generation (output capped at
165
+ * 720p). Usually populated from image prompt parts with
166
+ * `metadata.role: 'reference'` (or `'character'`); set explicitly to
167
+ * replace the part-derived list. Reference images are addressed from the
168
+ * prompt text as `<IMAGE_0>`, `<IMAGE_1>`, … in request order, and do not
169
+ * lock the first frame.
170
+ */
171
+ reference_images?: Array<{
172
+ url: string;
173
+ }>;
174
+ /**
175
+ * Preset TTS voices to reference for generated speech (max 3). Voice ids
176
+ * come from the xAI TTS voice roster (e.g. 'eve', 'rex') or a custom
177
+ * voice id, and are addressed from the prompt text as `<AUDIO_0>`,
178
+ * `<AUDIO_1>`, `<AUDIO_2>`.
179
+ */
180
+ reference_audios?: Array<{
181
+ voice_id: string;
182
+ }>;
183
+ }
184
+ /**
185
+ * Widest option surface. Used when `modelOptions` arrives as deserialized
186
+ * JSON and the adapter must validate fields the per-model map already
187
+ * hides at compile time.
188
+ *
189
+ * @experimental Video generation is an experimental feature and may change.
190
+ */
191
+ export type GrokVideoRuntimeOptions = GrokVideoSourceProviderOptions & GrokVideoProviderOptions;
192
+ /**
193
+ * Maximum reference voices accepted by the Imagine video endpoint.
194
+ */
195
+ export declare const GROK_VIDEO_MAX_REFERENCE_AUDIOS = 3;
196
+ /**
197
+ * Maximum reference images accepted by the Imagine video endpoint.
198
+ */
199
+ export declare const GROK_VIDEO_MAX_REFERENCE_IMAGES = 7;
200
+ /**
201
+ * True when the model accepts reference-to-video inputs.
202
+ *
203
+ * @experimental Video generation is an experimental feature and may change.
204
+ */
205
+ export declare function isGrokVideoReferenceModel(model: string): boolean;
206
+ /**
207
+ * True when the model accepts edit / extend source-video jobs.
208
+ *
209
+ * @experimental Video generation is an experimental feature and may change.
210
+ */
211
+ export declare function isGrokVideoSourceModel(model: string): boolean;
212
+ /**
213
+ * Type-only map from model name to its specific provider options. Only
214
+ * grok-imagine-video-1.5 exposes the reference-to-video fields. Only
215
+ * grok-imagine-video (v1.0) exposes `mode` for edit / extend.
101
216
  *
102
217
  * @experimental Video generation is an experimental feature and may change.
103
218
  */
104
219
  export type GrokVideoModelProviderOptionsByName = {
105
- 'grok-imagine-video': GrokVideoProviderOptions;
220
+ 'grok-imagine-video': GrokVideoSourceProviderOptions;
106
221
  'grok-imagine-video-1.5': GrokVideoProviderOptions;
107
222
  };
108
223
  /**
@@ -111,25 +226,20 @@ export type GrokVideoModelProviderOptionsByName = {
111
226
  * @experimental Video generation is an experimental feature and may change.
112
227
  */
113
228
  export type GrokVideoModelSizeByName = {
114
- 'grok-imagine-video': GrokVideoSize;
229
+ 'grok-imagine-video': GrokVideoSizeV1;
115
230
  'grok-imagine-video-1.5': GrokVideoSize;
116
231
  };
117
232
  /**
118
233
  * Type-only map from model name to the non-text prompt modalities it accepts.
119
- * Both models accept an `image` prompt part as the starting frame:
120
- * `grok-imagine-video` (v1.0) does text-to-video and image-to-video, while
121
- * `grok-imagine-video-1.5` is image-to-video only (the image is required).
234
+ * Both models support text-to-video and accept an optional `image` prompt
235
+ * part as the starting frame; image parts with `metadata.role: 'reference'`
236
+ * or `'character'` become `reference_images` (grok-imagine-video-1.5 only).
237
+ * A `video` prompt part carries the source clip for edit / extension mode
238
+ * on grok-imagine-video only (`modelOptions.mode: 'edit' | 'extend'`).
122
239
  *
123
240
  * @experimental Video generation is an experimental feature and may change.
124
241
  */
125
242
  export type GrokVideoModelInputModalitiesByName = {
126
- 'grok-imagine-video': readonly ['image'];
243
+ 'grok-imagine-video': readonly ['image', 'video'];
127
244
  'grok-imagine-video-1.5': readonly ['image'];
128
245
  };
129
- /**
130
- * True when the model only supports image-to-video (a starting frame is
131
- * required).
132
- *
133
- * @experimental Video generation is an experimental feature and may change.
134
- */
135
- export declare function isImageToVideoOnlyModel(model: string): boolean;