@tanstack/ai-client 0.5.2 → 0.6.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.
@@ -0,0 +1,270 @@
1
+ import type { StreamChunk } from '@tanstack/ai'
2
+ import type { ConnectionAdapter } from './connection-adapters'
3
+
4
+ // ===========================
5
+ // Inference Utilities
6
+ // ===========================
7
+
8
+ /**
9
+ * Infers the output type from an `onResult` callback's return type.
10
+ *
11
+ * - If the callback returns a concrete type (excluding null/void/undefined), uses that type.
12
+ * - If the callback only returns null/void/undefined, or is not provided, falls back to TResult.
13
+ *
14
+ * @template TResult - The raw result type from the generation
15
+ * @template TFn - The onResult callback type (or undefined if not provided)
16
+ */
17
+ export type InferGenerationOutput<TResult, TFn> = TFn extends (
18
+ result: any,
19
+ ) => infer R
20
+ ? [Exclude<R, null | void | undefined>] extends [never]
21
+ ? TResult
22
+ : Exclude<R, null | void | undefined>
23
+ : TResult
24
+
25
+ // ===========================
26
+ // State
27
+ // ===========================
28
+
29
+ /**
30
+ * State machine for generation clients.
31
+ * Simpler than ChatClientState since generation is a single request/response cycle.
32
+ */
33
+ export type GenerationClientState = 'idle' | 'generating' | 'success' | 'error'
34
+
35
+ // ===========================
36
+ // Event Constants
37
+ // ===========================
38
+
39
+ /**
40
+ * Well-known CUSTOM event names used by generation clients.
41
+ * These events are emitted by the server-side streaming helpers
42
+ * and consumed by the client-side GenerationClient.
43
+ */
44
+ export const GENERATION_EVENTS = {
45
+ /** The generation result payload */
46
+ RESULT: 'generation:result',
47
+ /** Progress update (0-100) with optional message */
48
+ PROGRESS: 'generation:progress',
49
+ /** Video job created with jobId */
50
+ VIDEO_JOB_CREATED: 'video:job:created',
51
+ /** Video job status update */
52
+ VIDEO_STATUS: 'video:status',
53
+ } as const
54
+
55
+ // ===========================
56
+ // Transport Types
57
+ // ===========================
58
+
59
+ /**
60
+ * Options passed to a fetcher function by the generation client.
61
+ */
62
+ export interface GenerationFetcherOptions {
63
+ /** AbortSignal that is triggered when the user calls `stop()` */
64
+ signal: AbortSignal
65
+ }
66
+
67
+ /**
68
+ * A direct async function that performs a generation request.
69
+ *
70
+ * Can return the result directly, or return a `Response` with an SSE body
71
+ * (e.g., from a TanStack Start server function using `toServerSentEventsResponse()`).
72
+ * When a `Response` is returned, the client will parse it as an SSE stream.
73
+ *
74
+ * @template TInput - The input type for the generation request
75
+ * @template TResult - The result type returned by the generation
76
+ */
77
+ export type GenerationFetcher<TInput, TResult> = (
78
+ input: TInput,
79
+ options?: GenerationFetcherOptions,
80
+ ) => Promise<TResult | Response>
81
+
82
+ /**
83
+ * Transport configuration for generation clients.
84
+ * Supports either a ConnectionAdapter (streaming) or a direct fetcher function.
85
+ */
86
+ export type GenerationTransport<TInput, TResult> =
87
+ | { connection: ConnectionAdapter; fetcher?: never }
88
+ | { fetcher: GenerationFetcher<TInput, TResult>; connection?: never }
89
+
90
+ // ===========================
91
+ // Client Options
92
+ // ===========================
93
+
94
+ /**
95
+ * Options for the GenerationClient.
96
+ *
97
+ * @template TInput - The input type for the generation request (used by consuming code)
98
+ * @template TResult - The result type returned by the generation
99
+ * @template TOutput - The output type after optional transform (defaults to TResult)
100
+ */
101
+ // eslint-disable-next-line @typescript-eslint/naming-convention
102
+ export interface GenerationClientOptions<_TInput, TResult, TOutput = TResult> {
103
+ /** Unique identifier for this generation client instance */
104
+ id?: string
105
+
106
+ /** Additional body parameters to send with ConnectionAdapter requests */
107
+ body?: Record<string, any>
108
+
109
+ /**
110
+ * Callback when a result is received. Can optionally return a transformed value
111
+ * that replaces the stored result.
112
+ *
113
+ * - Return a non-null value to transform and store it as the result
114
+ * - Return `null` to keep the previous result unchanged
115
+ * - Return nothing (`void`) to store the raw result as-is
116
+ */
117
+ onResult?: (result: TResult) => TOutput | null | void
118
+ /** Callback when an error occurs */
119
+ onError?: (error: Error) => void
120
+ /** Callback when progress is reported (0-100) */
121
+ onProgress?: (progress: number, message?: string) => void
122
+ /** Callback for each stream chunk (ConnectionAdapter mode only) */
123
+ onChunk?: (chunk: StreamChunk) => void
124
+
125
+ // Framework state callbacks (set by hooks, not users)
126
+ /** @internal Called when result changes */
127
+ onResultChange?: (result: TOutput | null) => void
128
+ /** @internal Called when loading state changes */
129
+ onLoadingChange?: (isLoading: boolean) => void
130
+ /** @internal Called when error state changes */
131
+ onErrorChange?: (error: Error | undefined) => void
132
+ /** @internal Called when generation status changes */
133
+ onStatusChange?: (status: GenerationClientState) => void
134
+ }
135
+
136
+ // ===========================
137
+ // Video-Specific Options
138
+ // ===========================
139
+
140
+ /**
141
+ * Video status information returned during job polling.
142
+ */
143
+ export interface VideoStatusInfo {
144
+ /** Job identifier */
145
+ jobId: string
146
+ /** Current status of the video generation job */
147
+ status: 'pending' | 'processing' | 'completed' | 'failed'
148
+ /** Progress percentage (0-100), if available */
149
+ progress?: number
150
+ /** URL to the generated video (when completed) */
151
+ url?: string
152
+ /** Error message if status is 'failed' */
153
+ error?: string
154
+ }
155
+
156
+ /**
157
+ * Composite result for video generation (job completion).
158
+ */
159
+ export interface VideoGenerateResult {
160
+ /** Job identifier */
161
+ jobId: string
162
+ /** Final status */
163
+ status: 'completed'
164
+ /** URL to the generated video */
165
+ url: string
166
+ /** When the URL expires, if applicable */
167
+ expiresAt?: Date
168
+ }
169
+
170
+ /**
171
+ * Options for the VideoGenerationClient.
172
+ */
173
+ export interface VideoGenerationClientOptions<
174
+ TOutput = VideoGenerateResult,
175
+ > extends GenerationClientOptions<
176
+ VideoGenerateInput,
177
+ VideoGenerateResult,
178
+ TOutput
179
+ > {
180
+ /** Callback when a video job is created */
181
+ onJobCreated?: (jobId: string) => void
182
+ /** Callback on each status update */
183
+ onStatusUpdate?: (status: VideoStatusInfo) => void
184
+
185
+ // Framework state callbacks
186
+ /** @internal Called when jobId changes */
187
+ onJobIdChange?: (jobId: string | null) => void
188
+ /** @internal Called when video status changes */
189
+ onVideoStatusChange?: (status: VideoStatusInfo | null) => void
190
+ }
191
+
192
+ // ===========================
193
+ // Input Types
194
+ // ===========================
195
+
196
+ /**
197
+ * Input for image generation.
198
+ */
199
+ export interface ImageGenerateInput {
200
+ /** Text description of the desired image(s) */
201
+ prompt: string
202
+ /** Number of images to generate (default: 1) */
203
+ numberOfImages?: number
204
+ /** Image size in WIDTHxHEIGHT format (e.g., "1024x1024") */
205
+ size?: string
206
+ /** Model-specific options */
207
+ modelOptions?: Record<string, any>
208
+ }
209
+
210
+ /**
211
+ * Input for text-to-speech generation.
212
+ */
213
+ export interface SpeechGenerateInput {
214
+ /** The text to convert to speech */
215
+ text: string
216
+ /** The voice to use for generation */
217
+ voice?: string
218
+ /** The output audio format */
219
+ format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'
220
+ /** The speed of the generated audio (0.25 to 4.0) */
221
+ speed?: number
222
+ /** Model-specific options */
223
+ modelOptions?: Record<string, any>
224
+ }
225
+
226
+ /**
227
+ * Input for audio transcription.
228
+ */
229
+ export interface TranscriptionGenerateInput {
230
+ /** The audio data to transcribe - can be base64 string, File, or Blob */
231
+ audio: string | File | Blob
232
+ /** The language of the audio in ISO-639-1 format (e.g., 'en') */
233
+ language?: string
234
+ /** An optional prompt to guide the transcription */
235
+ prompt?: string
236
+ /** The format of the transcription output */
237
+ responseFormat?: 'json' | 'text' | 'srt' | 'verbose_json' | 'vtt'
238
+ /** Model-specific options */
239
+ modelOptions?: Record<string, any>
240
+ }
241
+
242
+ /**
243
+ * Input for text summarization.
244
+ */
245
+ export interface SummarizeGenerateInput {
246
+ /** The text to summarize */
247
+ text: string
248
+ /** Maximum length of the summary */
249
+ maxLength?: number
250
+ /** Style of the summary */
251
+ style?: 'bullet-points' | 'paragraph' | 'concise'
252
+ /** Topics to focus on */
253
+ focus?: Array<string>
254
+ /** Model-specific options */
255
+ modelOptions?: Record<string, any>
256
+ }
257
+
258
+ /**
259
+ * Input for video generation.
260
+ */
261
+ export interface VideoGenerateInput {
262
+ /** Text description of the desired video */
263
+ prompt: string
264
+ /** Video size — format depends on provider (e.g., "16:9", "1280x720") */
265
+ size?: string
266
+ /** Video duration in seconds */
267
+ duration?: number
268
+ /** Model-specific options */
269
+ modelOptions?: Record<string, any>
270
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export { ChatClient } from './chat-client'
2
+ export { GenerationClient } from './generation-client'
3
+ export { VideoGenerationClient } from './video-generation-client'
2
4
  export type {
3
5
  // Core message types (re-exported from @tanstack/ai via types.ts)
4
6
  UIMessage,
@@ -15,6 +17,24 @@ export type {
15
17
  // Multimodal content input type
16
18
  MultimodalContent,
17
19
  } from './types'
20
+ // Generation client types
21
+ export type {
22
+ InferGenerationOutput,
23
+ GenerationClientState,
24
+ GenerationClientOptions,
25
+ GenerationFetcher,
26
+ GenerationFetcherOptions,
27
+ GenerationTransport,
28
+ VideoGenerationClientOptions,
29
+ VideoStatusInfo,
30
+ VideoGenerateResult,
31
+ ImageGenerateInput,
32
+ SpeechGenerateInput,
33
+ TranscriptionGenerateInput,
34
+ SummarizeGenerateInput,
35
+ VideoGenerateInput,
36
+ } from './generation-types'
37
+ export { GENERATION_EVENTS } from './generation-types'
18
38
  export { clientTools, createChatClientOptions } from './types'
19
39
  export type {
20
40
  ExtractToolNames,
@@ -0,0 +1,76 @@
1
+ import type { StreamChunk } from '@tanstack/ai'
2
+
3
+ /**
4
+ * Read lines from a stream (newline-delimited)
5
+ */
6
+ async function* readStreamLines(
7
+ reader: ReadableStreamDefaultReader<Uint8Array>,
8
+ abortSignal?: AbortSignal,
9
+ ): AsyncGenerator<string> {
10
+ try {
11
+ const decoder = new TextDecoder()
12
+ let buffer = ''
13
+
14
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
15
+ while (true) {
16
+ if (abortSignal?.aborted) {
17
+ break
18
+ }
19
+
20
+ const { done, value } = await reader.read()
21
+ if (done) break
22
+
23
+ buffer += decoder.decode(value, { stream: true })
24
+ const lines = buffer.split('\n')
25
+
26
+ buffer = lines.pop() || ''
27
+
28
+ for (const line of lines) {
29
+ if (line.trim()) {
30
+ yield line
31
+ }
32
+ }
33
+ }
34
+
35
+ if (buffer.trim()) {
36
+ yield buffer
37
+ }
38
+ } finally {
39
+ reader.releaseLock()
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Parse a Response body as Server-Sent Events, yielding StreamChunks.
45
+ *
46
+ * Used by GenerationClient to parse SSE Responses returned from fetchers
47
+ * (e.g., TanStack Start server functions using `toServerSentEventsResponse()`).
48
+ */
49
+ export async function* parseSSEResponse(
50
+ response: Response,
51
+ abortSignal?: AbortSignal,
52
+ ): AsyncGenerator<StreamChunk> {
53
+ if (!response.ok) {
54
+ throw new Error(
55
+ `HTTP error! status: ${response.status} ${response.statusText}`,
56
+ )
57
+ }
58
+
59
+ const reader = response.body?.getReader()
60
+ if (!reader) {
61
+ throw new Error('Response body is not readable')
62
+ }
63
+
64
+ for await (const line of readStreamLines(reader, abortSignal)) {
65
+ const data = line.startsWith('data: ') ? line.slice(6) : line
66
+
67
+ if (data === '[DONE]') continue
68
+
69
+ try {
70
+ const parsed: StreamChunk = JSON.parse(data)
71
+ yield parsed
72
+ } catch (parseError) {
73
+ console.warn('Failed to parse SSE chunk:', data)
74
+ }
75
+ }
76
+ }