@tanstack/ai 0.32.0 → 0.34.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 +47 -20
- 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/chat/tools/schema-converter.d.ts +13 -0
- package/dist/esm/activities/chat/tools/schema-converter.js +61 -33
- package/dist/esm/activities/chat/tools/schema-converter.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 +3 -2
- package/src/activities/chat/index.ts +125 -35
- package/src/activities/chat/middleware/types.ts +7 -0
- package/src/activities/chat/tools/lazy-tool-manager.ts +46 -4
- package/src/activities/chat/tools/schema-converter.ts +146 -93
- 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
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import type { TokenUsage } from '../../types'
|
|
2
|
+
|
|
3
|
+
// ===========================
|
|
4
|
+
// Generation middleware
|
|
5
|
+
// ===========================
|
|
6
|
+
//
|
|
7
|
+
// The base, activity-agnostic middleware contract. Every activity — chat and
|
|
8
|
+
// the media activities — runs middleware that satisfies this shape. `chat()`
|
|
9
|
+
// accepts the richer `ChatMiddleware` superset (it adds config/chunk/tool
|
|
10
|
+
// hooks and capability primitives on top of these lifecycle hooks); media
|
|
11
|
+
// activities accept `GenerationMiddleware` directly.
|
|
12
|
+
//
|
|
13
|
+
// The relationship is intentionally STRUCTURAL, not nominal: `ChatMiddleware`
|
|
14
|
+
// does not `extends GenerationMiddleware`. Chat hooks use function-property
|
|
15
|
+
// syntax, so under `strictFunctionTypes` a narrowed-context subtype would be
|
|
16
|
+
// rejected; declaring it via inheritance would force method syntax and reopen
|
|
17
|
+
// a bivariance hole (a chat hook reading `ctx.messages` slotted where only a
|
|
18
|
+
// base context exists). Instead, the base context/info types are SUPERTYPES
|
|
19
|
+
// (fewer fields) and the chat context/info types are SUBTYPES (more fields),
|
|
20
|
+
// so a single value whose lifecycle hooks are authored against the base — like
|
|
21
|
+
// `otelMiddleware()` — satisfies `GenerationMiddleware & ChatMiddleware` by
|
|
22
|
+
// contravariance, while an arbitrary `ChatMiddleware` is NOT assignable to
|
|
23
|
+
// `GenerationMiddleware`.
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The activity an observability event describes.
|
|
27
|
+
*
|
|
28
|
+
* Mirrors the public surface a caller reaches for: `'chat'` for `chat()`, and
|
|
29
|
+
* the media kinds for the `generate*` activities. `'tts'` matches the speech
|
|
30
|
+
* adapter's kind (the public discriminator avoids inventing a parallel
|
|
31
|
+
* `'speech'`/`'text'` vocabulary). `otelMiddleware` maps each to its
|
|
32
|
+
* `gen_ai.operation.name`.
|
|
33
|
+
*/
|
|
34
|
+
export type GenerationActivity =
|
|
35
|
+
| 'chat'
|
|
36
|
+
| 'image'
|
|
37
|
+
| 'video'
|
|
38
|
+
| 'audio'
|
|
39
|
+
| 'tts'
|
|
40
|
+
| 'transcription'
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Stable context passed to every {@link GenerationMiddleware} hook. Created
|
|
44
|
+
* once per activity call and shared across the hooks of that call.
|
|
45
|
+
*
|
|
46
|
+
* Carries only fields every activity can honor. `ChatMiddlewareContext`
|
|
47
|
+
* structurally includes all of these plus chat-only state (messages,
|
|
48
|
+
* iteration, capabilities, …), which is why a chat middleware that reads those
|
|
49
|
+
* extra fields is not assignable to `GenerationMiddleware`.
|
|
50
|
+
*/
|
|
51
|
+
export interface GenerationMiddlewareContext<TContext = unknown> {
|
|
52
|
+
/**
|
|
53
|
+
* Stable id correlating the `onStart` / `onFinish` / `onError` / `onAbort`
|
|
54
|
+
* hooks of a single activity call.
|
|
55
|
+
*/
|
|
56
|
+
requestId: string
|
|
57
|
+
/** Which activity this call is. Discriminates media from chat. */
|
|
58
|
+
activity: GenerationActivity
|
|
59
|
+
/** Provider/adapter name (e.g. `"openai"`). Emitted as `gen_ai.system`. */
|
|
60
|
+
provider: string
|
|
61
|
+
/** Model id. Emitted as `gen_ai.request.model`. */
|
|
62
|
+
model: string
|
|
63
|
+
/**
|
|
64
|
+
* Provider-specific options passed to the activity, if any. Typed `unknown`
|
|
65
|
+
* because each activity's options are strongly typed per model; a supertype
|
|
66
|
+
* of `ChatMiddlewareContext`'s `modelOptions`.
|
|
67
|
+
*/
|
|
68
|
+
modelOptions?: unknown
|
|
69
|
+
/** Where the call originates. Always `'server'` for media activities. */
|
|
70
|
+
source: 'client' | 'server'
|
|
71
|
+
/** Generate a unique id with the given prefix. */
|
|
72
|
+
createId: (prefix: string) => string
|
|
73
|
+
/** Runtime context provided by the activity options, if any. */
|
|
74
|
+
context: TContext
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ===========================
|
|
78
|
+
// Hook payloads
|
|
79
|
+
// ===========================
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Token usage passed to {@link GenerationMiddleware.onUsage}. Kept as an
|
|
83
|
+
* interface extending `TokenUsage` to preserve declaration merging for this
|
|
84
|
+
* publicly exported type.
|
|
85
|
+
*/
|
|
86
|
+
export interface GenerationUsageInfo extends TokenUsage {}
|
|
87
|
+
|
|
88
|
+
/** Information passed to {@link GenerationMiddleware.onFinish}. */
|
|
89
|
+
export interface GenerationFinishInfo {
|
|
90
|
+
/** Wall-clock duration of the activity call, in milliseconds. */
|
|
91
|
+
duration: number
|
|
92
|
+
/** Unified usage, when the provider reported it. */
|
|
93
|
+
usage?: TokenUsage | undefined
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Information passed to {@link GenerationMiddleware.onAbort}. */
|
|
97
|
+
export interface GenerationAbortInfo {
|
|
98
|
+
/** The reason for the abort, if provided. */
|
|
99
|
+
reason?: string
|
|
100
|
+
/** Wall-clock duration until the abort, in milliseconds. */
|
|
101
|
+
duration: number
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Information passed to {@link GenerationMiddleware.onError}. */
|
|
105
|
+
export interface GenerationErrorInfo {
|
|
106
|
+
/** The thrown value (typically an `Error`). */
|
|
107
|
+
error: unknown
|
|
108
|
+
/** Wall-clock duration until the failure, in milliseconds. */
|
|
109
|
+
duration: number
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// ===========================
|
|
113
|
+
// Middleware interface
|
|
114
|
+
// ===========================
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Activity-agnostic, observe-only middleware.
|
|
118
|
+
*
|
|
119
|
+
* A thin lifecycle observer registerable on any activity via its `middleware`
|
|
120
|
+
* option. Unlike `ChatMiddleware` (which can also rewrite config, chunks, and
|
|
121
|
+
* tool calls), these hooks only observe — the right fit for the single
|
|
122
|
+
* request → response shape of media activities. Pass `otelMiddleware()` for
|
|
123
|
+
* OpenTelemetry, or implement the hooks directly for a custom backend.
|
|
124
|
+
*
|
|
125
|
+
* Hooks are awaited in registration order. A hook that throws PROPAGATES and
|
|
126
|
+
* fails the activity — matching `chat()` middleware semantics. Keep them cheap;
|
|
127
|
+
* they run inline with the request.
|
|
128
|
+
*
|
|
129
|
+
* Exactly one of `onFinish` / `onAbort` / `onError` fires per call.
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* ```ts
|
|
133
|
+
* import { generateImage } from '@tanstack/ai'
|
|
134
|
+
* import { otelMiddleware } from '@tanstack/ai/middlewares/otel'
|
|
135
|
+
* import { openaiImage } from '@tanstack/ai-openai'
|
|
136
|
+
* import { trace } from '@opentelemetry/api'
|
|
137
|
+
*
|
|
138
|
+
* await generateImage({
|
|
139
|
+
* adapter: openaiImage('gpt-image-1'),
|
|
140
|
+
* prompt: 'A serene mountain landscape at sunset',
|
|
141
|
+
* middleware: [otelMiddleware({ tracer: trace.getTracer('my-app') })],
|
|
142
|
+
* })
|
|
143
|
+
* ```
|
|
144
|
+
*/
|
|
145
|
+
export interface GenerationMiddleware<TContext = unknown> {
|
|
146
|
+
/** Optional name, surfaced in diagnostics. */
|
|
147
|
+
name?: string
|
|
148
|
+
/** Called before the adapter request begins. */
|
|
149
|
+
onStart?: (ctx: GenerationMiddlewareContext<TContext>) => void | Promise<void>
|
|
150
|
+
/** Called when the provider reports usage, before `onFinish`. */
|
|
151
|
+
onUsage?: (
|
|
152
|
+
ctx: GenerationMiddlewareContext<TContext>,
|
|
153
|
+
usage: GenerationUsageInfo,
|
|
154
|
+
) => void | Promise<void>
|
|
155
|
+
/** Called after the activity completes successfully. */
|
|
156
|
+
onFinish?: (
|
|
157
|
+
ctx: GenerationMiddlewareContext<TContext>,
|
|
158
|
+
info: GenerationFinishInfo,
|
|
159
|
+
) => void | Promise<void>
|
|
160
|
+
/** Called when the activity is aborted (e.g. an abandoned stream). */
|
|
161
|
+
onAbort?: (
|
|
162
|
+
ctx: GenerationMiddlewareContext<TContext>,
|
|
163
|
+
info: GenerationAbortInfo,
|
|
164
|
+
) => void | Promise<void>
|
|
165
|
+
/** Called when the activity throws before completing. */
|
|
166
|
+
onError?: (
|
|
167
|
+
ctx: GenerationMiddlewareContext<TContext>,
|
|
168
|
+
info: GenerationErrorInfo,
|
|
169
|
+
) => void | Promise<void>
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A `GenerationMiddleware` with a permissive context — for use as a constraint. */
|
|
173
|
+
export type AnyGenerationMiddleware = GenerationMiddleware<any>
|
package/src/index.ts
CHANGED
|
@@ -82,6 +82,10 @@ export {
|
|
|
82
82
|
// Tool call management
|
|
83
83
|
export { ToolCallManager } from './activities/chat/tools/tool-calls'
|
|
84
84
|
|
|
85
|
+
// Lazy tool discovery (name of the synthetic discovery tool, for custom
|
|
86
|
+
// message-compaction logic that needs to reference it)
|
|
87
|
+
export { DISCOVERY_TOOL_NAME } from './activities/chat/tools/lazy-tool-manager'
|
|
88
|
+
|
|
85
89
|
// Provider tool type
|
|
86
90
|
export type { ProviderTool } from './tools/provider-tool'
|
|
87
91
|
export { brandProviderTool } from './tools/provider-tool'
|
|
@@ -118,6 +122,21 @@ export type {
|
|
|
118
122
|
ErrorInfo,
|
|
119
123
|
} from './activities/chat/middleware/index'
|
|
120
124
|
|
|
125
|
+
// Base, activity-agnostic middleware. The observe-only superset that media
|
|
126
|
+
// activities accept via their `middleware` option; `ChatMiddleware` adds the
|
|
127
|
+
// chat-only hooks on top. Pure types only — the `otelMiddleware` value lives at
|
|
128
|
+
// `@tanstack/ai/middlewares/otel` so the root barrel never requires the
|
|
129
|
+
// optional `@opentelemetry/api` peer dependency.
|
|
130
|
+
export type {
|
|
131
|
+
GenerationMiddleware,
|
|
132
|
+
GenerationMiddlewareContext,
|
|
133
|
+
GenerationActivity,
|
|
134
|
+
GenerationUsageInfo,
|
|
135
|
+
GenerationFinishInfo,
|
|
136
|
+
GenerationAbortInfo,
|
|
137
|
+
GenerationErrorInfo,
|
|
138
|
+
AnyGenerationMiddleware,
|
|
139
|
+
} from './activities/middleware/index'
|
|
121
140
|
// Capability primitives + middleware builder
|
|
122
141
|
export {
|
|
123
142
|
createCapability,
|