@tanstack/ai 0.32.0 → 0.33.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/activities/chat/index.js +24 -3
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +7 -0
- package/dist/esm/activities/chat/tools/lazy-tool-manager.d.ts +25 -1
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js +26 -2
- package/dist/esm/activities/chat/tools/lazy-tool-manager.js.map +1 -1
- package/dist/esm/activities/generateAudio/index.d.ts +7 -0
- package/dist/esm/activities/generateAudio/index.js +26 -1
- package/dist/esm/activities/generateAudio/index.js.map +1 -1
- package/dist/esm/activities/generateImage/index.d.ts +7 -0
- package/dist/esm/activities/generateImage/index.js +26 -1
- package/dist/esm/activities/generateImage/index.js.map +1 -1
- package/dist/esm/activities/generateSpeech/index.d.ts +7 -0
- package/dist/esm/activities/generateSpeech/index.js +26 -1
- package/dist/esm/activities/generateSpeech/index.js.map +1 -1
- package/dist/esm/activities/generateTranscription/index.d.ts +7 -0
- package/dist/esm/activities/generateTranscription/index.js +26 -1
- package/dist/esm/activities/generateTranscription/index.js.map +1 -1
- package/dist/esm/activities/generateVideo/index.d.ts +9 -0
- package/dist/esm/activities/generateVideo/index.js +52 -2
- package/dist/esm/activities/generateVideo/index.js.map +1 -1
- package/dist/esm/activities/middleware/index.d.ts +2 -0
- package/dist/esm/activities/middleware/run.d.ts +20 -0
- package/dist/esm/activities/middleware/run.js +42 -0
- package/dist/esm/activities/middleware/run.js.map +1 -0
- package/dist/esm/activities/middleware/types.d.ts +118 -0
- package/dist/esm/index.d.ts +2 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/middlewares/otel.d.ts +8 -2
- package/dist/esm/middlewares/otel.js +145 -95
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/middlewares/usage-attributes.d.ts +24 -0
- package/dist/esm/middlewares/usage-attributes.js +43 -0
- package/dist/esm/middlewares/usage-attributes.js.map +1 -0
- package/dist/esm/types.d.ts +7 -7
- package/dist/esm/utilities/errors.d.ts +13 -0
- package/dist/esm/utilities/errors.js +22 -0
- package/dist/esm/utilities/errors.js.map +1 -0
- package/dist/esm/utilities/numbers.d.ts +8 -0
- package/dist/esm/utilities/numbers.js +12 -0
- package/dist/esm/utilities/numbers.js.map +1 -0
- package/package.json +2 -2
- package/src/activities/chat/index.ts +32 -4
- package/src/activities/chat/middleware/types.ts +7 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
- package/src/activities/generateAudio/index.ts +42 -1
- package/src/activities/generateImage/index.ts +42 -1
- package/src/activities/generateSpeech/index.ts +42 -1
- package/src/activities/generateTranscription/index.ts +42 -1
- package/src/activities/generateVideo/index.ts +88 -2
- package/src/activities/middleware/index.ts +20 -0
- package/src/activities/middleware/run.ts +88 -0
- package/src/activities/middleware/types.ts +173 -0
- package/src/index.ts +19 -0
- package/src/middlewares/otel.ts +195 -120
- package/src/middlewares/usage-attributes.ts +65 -0
- package/src/types.ts +7 -7
- package/src/utilities/errors.ts +29 -0
- package/src/utilities/numbers.ts +15 -0
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
9
9
|
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
10
10
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
11
|
+
import {
|
|
12
|
+
createGenerationContext,
|
|
13
|
+
runGenerationError,
|
|
14
|
+
runGenerationFinish,
|
|
15
|
+
runGenerationStart,
|
|
16
|
+
runGenerationUsage,
|
|
17
|
+
} from '../middleware'
|
|
11
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
12
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware'
|
|
13
21
|
import type { AudioAdapter } from './adapter'
|
|
14
22
|
import type { AudioGenerationResult, StreamChunk } from '../../types'
|
|
15
23
|
|
|
@@ -70,6 +78,12 @@ export interface AudioActivityOptions<
|
|
|
70
78
|
* control and/or a custom `Logger`.
|
|
71
79
|
*/
|
|
72
80
|
debug?: DebugOption
|
|
81
|
+
/**
|
|
82
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
83
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
84
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
85
|
+
*/
|
|
86
|
+
middleware?: Array<GenerationMiddleware>
|
|
73
87
|
}
|
|
74
88
|
|
|
75
89
|
// ===========================
|
|
@@ -135,7 +149,13 @@ async function runGenerateAudio<
|
|
|
135
149
|
>(
|
|
136
150
|
options: AudioActivityOptions<TAdapter, boolean>,
|
|
137
151
|
): Promise<AudioGenerationResult> {
|
|
138
|
-
const {
|
|
152
|
+
const {
|
|
153
|
+
adapter,
|
|
154
|
+
stream: _stream,
|
|
155
|
+
debug: _debug,
|
|
156
|
+
middleware,
|
|
157
|
+
...rest
|
|
158
|
+
} = options
|
|
139
159
|
const model = adapter.model
|
|
140
160
|
const requestId = createId('audio')
|
|
141
161
|
const startTime = Date.now()
|
|
@@ -145,6 +165,17 @@ async function runGenerateAudio<
|
|
|
145
165
|
(adapter as { name?: string }).name ??
|
|
146
166
|
'unknown'
|
|
147
167
|
|
|
168
|
+
const mwCtx = createGenerationContext({
|
|
169
|
+
requestId,
|
|
170
|
+
activity: 'audio',
|
|
171
|
+
provider: adapter.name,
|
|
172
|
+
model,
|
|
173
|
+
modelOptions: rest.modelOptions,
|
|
174
|
+
createId,
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
await runGenerationStart(middleware, mwCtx)
|
|
178
|
+
|
|
148
179
|
aiEventClient.emit('audio:request:started', {
|
|
149
180
|
requestId,
|
|
150
181
|
provider: adapter.name,
|
|
@@ -189,6 +220,12 @@ async function runGenerateAudio<
|
|
|
189
220
|
audioDuration: result.audio.duration,
|
|
190
221
|
})
|
|
191
222
|
|
|
223
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
|
|
224
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
225
|
+
duration: elapsedMs,
|
|
226
|
+
usage: result.usage,
|
|
227
|
+
})
|
|
228
|
+
|
|
192
229
|
return result
|
|
193
230
|
} catch (error) {
|
|
194
231
|
const elapsedMs = Date.now() - startTime
|
|
@@ -202,6 +239,10 @@ async function runGenerateAudio<
|
|
|
202
239
|
modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
|
|
203
240
|
timestamp: Date.now(),
|
|
204
241
|
})
|
|
242
|
+
await runGenerationError(middleware, mwCtx, {
|
|
243
|
+
error,
|
|
244
|
+
duration: elapsedMs,
|
|
245
|
+
})
|
|
205
246
|
logger.errors('generateAudio activity failed', {
|
|
206
247
|
error,
|
|
207
248
|
source: 'generateAudio',
|
|
@@ -8,9 +8,17 @@
|
|
|
8
8
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
9
9
|
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
10
10
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
11
|
+
import {
|
|
12
|
+
createGenerationContext,
|
|
13
|
+
runGenerationError,
|
|
14
|
+
runGenerationFinish,
|
|
15
|
+
runGenerationStart,
|
|
16
|
+
runGenerationUsage,
|
|
17
|
+
} from '../middleware'
|
|
11
18
|
import { resolveMediaPrompt } from '../../utilities/media-prompt'
|
|
12
19
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
13
20
|
import type { DebugOption } from '../../logger/types'
|
|
21
|
+
import type { GenerationMiddleware } from '../middleware'
|
|
14
22
|
import type { ImageAdapter } from './adapter'
|
|
15
23
|
import type {
|
|
16
24
|
ImageGenerationResult,
|
|
@@ -123,6 +131,12 @@ export type ImageActivityOptions<
|
|
|
123
131
|
* control and/or a custom `Logger`.
|
|
124
132
|
*/
|
|
125
133
|
debug?: DebugOption
|
|
134
|
+
/**
|
|
135
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
136
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
137
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
138
|
+
*/
|
|
139
|
+
middleware?: Array<GenerationMiddleware>
|
|
126
140
|
} & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>
|
|
127
141
|
? {
|
|
128
142
|
/** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<
|
|
@@ -228,12 +242,29 @@ async function runGenerateImage<
|
|
|
228
242
|
>(
|
|
229
243
|
options: ImageActivityOptions<TAdapter, boolean>,
|
|
230
244
|
): Promise<ImageGenerationResult> {
|
|
231
|
-
const {
|
|
245
|
+
const {
|
|
246
|
+
adapter,
|
|
247
|
+
stream: _stream,
|
|
248
|
+
debug: _debug,
|
|
249
|
+
middleware,
|
|
250
|
+
...rest
|
|
251
|
+
} = options
|
|
232
252
|
const model = adapter.model
|
|
233
253
|
const requestId = createId('image')
|
|
234
254
|
const startTime = Date.now()
|
|
235
255
|
const logger: InternalLogger = resolveDebugOption(options.debug)
|
|
236
256
|
|
|
257
|
+
const mwCtx = createGenerationContext({
|
|
258
|
+
requestId,
|
|
259
|
+
activity: 'image',
|
|
260
|
+
provider: adapter.name,
|
|
261
|
+
model,
|
|
262
|
+
modelOptions: rest.modelOptions,
|
|
263
|
+
createId,
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
await runGenerationStart(middleware, mwCtx)
|
|
267
|
+
|
|
237
268
|
// Devtools events carry the flattened prompt text plus media-part counts —
|
|
238
269
|
// the wire payload stays `prompt: string` regardless of the prompt shape.
|
|
239
270
|
const resolved = resolveMediaPrompt(rest.prompt)
|
|
@@ -299,8 +330,18 @@ async function runGenerateImage<
|
|
|
299
330
|
count: result.images.length,
|
|
300
331
|
})
|
|
301
332
|
|
|
333
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
|
|
334
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
335
|
+
duration,
|
|
336
|
+
usage: result.usage,
|
|
337
|
+
})
|
|
338
|
+
|
|
302
339
|
return result
|
|
303
340
|
} catch (error) {
|
|
341
|
+
await runGenerationError(middleware, mwCtx, {
|
|
342
|
+
error,
|
|
343
|
+
duration: Date.now() - startTime,
|
|
344
|
+
})
|
|
304
345
|
logger.errors('generateImage activity failed', {
|
|
305
346
|
error,
|
|
306
347
|
source: 'generateImage',
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
9
9
|
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
10
10
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
11
|
+
import {
|
|
12
|
+
createGenerationContext,
|
|
13
|
+
runGenerationError,
|
|
14
|
+
runGenerationFinish,
|
|
15
|
+
runGenerationStart,
|
|
16
|
+
runGenerationUsage,
|
|
17
|
+
} from '../middleware'
|
|
11
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
12
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware'
|
|
13
21
|
import type { TTSAdapter } from './adapter'
|
|
14
22
|
import type { StreamChunk, TTSResult } from '../../types'
|
|
15
23
|
|
|
@@ -73,6 +81,12 @@ export interface TTSActivityOptions<
|
|
|
73
81
|
* control and/or a custom `Logger`.
|
|
74
82
|
*/
|
|
75
83
|
debug?: DebugOption
|
|
84
|
+
/**
|
|
85
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
86
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
87
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
88
|
+
*/
|
|
89
|
+
middleware?: Array<GenerationMiddleware>
|
|
76
90
|
}
|
|
77
91
|
|
|
78
92
|
// ===========================
|
|
@@ -143,7 +157,13 @@ export function generateSpeech<
|
|
|
143
157
|
async function runGenerateSpeech<
|
|
144
158
|
TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,
|
|
145
159
|
>(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {
|
|
146
|
-
const {
|
|
160
|
+
const {
|
|
161
|
+
adapter,
|
|
162
|
+
stream: _stream,
|
|
163
|
+
debug: _debug,
|
|
164
|
+
middleware,
|
|
165
|
+
...rest
|
|
166
|
+
} = options
|
|
147
167
|
const model = adapter.model
|
|
148
168
|
const requestId = createId('speech')
|
|
149
169
|
const startTime = Date.now()
|
|
@@ -153,6 +173,17 @@ async function runGenerateSpeech<
|
|
|
153
173
|
(adapter as { name?: string }).name ??
|
|
154
174
|
'unknown'
|
|
155
175
|
|
|
176
|
+
const mwCtx = createGenerationContext({
|
|
177
|
+
requestId,
|
|
178
|
+
activity: 'tts',
|
|
179
|
+
provider: adapter.name,
|
|
180
|
+
model,
|
|
181
|
+
modelOptions: rest.modelOptions,
|
|
182
|
+
createId,
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
await runGenerationStart(middleware, mwCtx)
|
|
186
|
+
|
|
156
187
|
aiEventClient.emit('speech:request:started', {
|
|
157
188
|
requestId,
|
|
158
189
|
provider: adapter.name,
|
|
@@ -202,6 +233,12 @@ async function runGenerateSpeech<
|
|
|
202
233
|
contentType: result.contentType,
|
|
203
234
|
})
|
|
204
235
|
|
|
236
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
|
|
237
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
238
|
+
duration,
|
|
239
|
+
usage: result.usage,
|
|
240
|
+
})
|
|
241
|
+
|
|
205
242
|
return result
|
|
206
243
|
} catch (error) {
|
|
207
244
|
const duration = Date.now() - startTime
|
|
@@ -215,6 +252,10 @@ async function runGenerateSpeech<
|
|
|
215
252
|
modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
|
|
216
253
|
timestamp: Date.now(),
|
|
217
254
|
})
|
|
255
|
+
await runGenerationError(middleware, mwCtx, {
|
|
256
|
+
error,
|
|
257
|
+
duration,
|
|
258
|
+
})
|
|
218
259
|
logger.errors('generateSpeech activity failed', {
|
|
219
260
|
error,
|
|
220
261
|
source: 'generateSpeech',
|
|
@@ -8,8 +8,16 @@
|
|
|
8
8
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
9
9
|
import { streamGenerationResult } from '../stream-generation-result.js'
|
|
10
10
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
11
|
+
import {
|
|
12
|
+
createGenerationContext,
|
|
13
|
+
runGenerationError,
|
|
14
|
+
runGenerationFinish,
|
|
15
|
+
runGenerationStart,
|
|
16
|
+
runGenerationUsage,
|
|
17
|
+
} from '../middleware'
|
|
11
18
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
12
19
|
import type { DebugOption } from '../../logger/types'
|
|
20
|
+
import type { GenerationMiddleware } from '../middleware'
|
|
13
21
|
import type { TranscriptionAdapter } from './adapter'
|
|
14
22
|
import type { StreamChunk, TranscriptionResult } from '../../types'
|
|
15
23
|
|
|
@@ -76,6 +84,12 @@ export interface TranscriptionActivityOptions<
|
|
|
76
84
|
* control and/or a custom `Logger`.
|
|
77
85
|
*/
|
|
78
86
|
debug?: DebugOption
|
|
87
|
+
/**
|
|
88
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
89
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
90
|
+
* `GenerationMiddleware` contract for a custom backend.
|
|
91
|
+
*/
|
|
92
|
+
middleware?: Array<GenerationMiddleware>
|
|
79
93
|
}
|
|
80
94
|
|
|
81
95
|
// ===========================
|
|
@@ -174,7 +188,13 @@ async function runGenerateTranscription<
|
|
|
174
188
|
>(
|
|
175
189
|
options: TranscriptionActivityOptions<TAdapter, boolean>,
|
|
176
190
|
): Promise<TranscriptionResult> {
|
|
177
|
-
const {
|
|
191
|
+
const {
|
|
192
|
+
adapter,
|
|
193
|
+
stream: _stream,
|
|
194
|
+
debug: _debug,
|
|
195
|
+
middleware,
|
|
196
|
+
...rest
|
|
197
|
+
} = options
|
|
178
198
|
const model = adapter.model
|
|
179
199
|
const requestId = createId('transcription')
|
|
180
200
|
const startTime = Date.now()
|
|
@@ -184,6 +204,17 @@ async function runGenerateTranscription<
|
|
|
184
204
|
(adapter as { name?: string }).name ??
|
|
185
205
|
'unknown'
|
|
186
206
|
|
|
207
|
+
const mwCtx = createGenerationContext({
|
|
208
|
+
requestId,
|
|
209
|
+
activity: 'transcription',
|
|
210
|
+
provider: adapter.name,
|
|
211
|
+
model,
|
|
212
|
+
modelOptions: rest.modelOptions,
|
|
213
|
+
createId,
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
await runGenerationStart(middleware, mwCtx)
|
|
217
|
+
|
|
187
218
|
aiEventClient.emit('transcription:request:started', {
|
|
188
219
|
requestId,
|
|
189
220
|
provider: adapter.name,
|
|
@@ -220,6 +251,12 @@ async function runGenerateTranscription<
|
|
|
220
251
|
{ hasText: !!result.text },
|
|
221
252
|
)
|
|
222
253
|
|
|
254
|
+
if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)
|
|
255
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
256
|
+
duration,
|
|
257
|
+
usage: result.usage,
|
|
258
|
+
})
|
|
259
|
+
|
|
223
260
|
return result
|
|
224
261
|
} catch (error) {
|
|
225
262
|
const duration = Date.now() - startTime
|
|
@@ -233,6 +270,10 @@ async function runGenerateTranscription<
|
|
|
233
270
|
modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
|
|
234
271
|
timestamp: Date.now(),
|
|
235
272
|
})
|
|
273
|
+
await runGenerationError(middleware, mwCtx, {
|
|
274
|
+
error,
|
|
275
|
+
duration,
|
|
276
|
+
})
|
|
236
277
|
logger.errors('generateTranscription activity failed', {
|
|
237
278
|
error,
|
|
238
279
|
source: 'generateTranscription',
|
|
@@ -10,8 +10,17 @@
|
|
|
10
10
|
import { aiEventClient } from '@tanstack/ai-event-client'
|
|
11
11
|
import { toRunErrorPayload } from '../error-payload'
|
|
12
12
|
import { resolveDebugOption } from '../../logger/resolve'
|
|
13
|
+
import {
|
|
14
|
+
createGenerationContext,
|
|
15
|
+
runGenerationAbort,
|
|
16
|
+
runGenerationError,
|
|
17
|
+
runGenerationFinish,
|
|
18
|
+
runGenerationStart,
|
|
19
|
+
runGenerationUsage,
|
|
20
|
+
} from '../middleware'
|
|
13
21
|
import type { InternalLogger } from '../../logger/internal-logger'
|
|
14
22
|
import type { DebugOption } from '../../logger/types'
|
|
23
|
+
import type { GenerationMiddleware } from '../middleware'
|
|
15
24
|
import type { VideoAdapter } from './adapter'
|
|
16
25
|
import type {
|
|
17
26
|
MediaPrompt,
|
|
@@ -160,6 +169,14 @@ export type VideoCreateOptions<
|
|
|
160
169
|
* control and/or a custom `Logger`.
|
|
161
170
|
*/
|
|
162
171
|
debug?: DebugOption
|
|
172
|
+
/**
|
|
173
|
+
* Observe-only middleware notified on start, usage, success, and error. Pass
|
|
174
|
+
* `otelMiddleware()` to emit OpenTelemetry spans, or implement the
|
|
175
|
+
* `GenerationMiddleware` contract for a custom backend. In streaming mode the
|
|
176
|
+
* span covers the full create→poll→complete lifecycle; in non-streaming mode
|
|
177
|
+
* it covers job submission. An abandoned stream fires `onAbort`.
|
|
178
|
+
*/
|
|
179
|
+
middleware?: Array<GenerationMiddleware>
|
|
163
180
|
} & ({} extends VideoProviderOptions<TAdapter>
|
|
164
181
|
? {
|
|
165
182
|
/** Provider-specific options for video generation */ modelOptions?: VideoProviderOptions<TAdapter>
|
|
@@ -299,14 +316,27 @@ export function generateVideo<
|
|
|
299
316
|
async function runCreateVideoJob<
|
|
300
317
|
TAdapter extends VideoAdapter<string, any, any, any>,
|
|
301
318
|
>(options: VideoCreateOptions<TAdapter, boolean>): Promise<VideoJobResult> {
|
|
302
|
-
const { adapter, prompt, size, duration, modelOptions } = options
|
|
319
|
+
const { adapter, prompt, size, duration, modelOptions, middleware } = options
|
|
303
320
|
const model = adapter.model
|
|
321
|
+
const requestId = createId('video')
|
|
322
|
+
const startTime = Date.now()
|
|
304
323
|
const logger: InternalLogger = resolveDebugOption(options.debug)
|
|
305
324
|
const providerName =
|
|
306
325
|
(adapter as { name?: string; provider?: string }).provider ??
|
|
307
326
|
(adapter as { name?: string }).name ??
|
|
308
327
|
'unknown'
|
|
309
328
|
|
|
329
|
+
const mwCtx = createGenerationContext({
|
|
330
|
+
requestId,
|
|
331
|
+
activity: 'video',
|
|
332
|
+
provider: adapter.name,
|
|
333
|
+
model,
|
|
334
|
+
modelOptions,
|
|
335
|
+
createId,
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
await runGenerationStart(middleware, mwCtx)
|
|
339
|
+
|
|
310
340
|
logger.request(`activity=generateVideo provider=${providerName}`, {
|
|
311
341
|
provider: providerName,
|
|
312
342
|
model,
|
|
@@ -325,8 +355,17 @@ async function runCreateVideoJob<
|
|
|
325
355
|
jobId: result.jobId,
|
|
326
356
|
model: result.model,
|
|
327
357
|
})
|
|
358
|
+
// Non-streaming create only submits the job; usage isn't known until the
|
|
359
|
+
// job completes via polling, so the span covers submission only.
|
|
360
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
361
|
+
duration: Date.now() - startTime,
|
|
362
|
+
})
|
|
328
363
|
return result
|
|
329
364
|
} catch (error) {
|
|
365
|
+
await runGenerationError(middleware, mwCtx, {
|
|
366
|
+
error,
|
|
367
|
+
duration: Date.now() - startTime,
|
|
368
|
+
})
|
|
330
369
|
logger.errors('generateVideo activity failed', {
|
|
331
370
|
error,
|
|
332
371
|
source: 'generateVideo',
|
|
@@ -346,9 +385,11 @@ function sleep(ms: number): Promise<void> {
|
|
|
346
385
|
async function* runStreamingVideoGeneration<
|
|
347
386
|
TAdapter extends VideoAdapter<string, any, any, any>,
|
|
348
387
|
>(options: VideoCreateOptions<TAdapter, true>): AsyncIterable<StreamChunk> {
|
|
349
|
-
const { adapter, prompt, size, duration, modelOptions } = options
|
|
388
|
+
const { adapter, prompt, size, duration, modelOptions, middleware } = options
|
|
350
389
|
const model = adapter.model
|
|
351
390
|
const runId = options.runId ?? createId('run')
|
|
391
|
+
const requestId = createId('video')
|
|
392
|
+
const obsStartTime = Date.now()
|
|
352
393
|
const pollingInterval = options.pollingInterval ?? 2000
|
|
353
394
|
const maxDuration = options.maxDuration ?? 600_000
|
|
354
395
|
const logger: InternalLogger = resolveDebugOption(options.debug)
|
|
@@ -366,6 +407,17 @@ async function* runStreamingVideoGeneration<
|
|
|
366
407
|
timestamp: Date.now(),
|
|
367
408
|
} as StreamChunk
|
|
368
409
|
|
|
410
|
+
const mwCtx = createGenerationContext({
|
|
411
|
+
requestId,
|
|
412
|
+
activity: 'video',
|
|
413
|
+
provider: adapter.name,
|
|
414
|
+
model,
|
|
415
|
+
modelOptions,
|
|
416
|
+
createId,
|
|
417
|
+
})
|
|
418
|
+
|
|
419
|
+
await runGenerationStart(middleware, mwCtx)
|
|
420
|
+
|
|
369
421
|
logger.request(
|
|
370
422
|
`activity=generateVideo provider=${providerName} stream=true`,
|
|
371
423
|
{
|
|
@@ -374,6 +426,9 @@ async function* runStreamingVideoGeneration<
|
|
|
374
426
|
},
|
|
375
427
|
)
|
|
376
428
|
|
|
429
|
+
// Tracks whether a terminal observer event (finish/error) has already fired,
|
|
430
|
+
// so the `finally` below can fire one on abandonment without double-firing.
|
|
431
|
+
let settled = false
|
|
377
432
|
try {
|
|
378
433
|
// Create the video generation job
|
|
379
434
|
const jobResult = await adapter.createVideoJob({
|
|
@@ -422,6 +477,18 @@ async function* runStreamingVideoGeneration<
|
|
|
422
477
|
},
|
|
423
478
|
)
|
|
424
479
|
|
|
480
|
+
// Fire finish before yielding the terminal chunks: the generation has
|
|
481
|
+
// succeeded, so a consumer that stops reading after `generation:result`
|
|
482
|
+
// (without pulling `RUN_FINISHED`) must not trip the abandonment path in
|
|
483
|
+
// `finally`, which would otherwise report a spurious cancellation.
|
|
484
|
+
if (urlResult.usage)
|
|
485
|
+
await runGenerationUsage(middleware, mwCtx, urlResult.usage)
|
|
486
|
+
await runGenerationFinish(middleware, mwCtx, {
|
|
487
|
+
duration: Date.now() - obsStartTime,
|
|
488
|
+
usage: urlResult.usage,
|
|
489
|
+
})
|
|
490
|
+
settled = true
|
|
491
|
+
|
|
425
492
|
yield {
|
|
426
493
|
type: 'CUSTOM',
|
|
427
494
|
name: 'generation:result',
|
|
@@ -453,6 +520,14 @@ async function* runStreamingVideoGeneration<
|
|
|
453
520
|
throw new Error('Video generation timed out')
|
|
454
521
|
} catch (error: unknown) {
|
|
455
522
|
const payload = toRunErrorPayload(error, 'Video generation failed')
|
|
523
|
+
// Mark settled before firing onError: if a user error-hook throws, the
|
|
524
|
+
// `finally` below must still not double-fire onAbort over the same op
|
|
525
|
+
// (which would mask the original error and end the span twice).
|
|
526
|
+
settled = true
|
|
527
|
+
await runGenerationError(middleware, mwCtx, {
|
|
528
|
+
error,
|
|
529
|
+
duration: Date.now() - obsStartTime,
|
|
530
|
+
})
|
|
456
531
|
logger.errors('generateVideo activity failed', {
|
|
457
532
|
message: payload.message,
|
|
458
533
|
code: payload.code,
|
|
@@ -467,6 +542,17 @@ async function* runStreamingVideoGeneration<
|
|
|
467
542
|
error: payload,
|
|
468
543
|
timestamp: Date.now(),
|
|
469
544
|
} as StreamChunk
|
|
545
|
+
} finally {
|
|
546
|
+
if (!settled) {
|
|
547
|
+
// The consumer abandoned the stream (broke the `for await` loop or
|
|
548
|
+
// disconnected) before completion, so the generator is being unwound at
|
|
549
|
+
// a `yield` without reaching finish/error. Fire `onAbort` — a cancel, not
|
|
550
|
+
// an error — so otelMiddleware ends its span instead of leaking it.
|
|
551
|
+
await runGenerationAbort(middleware, mwCtx, {
|
|
552
|
+
reason: 'Video generation stream abandoned before completion',
|
|
553
|
+
duration: Date.now() - obsStartTime,
|
|
554
|
+
})
|
|
555
|
+
}
|
|
470
556
|
}
|
|
471
557
|
}
|
|
472
558
|
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Base, activity-agnostic middleware shared by chat and the media activities.
|
|
2
|
+
// The `ChatMiddleware` superset lives at `../chat/middleware`.
|
|
3
|
+
export type {
|
|
4
|
+
GenerationActivity,
|
|
5
|
+
GenerationMiddleware,
|
|
6
|
+
GenerationMiddlewareContext,
|
|
7
|
+
GenerationUsageInfo,
|
|
8
|
+
GenerationFinishInfo,
|
|
9
|
+
GenerationAbortInfo,
|
|
10
|
+
GenerationErrorInfo,
|
|
11
|
+
AnyGenerationMiddleware,
|
|
12
|
+
} from './types'
|
|
13
|
+
export {
|
|
14
|
+
createGenerationContext,
|
|
15
|
+
runGenerationStart,
|
|
16
|
+
runGenerationUsage,
|
|
17
|
+
runGenerationFinish,
|
|
18
|
+
runGenerationAbort,
|
|
19
|
+
runGenerationError,
|
|
20
|
+
} from './run'
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
GenerationAbortInfo,
|
|
3
|
+
GenerationErrorInfo,
|
|
4
|
+
GenerationFinishInfo,
|
|
5
|
+
GenerationMiddleware,
|
|
6
|
+
GenerationMiddlewareContext,
|
|
7
|
+
GenerationUsageInfo,
|
|
8
|
+
} from './types'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Build the stable context for a single media-activity call.
|
|
12
|
+
*
|
|
13
|
+
* Media activities are always server-side and carry no user runtime context,
|
|
14
|
+
* so `source` is fixed to `'server'` and `context` to `undefined`.
|
|
15
|
+
*/
|
|
16
|
+
export function createGenerationContext(args: {
|
|
17
|
+
requestId: string
|
|
18
|
+
activity: GenerationMiddlewareContext['activity']
|
|
19
|
+
provider: string
|
|
20
|
+
model: string
|
|
21
|
+
modelOptions?: unknown
|
|
22
|
+
createId: (prefix: string) => string
|
|
23
|
+
}): GenerationMiddlewareContext {
|
|
24
|
+
return {
|
|
25
|
+
requestId: args.requestId,
|
|
26
|
+
activity: args.activity,
|
|
27
|
+
provider: args.provider,
|
|
28
|
+
model: args.model,
|
|
29
|
+
modelOptions: args.modelOptions,
|
|
30
|
+
source: 'server',
|
|
31
|
+
createId: args.createId,
|
|
32
|
+
context: undefined,
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Run a single lifecycle hook across each middleware in registration order,
|
|
38
|
+
* awaiting each. Exceptions PROPAGATE (matching `chat()` middleware) — a
|
|
39
|
+
* broken middleware fails the activity rather than being silently swallowed.
|
|
40
|
+
*/
|
|
41
|
+
async function run(
|
|
42
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
43
|
+
invoke: (mw: GenerationMiddleware) => void | Promise<void>,
|
|
44
|
+
): Promise<void> {
|
|
45
|
+
if (!middleware || middleware.length === 0) return
|
|
46
|
+
for (const mw of middleware) {
|
|
47
|
+
await invoke(mw)
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function runGenerationStart(
|
|
52
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
53
|
+
ctx: GenerationMiddlewareContext,
|
|
54
|
+
): Promise<void> {
|
|
55
|
+
return run(middleware, (mw) => mw.onStart?.(ctx))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function runGenerationUsage(
|
|
59
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
60
|
+
ctx: GenerationMiddlewareContext,
|
|
61
|
+
usage: GenerationUsageInfo,
|
|
62
|
+
): Promise<void> {
|
|
63
|
+
return run(middleware, (mw) => mw.onUsage?.(ctx, usage))
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function runGenerationFinish(
|
|
67
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
68
|
+
ctx: GenerationMiddlewareContext,
|
|
69
|
+
info: GenerationFinishInfo,
|
|
70
|
+
): Promise<void> {
|
|
71
|
+
return run(middleware, (mw) => mw.onFinish?.(ctx, info))
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function runGenerationAbort(
|
|
75
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
76
|
+
ctx: GenerationMiddlewareContext,
|
|
77
|
+
info: GenerationAbortInfo,
|
|
78
|
+
): Promise<void> {
|
|
79
|
+
return run(middleware, (mw) => mw.onAbort?.(ctx, info))
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function runGenerationError(
|
|
83
|
+
middleware: ReadonlyArray<GenerationMiddleware> | undefined,
|
|
84
|
+
ctx: GenerationMiddlewareContext,
|
|
85
|
+
info: GenerationErrorInfo,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
return run(middleware, (mw) => mw.onError?.(ctx, info))
|
|
88
|
+
}
|