@tanstack/ai-client 0.6.0 → 0.7.2
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.
- package/README.md +6 -6
- package/dist/esm/chat-client.d.ts +62 -3
- package/dist/esm/chat-client.js +264 -54
- package/dist/esm/chat-client.js.map +1 -1
- package/dist/esm/connection-adapters.d.ts +28 -12
- package/dist/esm/connection-adapters.js +93 -0
- package/dist/esm/connection-adapters.js.map +1 -1
- package/dist/esm/events.js +1 -1
- package/dist/esm/events.js.map +1 -1
- package/dist/esm/generation-client.d.ts +5 -5
- package/dist/esm/generation-client.js +1 -1
- package/dist/esm/generation-client.js.map +1 -1
- package/dist/esm/generation-types.d.ts +5 -5
- package/dist/esm/generation-types.js.map +1 -1
- package/dist/esm/index.d.ts +4 -2
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/realtime-client.d.ts +98 -0
- package/dist/esm/realtime-client.js +384 -0
- package/dist/esm/realtime-client.js.map +1 -0
- package/dist/esm/realtime-types.d.ts +118 -0
- package/dist/esm/types.d.ts +23 -2
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/video-generation-client.d.ts +5 -5
- package/dist/esm/video-generation-client.js +1 -1
- package/dist/esm/video-generation-client.js.map +1 -1
- package/package.json +3 -2
- package/src/chat-client.ts +355 -86
- package/src/connection-adapters.ts +147 -12
- package/src/events.ts +1 -1
- package/src/generation-client.ts +7 -7
- package/src/generation-types.ts +5 -5
- package/src/index.ts +11 -0
- package/src/realtime-client.ts +526 -0
- package/src/realtime-types.ts +180 -0
- package/src/types.ts +31 -2
- package/src/video-generation-client.ts +6 -6
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generation-client.js","sources":["../../src/generation-client.ts"],"sourcesContent":["import { GENERATION_EVENTS } from './generation-types'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type { ConnectionAdapter } from './connection-adapters'\nimport type {\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\ninterface GenerationCallbacks<TResult, TOutput> {\n onResult?: (result: TResult) => TOutput | null | void\n onError?: (error: Error) => void\n onProgress?: (progress: number, message?: string) => void\n onChunk?: (chunk: StreamChunk) => void\n onResultChange?: (result: TOutput | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onErrorChange?: (error: Error | undefined) => void\n onStatusChange?: (status: GenerationClientState) => void\n}\n\n/**\n * A lightweight, generic client for one-shot generation tasks\n * (image, speech, transcription, summarize).\n *\n * Supports two transport modes:\n * - **ConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).\n * Server wraps results in StreamChunk events with CUSTOM event names.\n * - **Fetcher** — Direct async function call. No streaming protocol needed.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```typescript\n * // With ConnectionAdapter (streaming)\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * connection: fetchServerSentEvents('/api/generate/image'),\n * onResultChange: setResult,\n * onLoadingChange: setIsLoading,\n * })\n *\n * // With fetcher (direct)\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * fetcher: async (input) => {\n * const res = await fetch('/api/generate/image', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json()\n * },\n * })\n *\n * await client.generate({ prompt: 'A sunset over mountains' })\n * ```\n */\nexport class GenerationClient<\n TInput extends Record<string, any>,\n TResult,\n TOutput = TResult,\n> {\n private connection: ConnectionAdapter | undefined\n private fetcher: GenerationFetcher<TInput, TResult> | undefined\n private body: Record<string, any>\n private result: TOutput | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private abortController: AbortController | null = null\n private callbacksRef: GenerationCallbacks<TResult, TOutput>\n\n constructor(\n options: GenerationClientOptions<TInput, TResult, TOutput> &\n (\n | { connection: ConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<TInput, TResult>\n connection?: never\n }\n ),\n ) {\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.body = options.body ?? {}\n\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n }\n }\n\n /**\n * Trigger a generation request.\n * Only one generation can be in-flight at a time; calling generate()\n * while already generating will be a no-op.\n */\n async generate(input: TInput): Promise<void> {\n if (this.isLoading) return\n\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n // Direct fetch path\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(parseSSEResponse(result, signal))\n } else {\n this.setResult(result)\n this.setStatus('success')\n }\n } else if (this.connection) {\n // ConnectionAdapter streaming path\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect([], mergedData, signal)\n await this.processStream(stream)\n } else {\n throw new Error(\n 'GenerationClient requires either a connection or fetcher option',\n )\n }\n } catch (err: any) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n this.setError(error)\n this.setStatus('error')\n this.callbacksRef.onError?.(error)\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n\n /**\n * Process a stream of AG-UI events from the ConnectionAdapter.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<void> {\n for await (const chunk of source) {\n if (this.abortController?.signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\n\n switch (chunk.type) {\n case 'CUSTOM': {\n if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as TResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.callbacksRef.onProgress?.(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n throw new Error(chunk.error.message)\n }\n }\n }\n }\n\n /**\n * Abort any in-flight generation request.\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n }\n }\n\n /**\n * Clear the result, error, and return to idle state.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.setError(undefined)\n this.setStatus('idle')\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: TResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"generation-client.js","sources":["../../src/generation-client.ts"],"sourcesContent":["import { GENERATION_EVENTS } from './generation-types'\nimport { parseSSEResponse } from './sse-parser'\nimport type { StreamChunk } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\nimport type {\n GenerationClientOptions,\n GenerationClientState,\n GenerationFetcher,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\ninterface GenerationCallbacks<TResult, TOutput> {\n onResult?: (result: TResult) => TOutput | null | void\n onError?: (error: Error) => void\n onProgress?: (progress: number, message?: string) => void\n onChunk?: (chunk: StreamChunk) => void\n onResultChange?: (result: TOutput | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onErrorChange?: (error: Error | undefined) => void\n onStatusChange?: (status: GenerationClientState) => void\n}\n\n/**\n * A lightweight, generic client for one-shot generation tasks\n * (image, speech, transcription, summarize).\n *\n * Supports two transport modes:\n * - **ConnectConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).\n * Server wraps results in StreamChunk events with CUSTOM event names.\n * - **Fetcher** — Direct async function call. No streaming protocol needed.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n *\n * @example\n * ```typescript\n * // With streaming connection adapter\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * connection: fetchServerSentEvents('/api/generate/image'),\n * onResultChange: setResult,\n * onLoadingChange: setIsLoading,\n * })\n *\n * // With fetcher (direct)\n * const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({\n * fetcher: async (input) => {\n * const res = await fetch('/api/generate/image', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json()\n * },\n * })\n *\n * await client.generate({ prompt: 'A sunset over mountains' })\n * ```\n */\nexport class GenerationClient<\n TInput extends Record<string, any>,\n TResult,\n TOutput = TResult,\n> {\n private connection: ConnectConnectionAdapter | undefined\n private fetcher: GenerationFetcher<TInput, TResult> | undefined\n private body: Record<string, any>\n private result: TOutput | null = null\n private isLoading = false\n private error: Error | undefined = undefined\n private status: GenerationClientState = 'idle'\n private abortController: AbortController | null = null\n private callbacksRef: GenerationCallbacks<TResult, TOutput>\n\n constructor(\n options: GenerationClientOptions<TInput, TResult, TOutput> &\n (\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<TInput, TResult>\n connection?: never\n }\n ),\n ) {\n this.connection = options.connection\n this.fetcher = options.fetcher\n this.body = options.body ?? {}\n\n this.callbacksRef = {\n onResult: options.onResult,\n onError: options.onError,\n onProgress: options.onProgress,\n onChunk: options.onChunk,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n }\n }\n\n /**\n * Trigger a generation request.\n * Only one generation can be in-flight at a time; calling generate()\n * while already generating will be a no-op.\n */\n async generate(input: TInput): Promise<void> {\n if (this.isLoading) return\n\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n // Direct fetch path\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n if (result instanceof Response) {\n // Server function returned SSE Response — parse stream\n await this.processStream(parseSSEResponse(result, signal))\n } else {\n this.setResult(result)\n this.setStatus('success')\n }\n } else if (this.connection) {\n // Streaming adapter path\n const mergedData = { ...this.body, ...input }\n const stream = this.connection.connect([], mergedData, signal)\n await this.processStream(stream)\n } else {\n throw new Error(\n 'GenerationClient requires either a connection or fetcher option',\n )\n }\n } catch (err: any) {\n if (signal.aborted) return\n const error = err instanceof Error ? err : new Error(String(err))\n this.setError(error)\n this.setStatus('error')\n this.callbacksRef.onError?.(error)\n } finally {\n this.abortController = null\n this.setIsLoading(false)\n }\n }\n\n /**\n * Process a stream of AG-UI events from the streaming connection adapter.\n */\n private async processStream(\n source: AsyncIterable<StreamChunk>,\n ): Promise<void> {\n for await (const chunk of source) {\n if (this.abortController?.signal.aborted) break\n\n this.callbacksRef.onChunk?.(chunk)\n\n switch (chunk.type) {\n case 'CUSTOM': {\n if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as TResult)\n } else if (chunk.name === GENERATION_EVENTS.PROGRESS) {\n const { progress, message } = chunk.value as {\n progress: number\n message?: string\n }\n this.callbacksRef.onProgress?.(progress, message)\n }\n break\n }\n case 'RUN_FINISHED': {\n this.setStatus('success')\n break\n }\n case 'RUN_ERROR': {\n throw new Error(chunk.error.message)\n }\n }\n }\n }\n\n /**\n * Abort any in-flight generation request.\n */\n stop(): void {\n if (this.abortController) {\n this.abortController.abort()\n this.abortController = null\n }\n this.setIsLoading(false)\n if (this.status === 'generating') {\n this.setStatus('idle')\n }\n }\n\n /**\n * Clear the result, error, and return to idle state.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.setError(undefined)\n this.setStatus('idle')\n }\n\n /**\n * Update options without recreating the client.\n */\n updateOptions(\n options: Partial<\n Pick<\n GenerationClientOptions<TInput, TResult, TOutput>,\n 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'\n >\n >,\n ): void {\n if (options.body !== undefined) {\n this.body = options.body ?? {}\n }\n if (options.onResult !== undefined) {\n this.callbacksRef.onResult = options.onResult\n }\n if (options.onError !== undefined) {\n this.callbacksRef.onError = options.onError\n }\n if (options.onProgress !== undefined) {\n this.callbacksRef.onProgress = options.onProgress\n }\n if (options.onChunk !== undefined) {\n this.callbacksRef.onChunk = options.onChunk\n }\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getIsLoading(): boolean {\n return this.isLoading\n }\n\n getError(): Error | undefined {\n return this.error\n }\n\n getStatus(): GenerationClientState {\n return this.status\n }\n\n // ===========================\n // Private state setters\n // ===========================\n\n private setResult(rawResult: TResult | null): void {\n if (rawResult === null) {\n this.result = null\n this.callbacksRef.onResultChange?.(null)\n return\n }\n\n if (this.callbacksRef.onResult) {\n const transformed = this.callbacksRef.onResult(rawResult)\n if (transformed === null) {\n // null return → keep previous result unchanged\n return\n }\n if (transformed !== undefined) {\n // Non-null, non-undefined → use transformed value\n this.result = transformed\n this.callbacksRef.onResultChange?.(this.result)\n return\n }\n }\n\n // No onResult callback, or callback returned void → use raw value\n this.result = rawResult as unknown as TOutput\n this.callbacksRef.onResultChange?.(this.result)\n }\n\n private setIsLoading(isLoading: boolean): void {\n this.isLoading = isLoading\n this.callbacksRef.onLoadingChange?.(isLoading)\n }\n\n private setError(error: Error | undefined): void {\n this.error = error\n this.callbacksRef.onErrorChange?.(error)\n }\n\n private setStatus(status: GenerationClientState): void {\n this.status = status\n this.callbacksRef.onStatusChange?.(status)\n }\n}\n"],"names":[],"mappings":";;AA2DO,MAAM,iBAIX;AAAA,EAWA,YACE,SAQA;AAhBF,SAAQ,SAAyB;AACjC,SAAQ,YAAY;AACpB,SAAQ,QAA2B;AACnC,SAAQ,SAAgC;AACxC,SAAQ,kBAA0C;AAahD,SAAK,aAAa,QAAQ;AAC1B,SAAK,UAAU,QAAQ;AACvB,SAAK,OAAO,QAAQ,QAAQ,CAAA;AAE5B,SAAK,eAAe;AAAA,MAClB,UAAU,QAAQ;AAAA,MAClB,SAAS,QAAQ;AAAA,MACjB,YAAY,QAAQ;AAAA,MACpB,SAAS,QAAQ;AAAA,MACjB,gBAAgB,QAAQ;AAAA,MACxB,iBAAiB,QAAQ;AAAA,MACzB,eAAe,QAAQ;AAAA,MACvB,gBAAgB,QAAQ;AAAA,IAAA;AAAA,EAE5B;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAOA,MAAM,SAAS,OAA8B;AAC3C,QAAI,KAAK,UAAW;AAEpB,SAAK,aAAa,IAAI;AACtB,SAAK,UAAU,YAAY;AAC3B,SAAK,SAAS,MAAS;AAEvB,UAAM,kBAAkB,IAAI,gBAAA;AAC5B,SAAK,kBAAkB;AACvB,UAAM,EAAE,WAAW;AAEnB,QAAI;AACF,UAAI,KAAK,SAAS;AAEhB,cAAM,SAAS,MAAM,KAAK,QAAQ,OAAO,EAAE,QAAQ;AACnD,YAAI,OAAO,QAAS;AACpB,YAAI,kBAAkB,UAAU;AAE9B,gBAAM,KAAK,cAAc,iBAAiB,QAAQ,MAAM,CAAC;AAAA,QAC3D,OAAO;AACL,eAAK,UAAU,MAAM;AACrB,eAAK,UAAU,SAAS;AAAA,QAC1B;AAAA,MACF,WAAW,KAAK,YAAY;AAE1B,cAAM,aAAa,EAAE,GAAG,KAAK,MAAM,GAAG,MAAA;AACtC,cAAM,SAAS,KAAK,WAAW,QAAQ,CAAA,GAAI,YAAY,MAAM;AAC7D,cAAM,KAAK,cAAc,MAAM;AAAA,MACjC,OAAO;AACL,cAAM,IAAI;AAAA,UACR;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF,SAAS,KAAU;AACjB,UAAI,OAAO,QAAS;AACpB,YAAM,QAAQ,eAAe,QAAQ,MAAM,IAAI,MAAM,OAAO,GAAG,CAAC;AAChE,WAAK,SAAS,KAAK;AACnB,WAAK,UAAU,OAAO;AACtB,WAAK,aAAa,UAAU,KAAK;AAAA,IACnC,UAAA;AACE,WAAK,kBAAkB;AACvB,WAAK,aAAa,KAAK;AAAA,IACzB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,cACZ,QACe;AACf,qBAAiB,SAAS,QAAQ;AAChC,UAAI,KAAK,iBAAiB,OAAO,QAAS;AAE1C,WAAK,aAAa,UAAU,KAAK;AAEjC,cAAQ,MAAM,MAAA;AAAA,QACZ,KAAK,UAAU;AACb,cAAI,MAAM,SAAS,kBAAkB,QAAQ;AAC3C,iBAAK,UAAU,MAAM,KAAgB;AAAA,UACvC,WAAW,MAAM,SAAS,kBAAkB,UAAU;AACpD,kBAAM,EAAE,UAAU,QAAA,IAAY,MAAM;AAIpC,iBAAK,aAAa,aAAa,UAAU,OAAO;AAAA,UAClD;AACA;AAAA,QACF;AAAA,QACA,KAAK,gBAAgB;AACnB,eAAK,UAAU,SAAS;AACxB;AAAA,QACF;AAAA,QACA,KAAK,aAAa;AAChB,gBAAM,IAAI,MAAM,MAAM,MAAM,OAAO;AAAA,QACrC;AAAA,MAAA;AAAA,IAEJ;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,OAAa;AACX,QAAI,KAAK,iBAAiB;AACxB,WAAK,gBAAgB,MAAA;AACrB,WAAK,kBAAkB;AAAA,IACzB;AACA,SAAK,aAAa,KAAK;AACvB,QAAI,KAAK,WAAW,cAAc;AAChC,WAAK,UAAU,MAAM;AAAA,IACvB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,QAAc;AACZ,SAAK,KAAA;AACL,SAAK,UAAU,IAAI;AACnB,SAAK,SAAS,MAAS;AACvB,SAAK,UAAU,MAAM;AAAA,EACvB;AAAA;AAAA;AAAA;AAAA,EAKA,cACE,SAMM;AACN,QAAI,QAAQ,SAAS,QAAW;AAC9B,WAAK,OAAO,QAAQ,QAAQ,CAAA;AAAA,IAC9B;AACA,QAAI,QAAQ,aAAa,QAAW;AAClC,WAAK,aAAa,WAAW,QAAQ;AAAA,IACvC;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,UAAU,QAAQ;AAAA,IACtC;AACA,QAAI,QAAQ,eAAe,QAAW;AACpC,WAAK,aAAa,aAAa,QAAQ;AAAA,IACzC;AACA,QAAI,QAAQ,YAAY,QAAW;AACjC,WAAK,aAAa,UAAU,QAAQ;AAAA,IACtC;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAMA,YAA4B;AAC1B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,eAAwB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,WAA8B;AAC5B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,YAAmC;AACjC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA,EAMQ,UAAU,WAAiC;AACjD,QAAI,cAAc,MAAM;AACtB,WAAK,SAAS;AACd,WAAK,aAAa,iBAAiB,IAAI;AACvC;AAAA,IACF;AAEA,QAAI,KAAK,aAAa,UAAU;AAC9B,YAAM,cAAc,KAAK,aAAa,SAAS,SAAS;AACxD,UAAI,gBAAgB,MAAM;AAExB;AAAA,MACF;AACA,UAAI,gBAAgB,QAAW;AAE7B,aAAK,SAAS;AACd,aAAK,aAAa,iBAAiB,KAAK,MAAM;AAC9C;AAAA,MACF;AAAA,IACF;AAGA,SAAK,SAAS;AACd,SAAK,aAAa,iBAAiB,KAAK,MAAM;AAAA,EAChD;AAAA,EAEQ,aAAa,WAA0B;AAC7C,SAAK,YAAY;AACjB,SAAK,aAAa,kBAAkB,SAAS;AAAA,EAC/C;AAAA,EAEQ,SAAS,OAAgC;AAC/C,SAAK,QAAQ;AACb,SAAK,aAAa,gBAAgB,KAAK;AAAA,EACzC;AAAA,EAEQ,UAAU,QAAqC;AACrD,SAAK,SAAS;AACd,SAAK,aAAa,iBAAiB,MAAM;AAAA,EAC3C;AACF;"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { StreamChunk } from '@tanstack/ai';
|
|
2
|
-
import {
|
|
2
|
+
import { ConnectConnectionAdapter } from './connection-adapters.js';
|
|
3
3
|
/**
|
|
4
4
|
* Infers the output type from an `onResult` callback's return type.
|
|
5
5
|
*
|
|
@@ -50,10 +50,10 @@ export interface GenerationFetcherOptions {
|
|
|
50
50
|
export type GenerationFetcher<TInput, TResult> = (input: TInput, options?: GenerationFetcherOptions) => Promise<TResult | Response>;
|
|
51
51
|
/**
|
|
52
52
|
* Transport configuration for generation clients.
|
|
53
|
-
* Supports either a
|
|
53
|
+
* Supports either a connect-based streaming adapter or a direct fetcher function.
|
|
54
54
|
*/
|
|
55
55
|
export type GenerationTransport<TInput, TResult> = {
|
|
56
|
-
connection:
|
|
56
|
+
connection: ConnectConnectionAdapter;
|
|
57
57
|
fetcher?: never;
|
|
58
58
|
} | {
|
|
59
59
|
fetcher: GenerationFetcher<TInput, TResult>;
|
|
@@ -69,7 +69,7 @@ export type GenerationTransport<TInput, TResult> = {
|
|
|
69
69
|
export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
70
70
|
/** Unique identifier for this generation client instance */
|
|
71
71
|
id?: string;
|
|
72
|
-
/** Additional body parameters to send with
|
|
72
|
+
/** Additional body parameters to send with connect-based adapter requests */
|
|
73
73
|
body?: Record<string, any>;
|
|
74
74
|
/**
|
|
75
75
|
* Callback when a result is received. Can optionally return a transformed value
|
|
@@ -84,7 +84,7 @@ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
|
|
|
84
84
|
onError?: (error: Error) => void;
|
|
85
85
|
/** Callback when progress is reported (0-100) */
|
|
86
86
|
onProgress?: (progress: number, message?: string) => void;
|
|
87
|
-
/** Callback for each stream chunk (
|
|
87
|
+
/** Callback for each stream chunk (connect-based adapter mode only) */
|
|
88
88
|
onChunk?: (chunk: StreamChunk) => void;
|
|
89
89
|
/** @internal Called when result changes */
|
|
90
90
|
onResultChange?: (result: TOutput | null) => void;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generation-types.js","sources":["../../src/generation-types.ts"],"sourcesContent":["import type { StreamChunk } from '@tanstack/ai'\nimport type {
|
|
1
|
+
{"version":3,"file":"generation-types.js","sources":["../../src/generation-types.ts"],"sourcesContent":["import type { StreamChunk } from '@tanstack/ai'\nimport type { ConnectConnectionAdapter } from './connection-adapters'\n\n// ===========================\n// Inference Utilities\n// ===========================\n\n/**\n * Infers the output type from an `onResult` callback's return type.\n *\n * - If the callback returns a concrete type (excluding null/void/undefined), uses that type.\n * - If the callback only returns null/void/undefined, or is not provided, falls back to TResult.\n *\n * @template TResult - The raw result type from the generation\n * @template TFn - The onResult callback type (or undefined if not provided)\n */\nexport type InferGenerationOutput<TResult, TFn> = TFn extends (\n result: any,\n) => infer R\n ? [Exclude<R, null | void | undefined>] extends [never]\n ? TResult\n : Exclude<R, null | void | undefined>\n : TResult\n\n// ===========================\n// State\n// ===========================\n\n/**\n * State machine for generation clients.\n * Simpler than ChatClientState since generation is a single request/response cycle.\n */\nexport type GenerationClientState = 'idle' | 'generating' | 'success' | 'error'\n\n// ===========================\n// Event Constants\n// ===========================\n\n/**\n * Well-known CUSTOM event names used by generation clients.\n * These events are emitted by the server-side streaming helpers\n * and consumed by the client-side GenerationClient.\n */\nexport const GENERATION_EVENTS = {\n /** The generation result payload */\n RESULT: 'generation:result',\n /** Progress update (0-100) with optional message */\n PROGRESS: 'generation:progress',\n /** Video job created with jobId */\n VIDEO_JOB_CREATED: 'video:job:created',\n /** Video job status update */\n VIDEO_STATUS: 'video:status',\n} as const\n\n// ===========================\n// Transport Types\n// ===========================\n\n/**\n * Options passed to a fetcher function by the generation client.\n */\nexport interface GenerationFetcherOptions {\n /** AbortSignal that is triggered when the user calls `stop()` */\n signal: AbortSignal\n}\n\n/**\n * A direct async function that performs a generation request.\n *\n * Can return the result directly, or return a `Response` with an SSE body\n * (e.g., from a TanStack Start server function using `toServerSentEventsResponse()`).\n * When a `Response` is returned, the client will parse it as an SSE stream.\n *\n * @template TInput - The input type for the generation request\n * @template TResult - The result type returned by the generation\n */\nexport type GenerationFetcher<TInput, TResult> = (\n input: TInput,\n options?: GenerationFetcherOptions,\n) => Promise<TResult | Response>\n\n/**\n * Transport configuration for generation clients.\n * Supports either a connect-based streaming adapter or a direct fetcher function.\n */\nexport type GenerationTransport<TInput, TResult> =\n | { connection: ConnectConnectionAdapter; fetcher?: never }\n | { fetcher: GenerationFetcher<TInput, TResult>; connection?: never }\n\n// ===========================\n// Client Options\n// ===========================\n\n/**\n * Options for the GenerationClient.\n *\n * @template TInput - The input type for the generation request (used by consuming code)\n * @template TResult - The result type returned by the generation\n * @template TOutput - The output type after optional transform (defaults to TResult)\n */\n// eslint-disable-next-line @typescript-eslint/naming-convention\nexport interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {\n /** Unique identifier for this generation client instance */\n id?: string\n\n /** Additional body parameters to send with connect-based adapter requests */\n body?: Record<string, any>\n\n /**\n * Callback when a result is received. Can optionally return a transformed value\n * that replaces the stored result.\n *\n * - Return a non-null value to transform and store it as the result\n * - Return `null` to keep the previous result unchanged\n * - Return nothing (`void`) to store the raw result as-is\n */\n onResult?: (result: TResult) => TOutput | null | void\n /** Callback when an error occurs */\n onError?: (error: Error) => void\n /** Callback when progress is reported (0-100) */\n onProgress?: (progress: number, message?: string) => void\n /** Callback for each stream chunk (connect-based adapter mode only) */\n onChunk?: (chunk: StreamChunk) => void\n\n // Framework state callbacks (set by hooks, not users)\n /** @internal Called when result changes */\n onResultChange?: (result: TOutput | null) => void\n /** @internal Called when loading state changes */\n onLoadingChange?: (isLoading: boolean) => void\n /** @internal Called when error state changes */\n onErrorChange?: (error: Error | undefined) => void\n /** @internal Called when generation status changes */\n onStatusChange?: (status: GenerationClientState) => void\n}\n\n// ===========================\n// Video-Specific Options\n// ===========================\n\n/**\n * Video status information returned during job polling.\n */\nexport interface VideoStatusInfo {\n /** Job identifier */\n jobId: string\n /** Current status of the video generation job */\n status: 'pending' | 'processing' | 'completed' | 'failed'\n /** Progress percentage (0-100), if available */\n progress?: number\n /** URL to the generated video (when completed) */\n url?: string\n /** Error message if status is 'failed' */\n error?: string\n}\n\n/**\n * Composite result for video generation (job completion).\n */\nexport interface VideoGenerateResult {\n /** Job identifier */\n jobId: string\n /** Final status */\n status: 'completed'\n /** URL to the generated video */\n url: string\n /** When the URL expires, if applicable */\n expiresAt?: Date\n}\n\n/**\n * Options for the VideoGenerationClient.\n */\nexport interface VideoGenerationClientOptions<\n TOutput = VideoGenerateResult,\n> extends GenerationClientOptions<\n VideoGenerateInput,\n VideoGenerateResult,\n TOutput\n> {\n /** Callback when a video job is created */\n onJobCreated?: (jobId: string) => void\n /** Callback on each status update */\n onStatusUpdate?: (status: VideoStatusInfo) => void\n\n // Framework state callbacks\n /** @internal Called when jobId changes */\n onJobIdChange?: (jobId: string | null) => void\n /** @internal Called when video status changes */\n onVideoStatusChange?: (status: VideoStatusInfo | null) => void\n}\n\n// ===========================\n// Input Types\n// ===========================\n\n/**\n * Input for image generation.\n */\nexport interface ImageGenerateInput {\n /** Text description of the desired image(s) */\n prompt: string\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: string\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text-to-speech generation.\n */\nexport interface SpeechGenerateInput {\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for audio transcription.\n */\nexport interface TranscriptionGenerateInput {\n /** The audio data to transcribe - can be base64 string, File, or Blob */\n audio: string | File | Blob\n /** The language of the audio in ISO-639-1 format (e.g., 'en') */\n language?: string\n /** An optional prompt to guide the transcription */\n prompt?: string\n /** The format of the transcription output */\n responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for text summarization.\n */\nexport interface SummarizeGenerateInput {\n /** The text to summarize */\n text: string\n /** Maximum length of the summary */\n maxLength?: number\n /** Style of the summary */\n style?: 'bullet-points' | 'paragraph' | 'concise'\n /** Topics to focus on */\n focus?: Array<string>\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n\n/**\n * Input for video generation.\n */\nexport interface VideoGenerateInput {\n /** Text description of the desired video */\n prompt: string\n /** Video size — format depends on provider (e.g., \"16:9\", \"1280x720\") */\n size?: string\n /** Video duration in seconds */\n duration?: number\n /** Model-specific options */\n modelOptions?: Record<string, any>\n}\n"],"names":[],"mappings":"AA2CO,MAAM,oBAAoB;AAAA;AAAA,EAE/B,QAAQ;AAAA;AAAA,EAER,UAAU;AAAA;AAAA,EAEV,mBAAmB;AAAA;AAAA,EAEnB,cAAc;AAChB;"}
|
package/dist/esm/index.d.ts
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export { ChatClient } from './chat-client.js';
|
|
2
|
+
export { RealtimeClient } from './realtime-client.js';
|
|
2
3
|
export { GenerationClient } from './generation-client.js';
|
|
3
4
|
export { VideoGenerationClient } from './video-generation-client.js';
|
|
4
|
-
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, ChatClientOptions, ChatRequestBody, InferChatMessages, ChatClientState, MultimodalContent, } from './types.js';
|
|
5
|
+
export type { UIMessage, MessagePart, TextPart, ToolCallPart, ToolResultPart, ThinkingPart, ChatClientOptions, ChatRequestBody, InferChatMessages, ChatClientState, ConnectionStatus, MultimodalContent, } from './types.js';
|
|
5
6
|
export type { InferGenerationOutput, GenerationClientState, GenerationClientOptions, GenerationFetcher, GenerationFetcherOptions, GenerationTransport, VideoGenerationClientOptions, VideoStatusInfo, VideoGenerateResult, ImageGenerateInput, SpeechGenerateInput, TranscriptionGenerateInput, SummarizeGenerateInput, VideoGenerateInput, } from './generation-types.js';
|
|
6
7
|
export { GENERATION_EVENTS } from './generation-types.js';
|
|
7
8
|
export { clientTools, createChatClientOptions } from './types.js';
|
|
8
9
|
export type { ExtractToolNames, ExtractToolInput, ExtractToolOutput, } from './tool-types.js';
|
|
9
10
|
export type { AnyClientTool } from '@tanstack/ai';
|
|
10
|
-
export {
|
|
11
|
+
export type { RealtimeAdapter, RealtimeConnection, RealtimeClientOptions, RealtimeClientState, RealtimeStateChangeCallback, } from './realtime-types.js';
|
|
12
|
+
export { fetchServerSentEvents, fetchHttpStream, stream, rpcStream, type ConnectConnectionAdapter, type ConnectionAdapter, type FetchConnectionOptions, type SubscribeConnectionAdapter, } from './connection-adapters.js';
|
|
11
13
|
export { uiMessageToModelMessages, modelMessageToUIMessage, modelMessagesToUIMessages, convertMessagesToModelMessages, normalizeToUIMessage, generateMessageId, } from '@tanstack/ai';
|
|
12
14
|
export { StreamProcessor, ImmediateStrategy, PunctuationStrategy, BatchStrategy, WordBoundaryStrategy, CompositeStrategy, parsePartialJSON, PartialJSONParser, defaultJSONParser, type ChunkStrategy, type StreamProcessorOptions, type StreamProcessorEvents, type InternalToolCallState, type ToolCallState, type ToolResultState, type JSONParser, type ChunkRecording, type ProcessorResult, type ProcessorState, } from '@tanstack/ai';
|
package/dist/esm/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ChatClient } from "./chat-client.js";
|
|
2
|
+
import { RealtimeClient } from "./realtime-client.js";
|
|
2
3
|
import { GenerationClient } from "./generation-client.js";
|
|
3
4
|
import { VideoGenerationClient } from "./video-generation-client.js";
|
|
4
5
|
import { GENERATION_EVENTS } from "./generation-types.js";
|
|
@@ -14,6 +15,7 @@ export {
|
|
|
14
15
|
ImmediateStrategy,
|
|
15
16
|
PartialJSONParser,
|
|
16
17
|
PunctuationStrategy,
|
|
18
|
+
RealtimeClient,
|
|
17
19
|
StreamProcessor,
|
|
18
20
|
VideoGenerationClient,
|
|
19
21
|
WordBoundaryStrategy,
|
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { AudioVisualization, RealtimeMessage, RealtimeMode, RealtimeStatus } from '@tanstack/ai';
|
|
2
|
+
import { RealtimeClientOptions, RealtimeStateChangeCallback } from './realtime-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Client for managing realtime voice conversations.
|
|
5
|
+
*
|
|
6
|
+
* Handles connection lifecycle, audio I/O, message state,
|
|
7
|
+
* and tool execution for realtime voice-to-voice AI interactions.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { RealtimeClient } from '@tanstack/ai-client'
|
|
12
|
+
* import { openaiRealtime } from '@tanstack/ai-openai'
|
|
13
|
+
*
|
|
14
|
+
* const client = new RealtimeClient({
|
|
15
|
+
* getToken: () => fetch('/api/realtime-token').then(r => r.json()),
|
|
16
|
+
* adapter: openaiRealtime(),
|
|
17
|
+
* tools: [myTool.client(handler)],
|
|
18
|
+
* onMessage: (msg) => console.log('Message:', msg),
|
|
19
|
+
* })
|
|
20
|
+
*
|
|
21
|
+
* await client.connect()
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
export declare class RealtimeClient {
|
|
25
|
+
private options;
|
|
26
|
+
private connection;
|
|
27
|
+
private token;
|
|
28
|
+
private tokenRefreshTimeout;
|
|
29
|
+
private clientTools;
|
|
30
|
+
private stateChangeCallbacks;
|
|
31
|
+
private unsubscribers;
|
|
32
|
+
private state;
|
|
33
|
+
constructor(options: RealtimeClientOptions);
|
|
34
|
+
/**
|
|
35
|
+
* Connect to the realtime session.
|
|
36
|
+
* Fetches a token and establishes the connection.
|
|
37
|
+
*/
|
|
38
|
+
connect(): Promise<void>;
|
|
39
|
+
/**
|
|
40
|
+
* Disconnect from the realtime session.
|
|
41
|
+
*/
|
|
42
|
+
disconnect(): Promise<void>;
|
|
43
|
+
/**
|
|
44
|
+
* Start listening for voice input.
|
|
45
|
+
* Only needed when vadMode is 'manual'.
|
|
46
|
+
*/
|
|
47
|
+
startListening(): void;
|
|
48
|
+
/**
|
|
49
|
+
* Stop listening for voice input.
|
|
50
|
+
* Only needed when vadMode is 'manual'.
|
|
51
|
+
*/
|
|
52
|
+
stopListening(): void;
|
|
53
|
+
/**
|
|
54
|
+
* Interrupt the current assistant response.
|
|
55
|
+
*/
|
|
56
|
+
interrupt(): void;
|
|
57
|
+
/**
|
|
58
|
+
* Send a text message instead of voice.
|
|
59
|
+
*/
|
|
60
|
+
sendText(text: string): void;
|
|
61
|
+
/**
|
|
62
|
+
* Send an image to the conversation.
|
|
63
|
+
* @param imageData - Base64-encoded image data or a URL
|
|
64
|
+
* @param mimeType - MIME type of the image (e.g., 'image/png', 'image/jpeg')
|
|
65
|
+
*/
|
|
66
|
+
sendImage(imageData: string, mimeType: string): void;
|
|
67
|
+
/** Get current connection status */
|
|
68
|
+
get status(): RealtimeStatus;
|
|
69
|
+
/** Get current mode */
|
|
70
|
+
get mode(): RealtimeMode;
|
|
71
|
+
/** Get conversation messages */
|
|
72
|
+
get messages(): Array<RealtimeMessage>;
|
|
73
|
+
/** Get current error, if any */
|
|
74
|
+
get error(): Error | null;
|
|
75
|
+
/** Get pending user transcript (while user is speaking) */
|
|
76
|
+
get pendingUserTranscript(): string | null;
|
|
77
|
+
/** Get pending assistant transcript (while assistant is speaking) */
|
|
78
|
+
get pendingAssistantTranscript(): string | null;
|
|
79
|
+
/** Get audio visualization data */
|
|
80
|
+
get audio(): AudioVisualization | null;
|
|
81
|
+
/**
|
|
82
|
+
* Subscribe to state changes.
|
|
83
|
+
* @returns Unsubscribe function
|
|
84
|
+
*/
|
|
85
|
+
onStateChange(callback: RealtimeStateChangeCallback): () => void;
|
|
86
|
+
/**
|
|
87
|
+
* Clean up resources.
|
|
88
|
+
* Call this when disposing of the client.
|
|
89
|
+
*/
|
|
90
|
+
destroy(): void;
|
|
91
|
+
private updateState;
|
|
92
|
+
private addMessage;
|
|
93
|
+
private scheduleTokenRefresh;
|
|
94
|
+
private refreshToken;
|
|
95
|
+
private subscribeToConnectionEvents;
|
|
96
|
+
private applySessionConfig;
|
|
97
|
+
private generateId;
|
|
98
|
+
}
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
import { convertSchemaToJsonSchema } from "@tanstack/ai";
|
|
2
|
+
const TOKEN_REFRESH_BUFFER_MS = 6e4;
|
|
3
|
+
class RealtimeClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.connection = null;
|
|
6
|
+
this.token = null;
|
|
7
|
+
this.tokenRefreshTimeout = null;
|
|
8
|
+
this.stateChangeCallbacks = /* @__PURE__ */ new Set();
|
|
9
|
+
this.unsubscribers = [];
|
|
10
|
+
this.state = {
|
|
11
|
+
status: "idle",
|
|
12
|
+
mode: "idle",
|
|
13
|
+
messages: [],
|
|
14
|
+
pendingUserTranscript: null,
|
|
15
|
+
pendingAssistantTranscript: null,
|
|
16
|
+
error: null
|
|
17
|
+
};
|
|
18
|
+
this.options = {
|
|
19
|
+
autoPlayback: true,
|
|
20
|
+
autoCapture: true,
|
|
21
|
+
vadMode: "server",
|
|
22
|
+
...options
|
|
23
|
+
};
|
|
24
|
+
this.clientTools = /* @__PURE__ */ new Map();
|
|
25
|
+
if (options.tools) {
|
|
26
|
+
for (const tool of options.tools) {
|
|
27
|
+
this.clientTools.set(tool.name, tool);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
// ============================================================================
|
|
32
|
+
// Connection Lifecycle
|
|
33
|
+
// ============================================================================
|
|
34
|
+
/**
|
|
35
|
+
* Connect to the realtime session.
|
|
36
|
+
* Fetches a token and establishes the connection.
|
|
37
|
+
*/
|
|
38
|
+
async connect() {
|
|
39
|
+
if (this.state.status === "connected") {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.updateState({ status: "connecting", error: null });
|
|
43
|
+
try {
|
|
44
|
+
this.token = await this.options.getToken();
|
|
45
|
+
this.scheduleTokenRefresh();
|
|
46
|
+
const toolsList = this.clientTools.size > 0 ? Array.from(this.clientTools.values()) : void 0;
|
|
47
|
+
this.connection = await this.options.adapter.connect(
|
|
48
|
+
this.token,
|
|
49
|
+
toolsList
|
|
50
|
+
);
|
|
51
|
+
this.subscribeToConnectionEvents();
|
|
52
|
+
this.applySessionConfig();
|
|
53
|
+
if (this.options.autoCapture) {
|
|
54
|
+
await this.connection.startAudioCapture();
|
|
55
|
+
}
|
|
56
|
+
this.updateState({ status: "connected", mode: "listening" });
|
|
57
|
+
this.options.onConnect?.();
|
|
58
|
+
} catch (error) {
|
|
59
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
60
|
+
this.updateState({ status: "error", error: err });
|
|
61
|
+
this.options.onError?.(err);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Disconnect from the realtime session.
|
|
67
|
+
*/
|
|
68
|
+
async disconnect() {
|
|
69
|
+
if (this.tokenRefreshTimeout) {
|
|
70
|
+
clearTimeout(this.tokenRefreshTimeout);
|
|
71
|
+
this.tokenRefreshTimeout = null;
|
|
72
|
+
}
|
|
73
|
+
for (const unsub of this.unsubscribers) {
|
|
74
|
+
unsub();
|
|
75
|
+
}
|
|
76
|
+
this.unsubscribers = [];
|
|
77
|
+
if (this.connection) {
|
|
78
|
+
await this.connection.disconnect();
|
|
79
|
+
this.connection = null;
|
|
80
|
+
}
|
|
81
|
+
this.token = null;
|
|
82
|
+
this.updateState({
|
|
83
|
+
status: "idle",
|
|
84
|
+
mode: "idle",
|
|
85
|
+
pendingUserTranscript: null,
|
|
86
|
+
pendingAssistantTranscript: null
|
|
87
|
+
});
|
|
88
|
+
this.options.onDisconnect?.();
|
|
89
|
+
}
|
|
90
|
+
// ============================================================================
|
|
91
|
+
// Voice Control
|
|
92
|
+
// ============================================================================
|
|
93
|
+
/**
|
|
94
|
+
* Start listening for voice input.
|
|
95
|
+
* Only needed when vadMode is 'manual'.
|
|
96
|
+
*/
|
|
97
|
+
startListening() {
|
|
98
|
+
if (!this.connection || this.state.status !== "connected") {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
this.connection.startAudioCapture();
|
|
102
|
+
this.updateState({ mode: "listening" });
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Stop listening for voice input.
|
|
106
|
+
* Only needed when vadMode is 'manual'.
|
|
107
|
+
*/
|
|
108
|
+
stopListening() {
|
|
109
|
+
if (!this.connection) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
this.connection.stopAudioCapture();
|
|
113
|
+
this.updateState({ mode: "idle" });
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Interrupt the current assistant response.
|
|
117
|
+
*/
|
|
118
|
+
interrupt() {
|
|
119
|
+
if (!this.connection) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
this.connection.interrupt();
|
|
123
|
+
}
|
|
124
|
+
// ============================================================================
|
|
125
|
+
// Text Input
|
|
126
|
+
// ============================================================================
|
|
127
|
+
/**
|
|
128
|
+
* Send a text message instead of voice.
|
|
129
|
+
*/
|
|
130
|
+
sendText(text) {
|
|
131
|
+
if (!this.connection || this.state.status !== "connected") {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const userMessage = {
|
|
135
|
+
id: this.generateId(),
|
|
136
|
+
role: "user",
|
|
137
|
+
timestamp: Date.now(),
|
|
138
|
+
parts: [{ type: "text", content: text }]
|
|
139
|
+
};
|
|
140
|
+
this.addMessage(userMessage);
|
|
141
|
+
this.connection.sendText(text);
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Send an image to the conversation.
|
|
145
|
+
* @param imageData - Base64-encoded image data or a URL
|
|
146
|
+
* @param mimeType - MIME type of the image (e.g., 'image/png', 'image/jpeg')
|
|
147
|
+
*/
|
|
148
|
+
sendImage(imageData, mimeType) {
|
|
149
|
+
if (!this.connection || this.state.status !== "connected") {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const userMessage = {
|
|
153
|
+
id: this.generateId(),
|
|
154
|
+
role: "user",
|
|
155
|
+
timestamp: Date.now(),
|
|
156
|
+
parts: [{ type: "image", data: imageData, mimeType }]
|
|
157
|
+
};
|
|
158
|
+
this.addMessage(userMessage);
|
|
159
|
+
this.connection.sendImage(imageData, mimeType);
|
|
160
|
+
}
|
|
161
|
+
// ============================================================================
|
|
162
|
+
// State Access
|
|
163
|
+
// ============================================================================
|
|
164
|
+
/** Get current connection status */
|
|
165
|
+
get status() {
|
|
166
|
+
return this.state.status;
|
|
167
|
+
}
|
|
168
|
+
/** Get current mode */
|
|
169
|
+
get mode() {
|
|
170
|
+
return this.state.mode;
|
|
171
|
+
}
|
|
172
|
+
/** Get conversation messages */
|
|
173
|
+
get messages() {
|
|
174
|
+
return this.state.messages;
|
|
175
|
+
}
|
|
176
|
+
/** Get current error, if any */
|
|
177
|
+
get error() {
|
|
178
|
+
return this.state.error;
|
|
179
|
+
}
|
|
180
|
+
/** Get pending user transcript (while user is speaking) */
|
|
181
|
+
get pendingUserTranscript() {
|
|
182
|
+
return this.state.pendingUserTranscript;
|
|
183
|
+
}
|
|
184
|
+
/** Get pending assistant transcript (while assistant is speaking) */
|
|
185
|
+
get pendingAssistantTranscript() {
|
|
186
|
+
return this.state.pendingAssistantTranscript;
|
|
187
|
+
}
|
|
188
|
+
/** Get audio visualization data */
|
|
189
|
+
get audio() {
|
|
190
|
+
return this.connection?.getAudioVisualization() ?? null;
|
|
191
|
+
}
|
|
192
|
+
// ============================================================================
|
|
193
|
+
// State Subscription
|
|
194
|
+
// ============================================================================
|
|
195
|
+
/**
|
|
196
|
+
* Subscribe to state changes.
|
|
197
|
+
* @returns Unsubscribe function
|
|
198
|
+
*/
|
|
199
|
+
onStateChange(callback) {
|
|
200
|
+
this.stateChangeCallbacks.add(callback);
|
|
201
|
+
return () => {
|
|
202
|
+
this.stateChangeCallbacks.delete(callback);
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
// ============================================================================
|
|
206
|
+
// Cleanup
|
|
207
|
+
// ============================================================================
|
|
208
|
+
/**
|
|
209
|
+
* Clean up resources.
|
|
210
|
+
* Call this when disposing of the client.
|
|
211
|
+
*/
|
|
212
|
+
destroy() {
|
|
213
|
+
this.disconnect();
|
|
214
|
+
this.stateChangeCallbacks.clear();
|
|
215
|
+
}
|
|
216
|
+
// ============================================================================
|
|
217
|
+
// Private Methods
|
|
218
|
+
// ============================================================================
|
|
219
|
+
updateState(updates) {
|
|
220
|
+
this.state = { ...this.state, ...updates };
|
|
221
|
+
for (const callback of this.stateChangeCallbacks) {
|
|
222
|
+
callback(this.state);
|
|
223
|
+
}
|
|
224
|
+
if ("status" in updates && updates.status !== void 0) {
|
|
225
|
+
this.options.onStatusChange?.(updates.status);
|
|
226
|
+
}
|
|
227
|
+
if ("mode" in updates && updates.mode !== void 0) {
|
|
228
|
+
this.options.onModeChange?.(updates.mode);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
addMessage(message) {
|
|
232
|
+
this.updateState({
|
|
233
|
+
messages: [...this.state.messages, message]
|
|
234
|
+
});
|
|
235
|
+
this.options.onMessage?.(message);
|
|
236
|
+
}
|
|
237
|
+
scheduleTokenRefresh() {
|
|
238
|
+
if (!this.token) return;
|
|
239
|
+
const timeUntilExpiry = this.token.expiresAt - Date.now();
|
|
240
|
+
const refreshIn = Math.max(0, timeUntilExpiry - TOKEN_REFRESH_BUFFER_MS);
|
|
241
|
+
this.tokenRefreshTimeout = setTimeout(() => {
|
|
242
|
+
this.refreshToken();
|
|
243
|
+
}, refreshIn);
|
|
244
|
+
}
|
|
245
|
+
async refreshToken() {
|
|
246
|
+
try {
|
|
247
|
+
this.token = await this.options.getToken();
|
|
248
|
+
this.scheduleTokenRefresh();
|
|
249
|
+
} catch (error) {
|
|
250
|
+
const err = error instanceof Error ? error : new Error(String(error));
|
|
251
|
+
this.updateState({ error: err });
|
|
252
|
+
this.options.onError?.(err);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
subscribeToConnectionEvents() {
|
|
256
|
+
if (!this.connection) return;
|
|
257
|
+
this.unsubscribers.push(
|
|
258
|
+
this.connection.on("status_change", ({ status }) => {
|
|
259
|
+
this.updateState({ status });
|
|
260
|
+
})
|
|
261
|
+
);
|
|
262
|
+
this.unsubscribers.push(
|
|
263
|
+
this.connection.on("mode_change", ({ mode }) => {
|
|
264
|
+
this.updateState({ mode });
|
|
265
|
+
})
|
|
266
|
+
);
|
|
267
|
+
this.unsubscribers.push(
|
|
268
|
+
this.connection.on("transcript", ({ role, transcript, isFinal }) => {
|
|
269
|
+
if (role === "user") {
|
|
270
|
+
this.updateState({
|
|
271
|
+
pendingUserTranscript: isFinal ? null : transcript
|
|
272
|
+
});
|
|
273
|
+
if (isFinal && transcript) {
|
|
274
|
+
this.addMessage({
|
|
275
|
+
id: this.generateId(),
|
|
276
|
+
role: "user",
|
|
277
|
+
timestamp: Date.now(),
|
|
278
|
+
parts: [{ type: "audio", transcript, durationMs: 0 }]
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
} else {
|
|
282
|
+
this.updateState({
|
|
283
|
+
pendingAssistantTranscript: isFinal ? null : transcript
|
|
284
|
+
});
|
|
285
|
+
}
|
|
286
|
+
})
|
|
287
|
+
);
|
|
288
|
+
this.unsubscribers.push(
|
|
289
|
+
this.connection.on(
|
|
290
|
+
"tool_call",
|
|
291
|
+
async ({ toolCallId, toolName, input }) => {
|
|
292
|
+
const tool = this.clientTools.get(toolName);
|
|
293
|
+
if (tool?.execute) {
|
|
294
|
+
try {
|
|
295
|
+
const output = await tool.execute(input);
|
|
296
|
+
this.connection?.sendToolResult(
|
|
297
|
+
toolCallId,
|
|
298
|
+
typeof output === "string" ? output : JSON.stringify(output)
|
|
299
|
+
);
|
|
300
|
+
} catch (error) {
|
|
301
|
+
const errMsg = error instanceof Error ? error.message : String(error);
|
|
302
|
+
this.connection?.sendToolResult(
|
|
303
|
+
toolCallId,
|
|
304
|
+
JSON.stringify({ error: errMsg })
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
this.unsubscribers.push(
|
|
312
|
+
this.connection.on("message_complete", ({ message }) => {
|
|
313
|
+
const existingIndex = this.state.messages.findIndex(
|
|
314
|
+
(m) => m.id === message.id
|
|
315
|
+
);
|
|
316
|
+
if (existingIndex >= 0) {
|
|
317
|
+
const newMessages = [...this.state.messages];
|
|
318
|
+
newMessages[existingIndex] = message;
|
|
319
|
+
this.updateState({ messages: newMessages });
|
|
320
|
+
} else {
|
|
321
|
+
this.addMessage(message);
|
|
322
|
+
}
|
|
323
|
+
})
|
|
324
|
+
);
|
|
325
|
+
this.unsubscribers.push(
|
|
326
|
+
this.connection.on("interrupted", ({ messageId }) => {
|
|
327
|
+
if (messageId) {
|
|
328
|
+
const newMessages = this.state.messages.map(
|
|
329
|
+
(m) => m.id === messageId ? { ...m, interrupted: true } : m
|
|
330
|
+
);
|
|
331
|
+
this.updateState({ messages: newMessages });
|
|
332
|
+
}
|
|
333
|
+
this.updateState({
|
|
334
|
+
mode: "listening",
|
|
335
|
+
pendingAssistantTranscript: null
|
|
336
|
+
});
|
|
337
|
+
this.options.onInterrupted?.();
|
|
338
|
+
})
|
|
339
|
+
);
|
|
340
|
+
this.unsubscribers.push(
|
|
341
|
+
this.connection.on("error", ({ error }) => {
|
|
342
|
+
this.updateState({ error });
|
|
343
|
+
this.options.onError?.(error);
|
|
344
|
+
})
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
applySessionConfig() {
|
|
348
|
+
if (!this.connection) return;
|
|
349
|
+
const {
|
|
350
|
+
instructions,
|
|
351
|
+
voice,
|
|
352
|
+
vadMode,
|
|
353
|
+
tools,
|
|
354
|
+
outputModalities,
|
|
355
|
+
temperature,
|
|
356
|
+
maxOutputTokens,
|
|
357
|
+
semanticEagerness
|
|
358
|
+
} = this.options;
|
|
359
|
+
const hasConfig = instructions || voice || vadMode || tools && tools.length > 0 || outputModalities || temperature !== void 0 || maxOutputTokens !== void 0 || semanticEagerness;
|
|
360
|
+
if (!hasConfig) return;
|
|
361
|
+
const toolsConfig = tools ? Array.from(this.clientTools.values()).map((t) => ({
|
|
362
|
+
name: t.name,
|
|
363
|
+
description: t.description,
|
|
364
|
+
inputSchema: t.inputSchema ? convertSchemaToJsonSchema(t.inputSchema) : void 0
|
|
365
|
+
})) : void 0;
|
|
366
|
+
this.connection.updateSession({
|
|
367
|
+
instructions,
|
|
368
|
+
voice,
|
|
369
|
+
vadMode,
|
|
370
|
+
tools: toolsConfig,
|
|
371
|
+
outputModalities,
|
|
372
|
+
temperature,
|
|
373
|
+
maxOutputTokens,
|
|
374
|
+
semanticEagerness
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
generateId() {
|
|
378
|
+
return `msg-${Date.now()}-${Math.random().toString(36).substring(7)}`;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
export {
|
|
382
|
+
RealtimeClient
|
|
383
|
+
};
|
|
384
|
+
//# sourceMappingURL=realtime-client.js.map
|