@tanstack/ai-client 0.5.3 → 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.
- package/dist/esm/generation-client.d.ts +85 -0
- package/dist/esm/generation-client.js +190 -0
- package/dist/esm/generation-client.js.map +1 -0
- package/dist/esm/generation-types.d.ts +209 -0
- package/dist/esm/generation-types.js +14 -0
- package/dist/esm/generation-types.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/sse-parser.d.ts +8 -0
- package/dist/esm/sse-parser.js +51 -0
- package/dist/esm/sse-parser.js.map +1 -0
- package/dist/esm/video-generation-client.d.ts +95 -0
- package/dist/esm/video-generation-client.js +238 -0
- package/dist/esm/video-generation-client.js.map +1 -0
- package/package.json +2 -2
- package/src/generation-client.ts +302 -0
- package/src/generation-types.ts +270 -0
- package/src/index.ts +20 -0
- package/src/sse-parser.ts +76 -0
- package/src/video-generation-client.ts +371 -0
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { ConnectionAdapter } from './connection-adapters.js';
|
|
2
|
+
import { GenerationClientState, GenerationFetcher, VideoGenerateInput, VideoGenerateResult, VideoGenerationClientOptions, VideoStatusInfo } from './generation-types.js';
|
|
3
|
+
/**
|
|
4
|
+
* A specialized client for job-based video generation.
|
|
5
|
+
*
|
|
6
|
+
* Video generation is asynchronous: a job is created, then polled for status
|
|
7
|
+
* until completion. This client handles the full lifecycle.
|
|
8
|
+
*
|
|
9
|
+
* Supports two transport modes:
|
|
10
|
+
* - **ConnectionAdapter** — Server handles the polling loop internally and
|
|
11
|
+
* streams status updates via CUSTOM events.
|
|
12
|
+
* - **Fetcher** — Direct async function that returns a completed
|
|
13
|
+
* `VideoGenerateResult`.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* // With ConnectionAdapter (server-driven polling)
|
|
18
|
+
* const client = new VideoGenerationClient({
|
|
19
|
+
* connection: fetchServerSentEvents('/api/generate/video'),
|
|
20
|
+
* onResultChange: setResult,
|
|
21
|
+
* onVideoStatusChange: setVideoStatus,
|
|
22
|
+
* })
|
|
23
|
+
*
|
|
24
|
+
* // With fetcher (direct result)
|
|
25
|
+
* const client = new VideoGenerationClient({
|
|
26
|
+
* fetcher: async (input) => {
|
|
27
|
+
* const res = await fetch('/api/video/generate', {
|
|
28
|
+
* method: 'POST',
|
|
29
|
+
* body: JSON.stringify(input),
|
|
30
|
+
* })
|
|
31
|
+
* return res.json() // { jobId, status: 'completed', url, expiresAt }
|
|
32
|
+
* },
|
|
33
|
+
* })
|
|
34
|
+
*
|
|
35
|
+
* await client.generate({ prompt: 'A flying car over a city' })
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare class VideoGenerationClient<TOutput = VideoGenerateResult> {
|
|
39
|
+
private connection;
|
|
40
|
+
private fetcher;
|
|
41
|
+
private body;
|
|
42
|
+
private result;
|
|
43
|
+
private jobId;
|
|
44
|
+
private videoStatus;
|
|
45
|
+
private isLoading;
|
|
46
|
+
private error;
|
|
47
|
+
private status;
|
|
48
|
+
private abortController;
|
|
49
|
+
private callbacksRef;
|
|
50
|
+
constructor(options: VideoGenerationClientOptions<TOutput> & ({
|
|
51
|
+
connection: ConnectionAdapter;
|
|
52
|
+
fetcher?: never;
|
|
53
|
+
} | {
|
|
54
|
+
fetcher: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>;
|
|
55
|
+
connection?: never;
|
|
56
|
+
}));
|
|
57
|
+
/**
|
|
58
|
+
* Trigger video generation.
|
|
59
|
+
* Only one generation can be in-flight at a time.
|
|
60
|
+
*/
|
|
61
|
+
generate(input: VideoGenerateInput): Promise<void>;
|
|
62
|
+
/**
|
|
63
|
+
* Direct fetcher mode: call fetcher and set result.
|
|
64
|
+
*/
|
|
65
|
+
private generateWithFetcher;
|
|
66
|
+
/**
|
|
67
|
+
* Process a stream of AG-UI events from the ConnectionAdapter.
|
|
68
|
+
* The server handles the polling loop and streams status updates.
|
|
69
|
+
*/
|
|
70
|
+
private processStream;
|
|
71
|
+
/**
|
|
72
|
+
* Abort any in-flight generation or polling.
|
|
73
|
+
*/
|
|
74
|
+
stop(): void;
|
|
75
|
+
/**
|
|
76
|
+
* Clear all state and return to idle.
|
|
77
|
+
*/
|
|
78
|
+
reset(): void;
|
|
79
|
+
/**
|
|
80
|
+
* Update options without recreating the client.
|
|
81
|
+
*/
|
|
82
|
+
updateOptions(options: Partial<Pick<VideoGenerationClientOptions<TOutput>, 'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk' | 'onJobCreated' | 'onStatusUpdate'>>): void;
|
|
83
|
+
getResult(): TOutput | null;
|
|
84
|
+
getJobId(): string | null;
|
|
85
|
+
getVideoStatus(): VideoStatusInfo | null;
|
|
86
|
+
getIsLoading(): boolean;
|
|
87
|
+
getError(): Error | undefined;
|
|
88
|
+
getStatus(): GenerationClientState;
|
|
89
|
+
private setResult;
|
|
90
|
+
private setJobId;
|
|
91
|
+
private setVideoStatus;
|
|
92
|
+
private setIsLoading;
|
|
93
|
+
private setError;
|
|
94
|
+
private setStatus;
|
|
95
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { GENERATION_EVENTS } from "./generation-types.js";
|
|
2
|
+
import { parseSSEResponse } from "./sse-parser.js";
|
|
3
|
+
class VideoGenerationClient {
|
|
4
|
+
constructor(options) {
|
|
5
|
+
this.result = null;
|
|
6
|
+
this.jobId = null;
|
|
7
|
+
this.videoStatus = null;
|
|
8
|
+
this.isLoading = false;
|
|
9
|
+
this.error = void 0;
|
|
10
|
+
this.status = "idle";
|
|
11
|
+
this.abortController = null;
|
|
12
|
+
this.connection = options.connection;
|
|
13
|
+
this.fetcher = options.fetcher;
|
|
14
|
+
this.body = options.body ?? {};
|
|
15
|
+
this.callbacksRef = {
|
|
16
|
+
onResult: options.onResult,
|
|
17
|
+
onError: options.onError,
|
|
18
|
+
onProgress: options.onProgress,
|
|
19
|
+
onChunk: options.onChunk,
|
|
20
|
+
onJobCreated: options.onJobCreated,
|
|
21
|
+
onStatusUpdate: options.onStatusUpdate,
|
|
22
|
+
onResultChange: options.onResultChange,
|
|
23
|
+
onLoadingChange: options.onLoadingChange,
|
|
24
|
+
onErrorChange: options.onErrorChange,
|
|
25
|
+
onStatusChange: options.onStatusChange,
|
|
26
|
+
onJobIdChange: options.onJobIdChange,
|
|
27
|
+
onVideoStatusChange: options.onVideoStatusChange
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Trigger video generation.
|
|
32
|
+
* Only one generation can be in-flight at a time.
|
|
33
|
+
*/
|
|
34
|
+
async generate(input) {
|
|
35
|
+
if (this.isLoading) return;
|
|
36
|
+
this.setIsLoading(true);
|
|
37
|
+
this.setStatus("generating");
|
|
38
|
+
this.setError(void 0);
|
|
39
|
+
this.setJobId(null);
|
|
40
|
+
this.setVideoStatus(null);
|
|
41
|
+
const abortController = new AbortController();
|
|
42
|
+
this.abortController = abortController;
|
|
43
|
+
const { signal } = abortController;
|
|
44
|
+
try {
|
|
45
|
+
if (this.fetcher) {
|
|
46
|
+
await this.generateWithFetcher(input, signal);
|
|
47
|
+
} else if (this.connection) {
|
|
48
|
+
const mergedData = { ...this.body, ...input };
|
|
49
|
+
const stream = this.connection.connect([], mergedData, signal);
|
|
50
|
+
await this.processStream(stream);
|
|
51
|
+
} else {
|
|
52
|
+
throw new Error(
|
|
53
|
+
"VideoGenerationClient requires either a connection or fetcher option"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (signal.aborted) return;
|
|
58
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
59
|
+
this.setError(error);
|
|
60
|
+
this.setStatus("error");
|
|
61
|
+
this.callbacksRef.onError?.(error);
|
|
62
|
+
} finally {
|
|
63
|
+
this.abortController = null;
|
|
64
|
+
this.setIsLoading(false);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Direct fetcher mode: call fetcher and set result.
|
|
69
|
+
*/
|
|
70
|
+
async generateWithFetcher(input, signal) {
|
|
71
|
+
if (!this.fetcher) return;
|
|
72
|
+
const result = await this.fetcher(input, { signal });
|
|
73
|
+
if (signal.aborted) return;
|
|
74
|
+
if (result instanceof Response) {
|
|
75
|
+
await this.processStream(parseSSEResponse(result, signal));
|
|
76
|
+
} else {
|
|
77
|
+
this.setResult(result);
|
|
78
|
+
this.setStatus("success");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Process a stream of AG-UI events from the ConnectionAdapter.
|
|
83
|
+
* The server handles the polling loop and streams status updates.
|
|
84
|
+
*/
|
|
85
|
+
async processStream(source) {
|
|
86
|
+
for await (const chunk of source) {
|
|
87
|
+
if (this.abortController?.signal.aborted) break;
|
|
88
|
+
this.callbacksRef.onChunk?.(chunk);
|
|
89
|
+
switch (chunk.type) {
|
|
90
|
+
case "CUSTOM": {
|
|
91
|
+
if (chunk.name === GENERATION_EVENTS.VIDEO_JOB_CREATED) {
|
|
92
|
+
const { jobId } = chunk.value;
|
|
93
|
+
this.setJobId(jobId);
|
|
94
|
+
this.callbacksRef.onJobCreated?.(jobId);
|
|
95
|
+
} else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {
|
|
96
|
+
const statusInfo = chunk.value;
|
|
97
|
+
this.setVideoStatus(statusInfo);
|
|
98
|
+
this.callbacksRef.onStatusUpdate?.(statusInfo);
|
|
99
|
+
if (statusInfo.progress !== void 0) {
|
|
100
|
+
this.callbacksRef.onProgress?.(statusInfo.progress);
|
|
101
|
+
}
|
|
102
|
+
} else if (chunk.name === GENERATION_EVENTS.RESULT) {
|
|
103
|
+
this.setResult(chunk.value);
|
|
104
|
+
} else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
|
|
105
|
+
const { progress, message } = chunk.value;
|
|
106
|
+
this.callbacksRef.onProgress?.(progress, message);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
case "RUN_FINISHED": {
|
|
111
|
+
this.setStatus("success");
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "RUN_ERROR": {
|
|
115
|
+
throw new Error(chunk.error.message);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Abort any in-flight generation or polling.
|
|
122
|
+
*/
|
|
123
|
+
stop() {
|
|
124
|
+
if (this.abortController) {
|
|
125
|
+
this.abortController.abort();
|
|
126
|
+
this.abortController = null;
|
|
127
|
+
}
|
|
128
|
+
this.setIsLoading(false);
|
|
129
|
+
if (this.status === "generating") {
|
|
130
|
+
this.setStatus("idle");
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Clear all state and return to idle.
|
|
135
|
+
*/
|
|
136
|
+
reset() {
|
|
137
|
+
this.stop();
|
|
138
|
+
this.setResult(null);
|
|
139
|
+
this.setJobId(null);
|
|
140
|
+
this.setVideoStatus(null);
|
|
141
|
+
this.setError(void 0);
|
|
142
|
+
this.setStatus("idle");
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Update options without recreating the client.
|
|
146
|
+
*/
|
|
147
|
+
updateOptions(options) {
|
|
148
|
+
if (options.body !== void 0) {
|
|
149
|
+
this.body = options.body ?? {};
|
|
150
|
+
}
|
|
151
|
+
if (options.onResult !== void 0) {
|
|
152
|
+
this.callbacksRef.onResult = options.onResult;
|
|
153
|
+
}
|
|
154
|
+
if (options.onError !== void 0) {
|
|
155
|
+
this.callbacksRef.onError = options.onError;
|
|
156
|
+
}
|
|
157
|
+
if (options.onProgress !== void 0) {
|
|
158
|
+
this.callbacksRef.onProgress = options.onProgress;
|
|
159
|
+
}
|
|
160
|
+
if (options.onChunk !== void 0) {
|
|
161
|
+
this.callbacksRef.onChunk = options.onChunk;
|
|
162
|
+
}
|
|
163
|
+
if (options.onJobCreated !== void 0) {
|
|
164
|
+
this.callbacksRef.onJobCreated = options.onJobCreated;
|
|
165
|
+
}
|
|
166
|
+
if (options.onStatusUpdate !== void 0) {
|
|
167
|
+
this.callbacksRef.onStatusUpdate = options.onStatusUpdate;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
// ===========================
|
|
171
|
+
// Getters
|
|
172
|
+
// ===========================
|
|
173
|
+
getResult() {
|
|
174
|
+
return this.result;
|
|
175
|
+
}
|
|
176
|
+
getJobId() {
|
|
177
|
+
return this.jobId;
|
|
178
|
+
}
|
|
179
|
+
getVideoStatus() {
|
|
180
|
+
return this.videoStatus;
|
|
181
|
+
}
|
|
182
|
+
getIsLoading() {
|
|
183
|
+
return this.isLoading;
|
|
184
|
+
}
|
|
185
|
+
getError() {
|
|
186
|
+
return this.error;
|
|
187
|
+
}
|
|
188
|
+
getStatus() {
|
|
189
|
+
return this.status;
|
|
190
|
+
}
|
|
191
|
+
// ===========================
|
|
192
|
+
// Private state setters
|
|
193
|
+
// ===========================
|
|
194
|
+
setResult(rawResult) {
|
|
195
|
+
if (rawResult === null) {
|
|
196
|
+
this.result = null;
|
|
197
|
+
this.callbacksRef.onResultChange?.(null);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (this.callbacksRef.onResult) {
|
|
201
|
+
const transformed = this.callbacksRef.onResult(rawResult);
|
|
202
|
+
if (transformed === null) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
if (transformed !== void 0) {
|
|
206
|
+
this.result = transformed;
|
|
207
|
+
this.callbacksRef.onResultChange?.(this.result);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
this.result = rawResult;
|
|
212
|
+
this.callbacksRef.onResultChange?.(this.result);
|
|
213
|
+
}
|
|
214
|
+
setJobId(jobId) {
|
|
215
|
+
this.jobId = jobId;
|
|
216
|
+
this.callbacksRef.onJobIdChange?.(jobId);
|
|
217
|
+
}
|
|
218
|
+
setVideoStatus(status) {
|
|
219
|
+
this.videoStatus = status;
|
|
220
|
+
this.callbacksRef.onVideoStatusChange?.(status);
|
|
221
|
+
}
|
|
222
|
+
setIsLoading(isLoading) {
|
|
223
|
+
this.isLoading = isLoading;
|
|
224
|
+
this.callbacksRef.onLoadingChange?.(isLoading);
|
|
225
|
+
}
|
|
226
|
+
setError(error) {
|
|
227
|
+
this.error = error;
|
|
228
|
+
this.callbacksRef.onErrorChange?.(error);
|
|
229
|
+
}
|
|
230
|
+
setStatus(status) {
|
|
231
|
+
this.status = status;
|
|
232
|
+
this.callbacksRef.onStatusChange?.(status);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
export {
|
|
236
|
+
VideoGenerationClient
|
|
237
|
+
};
|
|
238
|
+
//# sourceMappingURL=video-generation-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-generation-client.js","sources":["../../src/video-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 GenerationClientState,\n GenerationFetcher,\n VideoGenerateInput,\n VideoGenerateResult,\n VideoGenerationClientOptions,\n VideoStatusInfo,\n} from './generation-types'\n\n/**\n * Callbacks stored in a ref so hooks can update them without recreating the client.\n */\ninterface VideoCallbacks<TOutput> {\n onResult?: (result: VideoGenerateResult) => TOutput | null | void\n onError?: (error: Error) => void\n onProgress?: (progress: number, message?: string) => void\n onChunk?: (chunk: StreamChunk) => void\n onJobCreated?: (jobId: string) => void\n onStatusUpdate?: (status: VideoStatusInfo) => void\n onResultChange?: (result: TOutput | null) => void\n onLoadingChange?: (isLoading: boolean) => void\n onErrorChange?: (error: Error | undefined) => void\n onStatusChange?: (status: GenerationClientState) => void\n onJobIdChange?: (jobId: string | null) => void\n onVideoStatusChange?: (status: VideoStatusInfo | null) => void\n}\n\n/**\n * A specialized client for job-based video generation.\n *\n * Video generation is asynchronous: a job is created, then polled for status\n * until completion. This client handles the full lifecycle.\n *\n * Supports two transport modes:\n * - **ConnectionAdapter** — Server handles the polling loop internally and\n * streams status updates via CUSTOM events.\n * - **Fetcher** — Direct async function that returns a completed\n * `VideoGenerateResult`.\n *\n * @example\n * ```typescript\n * // With ConnectionAdapter (server-driven polling)\n * const client = new VideoGenerationClient({\n * connection: fetchServerSentEvents('/api/generate/video'),\n * onResultChange: setResult,\n * onVideoStatusChange: setVideoStatus,\n * })\n *\n * // With fetcher (direct result)\n * const client = new VideoGenerationClient({\n * fetcher: async (input) => {\n * const res = await fetch('/api/video/generate', {\n * method: 'POST',\n * body: JSON.stringify(input),\n * })\n * return res.json() // { jobId, status: 'completed', url, expiresAt }\n * },\n * })\n *\n * await client.generate({ prompt: 'A flying car over a city' })\n * ```\n */\nexport class VideoGenerationClient<TOutput = VideoGenerateResult> {\n private connection: ConnectionAdapter | undefined\n private fetcher:\n | GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\n | undefined\n private body: Record<string, any>\n\n private result: TOutput | null = null\n private jobId: string | null = null\n private videoStatus: VideoStatusInfo | 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: VideoCallbacks<TOutput>\n\n constructor(\n options: VideoGenerationClientOptions<TOutput> &\n (\n | { connection: ConnectionAdapter; fetcher?: never }\n | {\n fetcher: GenerationFetcher<VideoGenerateInput, VideoGenerateResult>\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 onJobCreated: options.onJobCreated,\n onStatusUpdate: options.onStatusUpdate,\n onResultChange: options.onResultChange,\n onLoadingChange: options.onLoadingChange,\n onErrorChange: options.onErrorChange,\n onStatusChange: options.onStatusChange,\n onJobIdChange: options.onJobIdChange,\n onVideoStatusChange: options.onVideoStatusChange,\n }\n }\n\n /**\n * Trigger video generation.\n * Only one generation can be in-flight at a time.\n */\n async generate(input: VideoGenerateInput): Promise<void> {\n if (this.isLoading) return\n\n this.setIsLoading(true)\n this.setStatus('generating')\n this.setError(undefined)\n this.setJobId(null)\n this.setVideoStatus(null)\n\n const abortController = new AbortController()\n this.abortController = abortController\n const { signal } = abortController\n\n try {\n if (this.fetcher) {\n await this.generateWithFetcher(input, signal)\n } else if (this.connection) {\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 'VideoGenerationClient 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 * Direct fetcher mode: call fetcher and set result.\n */\n private async generateWithFetcher(\n input: VideoGenerateInput,\n signal: AbortSignal,\n ): Promise<void> {\n if (!this.fetcher) return\n\n // Fetcher returns a completed result directly, or a Response with SSE body\n const result = await this.fetcher(input, { signal })\n if (signal.aborted) return\n\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 }\n\n /**\n * Process a stream of AG-UI events from the ConnectionAdapter.\n * The server handles the polling loop and streams status updates.\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.VIDEO_JOB_CREATED) {\n const { jobId } = chunk.value as { jobId: string }\n this.setJobId(jobId)\n this.callbacksRef.onJobCreated?.(jobId)\n } else if (chunk.name === GENERATION_EVENTS.VIDEO_STATUS) {\n const statusInfo = chunk.value as VideoStatusInfo\n this.setVideoStatus(statusInfo)\n this.callbacksRef.onStatusUpdate?.(statusInfo)\n if (statusInfo.progress !== undefined) {\n this.callbacksRef.onProgress?.(statusInfo.progress)\n }\n } else if (chunk.name === GENERATION_EVENTS.RESULT) {\n this.setResult(chunk.value as VideoGenerateResult)\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 or polling.\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 all state and return to idle.\n */\n reset(): void {\n this.stop()\n this.setResult(null)\n this.setJobId(null)\n this.setVideoStatus(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 VideoGenerationClientOptions<TOutput>,\n | 'body'\n | 'onResult'\n | 'onError'\n | 'onProgress'\n | 'onChunk'\n | 'onJobCreated'\n | 'onStatusUpdate'\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 if (options.onJobCreated !== undefined) {\n this.callbacksRef.onJobCreated = options.onJobCreated\n }\n if (options.onStatusUpdate !== undefined) {\n this.callbacksRef.onStatusUpdate = options.onStatusUpdate\n }\n }\n\n // ===========================\n // Getters\n // ===========================\n\n getResult(): TOutput | null {\n return this.result\n }\n\n getJobId(): string | null {\n return this.jobId\n }\n\n getVideoStatus(): VideoStatusInfo | null {\n return this.videoStatus\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: VideoGenerateResult | 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 setJobId(jobId: string | null): void {\n this.jobId = jobId\n this.callbacksRef.onJobIdChange?.(jobId)\n }\n\n private setVideoStatus(status: VideoStatusInfo | null): void {\n this.videoStatus = status\n this.callbacksRef.onVideoStatusChange?.(status)\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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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/ai-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Framework-agnostic headless client for TanStack AI",
|
|
5
5
|
"author": "",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"src"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@tanstack/ai": "0.6.
|
|
34
|
+
"@tanstack/ai": "0.6.3"
|
|
35
35
|
},
|
|
36
36
|
"devDependencies": {
|
|
37
37
|
"@vitest/coverage-v8": "4.0.14",
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { GENERATION_EVENTS } from './generation-types'
|
|
2
|
+
import { parseSSEResponse } from './sse-parser'
|
|
3
|
+
import type { StreamChunk } from '@tanstack/ai'
|
|
4
|
+
import type { ConnectionAdapter } from './connection-adapters'
|
|
5
|
+
import type {
|
|
6
|
+
GenerationClientOptions,
|
|
7
|
+
GenerationClientState,
|
|
8
|
+
GenerationFetcher,
|
|
9
|
+
} from './generation-types'
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Callbacks stored in a ref so hooks can update them without recreating the client.
|
|
13
|
+
*/
|
|
14
|
+
interface GenerationCallbacks<TResult, TOutput> {
|
|
15
|
+
onResult?: (result: TResult) => TOutput | null | void
|
|
16
|
+
onError?: (error: Error) => void
|
|
17
|
+
onProgress?: (progress: number, message?: string) => void
|
|
18
|
+
onChunk?: (chunk: StreamChunk) => void
|
|
19
|
+
onResultChange?: (result: TOutput | null) => void
|
|
20
|
+
onLoadingChange?: (isLoading: boolean) => void
|
|
21
|
+
onErrorChange?: (error: Error | undefined) => void
|
|
22
|
+
onStatusChange?: (status: GenerationClientState) => void
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A lightweight, generic client for one-shot generation tasks
|
|
27
|
+
* (image, speech, transcription, summarize).
|
|
28
|
+
*
|
|
29
|
+
* Supports two transport modes:
|
|
30
|
+
* - **ConnectionAdapter** — Streaming transport (SSE, HTTP stream, custom).
|
|
31
|
+
* Server wraps results in StreamChunk events with CUSTOM event names.
|
|
32
|
+
* - **Fetcher** — Direct async function call. No streaming protocol needed.
|
|
33
|
+
*
|
|
34
|
+
* @template TInput - The input type for the generation request
|
|
35
|
+
* @template TResult - The result type returned by the generation
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* // With ConnectionAdapter (streaming)
|
|
40
|
+
* const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({
|
|
41
|
+
* connection: fetchServerSentEvents('/api/generate/image'),
|
|
42
|
+
* onResultChange: setResult,
|
|
43
|
+
* onLoadingChange: setIsLoading,
|
|
44
|
+
* })
|
|
45
|
+
*
|
|
46
|
+
* // With fetcher (direct)
|
|
47
|
+
* const client = new GenerationClient<ImageGenerateInput, ImageGenerationResult>({
|
|
48
|
+
* fetcher: async (input) => {
|
|
49
|
+
* const res = await fetch('/api/generate/image', {
|
|
50
|
+
* method: 'POST',
|
|
51
|
+
* body: JSON.stringify(input),
|
|
52
|
+
* })
|
|
53
|
+
* return res.json()
|
|
54
|
+
* },
|
|
55
|
+
* })
|
|
56
|
+
*
|
|
57
|
+
* await client.generate({ prompt: 'A sunset over mountains' })
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export class GenerationClient<
|
|
61
|
+
TInput extends Record<string, any>,
|
|
62
|
+
TResult,
|
|
63
|
+
TOutput = TResult,
|
|
64
|
+
> {
|
|
65
|
+
private connection: ConnectionAdapter | undefined
|
|
66
|
+
private fetcher: GenerationFetcher<TInput, TResult> | undefined
|
|
67
|
+
private body: Record<string, any>
|
|
68
|
+
private result: TOutput | null = null
|
|
69
|
+
private isLoading = false
|
|
70
|
+
private error: Error | undefined = undefined
|
|
71
|
+
private status: GenerationClientState = 'idle'
|
|
72
|
+
private abortController: AbortController | null = null
|
|
73
|
+
private callbacksRef: GenerationCallbacks<TResult, TOutput>
|
|
74
|
+
|
|
75
|
+
constructor(
|
|
76
|
+
options: GenerationClientOptions<TInput, TResult, TOutput> &
|
|
77
|
+
(
|
|
78
|
+
| { connection: ConnectionAdapter; fetcher?: never }
|
|
79
|
+
| {
|
|
80
|
+
fetcher: GenerationFetcher<TInput, TResult>
|
|
81
|
+
connection?: never
|
|
82
|
+
}
|
|
83
|
+
),
|
|
84
|
+
) {
|
|
85
|
+
this.connection = options.connection
|
|
86
|
+
this.fetcher = options.fetcher
|
|
87
|
+
this.body = options.body ?? {}
|
|
88
|
+
|
|
89
|
+
this.callbacksRef = {
|
|
90
|
+
onResult: options.onResult,
|
|
91
|
+
onError: options.onError,
|
|
92
|
+
onProgress: options.onProgress,
|
|
93
|
+
onChunk: options.onChunk,
|
|
94
|
+
onResultChange: options.onResultChange,
|
|
95
|
+
onLoadingChange: options.onLoadingChange,
|
|
96
|
+
onErrorChange: options.onErrorChange,
|
|
97
|
+
onStatusChange: options.onStatusChange,
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Trigger a generation request.
|
|
103
|
+
* Only one generation can be in-flight at a time; calling generate()
|
|
104
|
+
* while already generating will be a no-op.
|
|
105
|
+
*/
|
|
106
|
+
async generate(input: TInput): Promise<void> {
|
|
107
|
+
if (this.isLoading) return
|
|
108
|
+
|
|
109
|
+
this.setIsLoading(true)
|
|
110
|
+
this.setStatus('generating')
|
|
111
|
+
this.setError(undefined)
|
|
112
|
+
|
|
113
|
+
const abortController = new AbortController()
|
|
114
|
+
this.abortController = abortController
|
|
115
|
+
const { signal } = abortController
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
if (this.fetcher) {
|
|
119
|
+
// Direct fetch path
|
|
120
|
+
const result = await this.fetcher(input, { signal })
|
|
121
|
+
if (signal.aborted) return
|
|
122
|
+
if (result instanceof Response) {
|
|
123
|
+
// Server function returned SSE Response — parse stream
|
|
124
|
+
await this.processStream(parseSSEResponse(result, signal))
|
|
125
|
+
} else {
|
|
126
|
+
this.setResult(result)
|
|
127
|
+
this.setStatus('success')
|
|
128
|
+
}
|
|
129
|
+
} else if (this.connection) {
|
|
130
|
+
// ConnectionAdapter streaming path
|
|
131
|
+
const mergedData = { ...this.body, ...input }
|
|
132
|
+
const stream = this.connection.connect([], mergedData, signal)
|
|
133
|
+
await this.processStream(stream)
|
|
134
|
+
} else {
|
|
135
|
+
throw new Error(
|
|
136
|
+
'GenerationClient requires either a connection or fetcher option',
|
|
137
|
+
)
|
|
138
|
+
}
|
|
139
|
+
} catch (err: any) {
|
|
140
|
+
if (signal.aborted) return
|
|
141
|
+
const error = err instanceof Error ? err : new Error(String(err))
|
|
142
|
+
this.setError(error)
|
|
143
|
+
this.setStatus('error')
|
|
144
|
+
this.callbacksRef.onError?.(error)
|
|
145
|
+
} finally {
|
|
146
|
+
this.abortController = null
|
|
147
|
+
this.setIsLoading(false)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Process a stream of AG-UI events from the ConnectionAdapter.
|
|
153
|
+
*/
|
|
154
|
+
private async processStream(
|
|
155
|
+
source: AsyncIterable<StreamChunk>,
|
|
156
|
+
): Promise<void> {
|
|
157
|
+
for await (const chunk of source) {
|
|
158
|
+
if (this.abortController?.signal.aborted) break
|
|
159
|
+
|
|
160
|
+
this.callbacksRef.onChunk?.(chunk)
|
|
161
|
+
|
|
162
|
+
switch (chunk.type) {
|
|
163
|
+
case 'CUSTOM': {
|
|
164
|
+
if (chunk.name === GENERATION_EVENTS.RESULT) {
|
|
165
|
+
this.setResult(chunk.value as TResult)
|
|
166
|
+
} else if (chunk.name === GENERATION_EVENTS.PROGRESS) {
|
|
167
|
+
const { progress, message } = chunk.value as {
|
|
168
|
+
progress: number
|
|
169
|
+
message?: string
|
|
170
|
+
}
|
|
171
|
+
this.callbacksRef.onProgress?.(progress, message)
|
|
172
|
+
}
|
|
173
|
+
break
|
|
174
|
+
}
|
|
175
|
+
case 'RUN_FINISHED': {
|
|
176
|
+
this.setStatus('success')
|
|
177
|
+
break
|
|
178
|
+
}
|
|
179
|
+
case 'RUN_ERROR': {
|
|
180
|
+
throw new Error(chunk.error.message)
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Abort any in-flight generation request.
|
|
188
|
+
*/
|
|
189
|
+
stop(): void {
|
|
190
|
+
if (this.abortController) {
|
|
191
|
+
this.abortController.abort()
|
|
192
|
+
this.abortController = null
|
|
193
|
+
}
|
|
194
|
+
this.setIsLoading(false)
|
|
195
|
+
if (this.status === 'generating') {
|
|
196
|
+
this.setStatus('idle')
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Clear the result, error, and return to idle state.
|
|
202
|
+
*/
|
|
203
|
+
reset(): void {
|
|
204
|
+
this.stop()
|
|
205
|
+
this.setResult(null)
|
|
206
|
+
this.setError(undefined)
|
|
207
|
+
this.setStatus('idle')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Update options without recreating the client.
|
|
212
|
+
*/
|
|
213
|
+
updateOptions(
|
|
214
|
+
options: Partial<
|
|
215
|
+
Pick<
|
|
216
|
+
GenerationClientOptions<TInput, TResult, TOutput>,
|
|
217
|
+
'body' | 'onResult' | 'onError' | 'onProgress' | 'onChunk'
|
|
218
|
+
>
|
|
219
|
+
>,
|
|
220
|
+
): void {
|
|
221
|
+
if (options.body !== undefined) {
|
|
222
|
+
this.body = options.body ?? {}
|
|
223
|
+
}
|
|
224
|
+
if (options.onResult !== undefined) {
|
|
225
|
+
this.callbacksRef.onResult = options.onResult
|
|
226
|
+
}
|
|
227
|
+
if (options.onError !== undefined) {
|
|
228
|
+
this.callbacksRef.onError = options.onError
|
|
229
|
+
}
|
|
230
|
+
if (options.onProgress !== undefined) {
|
|
231
|
+
this.callbacksRef.onProgress = options.onProgress
|
|
232
|
+
}
|
|
233
|
+
if (options.onChunk !== undefined) {
|
|
234
|
+
this.callbacksRef.onChunk = options.onChunk
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// ===========================
|
|
239
|
+
// Getters
|
|
240
|
+
// ===========================
|
|
241
|
+
|
|
242
|
+
getResult(): TOutput | null {
|
|
243
|
+
return this.result
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
getIsLoading(): boolean {
|
|
247
|
+
return this.isLoading
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
getError(): Error | undefined {
|
|
251
|
+
return this.error
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
getStatus(): GenerationClientState {
|
|
255
|
+
return this.status
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// ===========================
|
|
259
|
+
// Private state setters
|
|
260
|
+
// ===========================
|
|
261
|
+
|
|
262
|
+
private setResult(rawResult: TResult | null): void {
|
|
263
|
+
if (rawResult === null) {
|
|
264
|
+
this.result = null
|
|
265
|
+
this.callbacksRef.onResultChange?.(null)
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (this.callbacksRef.onResult) {
|
|
270
|
+
const transformed = this.callbacksRef.onResult(rawResult)
|
|
271
|
+
if (transformed === null) {
|
|
272
|
+
// null return → keep previous result unchanged
|
|
273
|
+
return
|
|
274
|
+
}
|
|
275
|
+
if (transformed !== undefined) {
|
|
276
|
+
// Non-null, non-undefined → use transformed value
|
|
277
|
+
this.result = transformed
|
|
278
|
+
this.callbacksRef.onResultChange?.(this.result)
|
|
279
|
+
return
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// No onResult callback, or callback returned void → use raw value
|
|
284
|
+
this.result = rawResult as unknown as TOutput
|
|
285
|
+
this.callbacksRef.onResultChange?.(this.result)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
private setIsLoading(isLoading: boolean): void {
|
|
289
|
+
this.isLoading = isLoading
|
|
290
|
+
this.callbacksRef.onLoadingChange?.(isLoading)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private setError(error: Error | undefined): void {
|
|
294
|
+
this.error = error
|
|
295
|
+
this.callbacksRef.onErrorChange?.(error)
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
private setStatus(status: GenerationClientState): void {
|
|
299
|
+
this.status = status
|
|
300
|
+
this.callbacksRef.onStatusChange?.(status)
|
|
301
|
+
}
|
|
302
|
+
}
|