@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
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
StandardJSONSchemaV1,
|
|
3
3
|
StandardSchemaV1,
|
|
4
4
|
} from '@standard-schema/spec'
|
|
5
|
+
import type { NullWideningMap } from '@tanstack/ai-utils'
|
|
5
6
|
import type { JSONSchema, SchemaInput } from '../../../types'
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -82,6 +83,22 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
|
82
83
|
)
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Result of {@link makeStructuredOutputCompatible}: the strict-ready schema plus
|
|
88
|
+
* a {@link NullWideningMap} recording every position where a `null` was
|
|
89
|
+
* synthesized, so the response can be un-widened before validation without
|
|
90
|
+
* re-deriving (or guessing) which nulls were synthetic.
|
|
91
|
+
*/
|
|
92
|
+
interface StructuredOutputConversion {
|
|
93
|
+
schema: JSONSchema
|
|
94
|
+
nullWidening: NullWideningMap | undefined
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Drop an empty map to `undefined` so leaf/no-op subtrees don't litter it. */
|
|
98
|
+
function pruneMap(map: NullWideningMap): NullWideningMap | undefined {
|
|
99
|
+
return Object.keys(map).length > 0 ? map : undefined
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
/**
|
|
86
103
|
* Transform a JSON schema to be compatible with OpenAI's structured output requirements.
|
|
87
104
|
* OpenAI requires:
|
|
@@ -89,59 +106,76 @@ export function isStandardSchema(schema: unknown): schema is StandardSchemaV1 {
|
|
|
89
106
|
* - Optional fields should have null added to their type union
|
|
90
107
|
* - additionalProperties must be false for objects
|
|
91
108
|
*
|
|
109
|
+
* Alongside the transformed schema it returns a {@link NullWideningMap} marking
|
|
110
|
+
* exactly the positions where `null` was added, so `undoNullWidening` can strip
|
|
111
|
+
* those synthesized nulls (and only those) from the provider's response.
|
|
112
|
+
*
|
|
92
113
|
* @param schema - JSON schema to transform
|
|
93
114
|
* @param originalRequired - Original required array (to know which fields were optional)
|
|
94
|
-
* @returns Transformed schema
|
|
115
|
+
* @returns Transformed schema + the null-widening map for the round trip
|
|
95
116
|
*/
|
|
96
117
|
function makeStructuredOutputCompatible(
|
|
97
118
|
schema: JSONSchema,
|
|
98
119
|
originalRequired: Array<string> = [],
|
|
99
|
-
):
|
|
120
|
+
): StructuredOutputConversion {
|
|
100
121
|
const result: JSONSchema = { ...schema }
|
|
122
|
+
const map: NullWideningMap = {}
|
|
101
123
|
|
|
102
124
|
// Handle object types
|
|
103
125
|
if (result.type === 'object' && result.properties) {
|
|
104
126
|
const properties: Record<string, JSONSchema> = { ...result.properties }
|
|
105
127
|
const allPropertyNames = Object.keys(properties)
|
|
128
|
+
const propertyMaps: Record<string, NullWideningMap> = {}
|
|
106
129
|
|
|
107
130
|
// Transform each property
|
|
108
131
|
for (const propName of allPropertyNames) {
|
|
109
132
|
const prop = properties[propName]
|
|
110
133
|
if (!prop) continue
|
|
111
134
|
const wasOptional = !originalRequired.includes(propName)
|
|
135
|
+
// `null` synthesized AT this property (the field itself can come back null).
|
|
136
|
+
let widenedHere = false
|
|
137
|
+
// Map describing widened positions INSIDE this property.
|
|
138
|
+
let childMap: NullWideningMap | undefined
|
|
112
139
|
|
|
113
140
|
// Recursively transform nested objects/arrays
|
|
114
141
|
if (prop.type === 'object' && prop.properties) {
|
|
115
|
-
const
|
|
116
|
-
prop,
|
|
117
|
-
prop.required || [],
|
|
118
|
-
)
|
|
142
|
+
const nested = makeStructuredOutputCompatible(prop, prop.required || [])
|
|
119
143
|
properties[propName] = wasOptional
|
|
120
|
-
? { ...
|
|
121
|
-
:
|
|
144
|
+
? { ...nested.schema, type: ['object', 'null'] }
|
|
145
|
+
: nested.schema
|
|
146
|
+
widenedHere = wasOptional
|
|
147
|
+
childMap = nested.nullWidening
|
|
122
148
|
} else if (prop.type === 'array' && prop.items) {
|
|
123
149
|
const items = Array.isArray(prop.items) ? prop.items[0] : prop.items
|
|
124
|
-
const
|
|
150
|
+
const nestedItems = items
|
|
151
|
+
? makeStructuredOutputCompatible(items, items.required || [])
|
|
152
|
+
: undefined
|
|
153
|
+
properties[propName] = {
|
|
125
154
|
...prop,
|
|
126
|
-
items: items
|
|
127
|
-
|
|
128
|
-
: prop.items,
|
|
155
|
+
items: nestedItems ? nestedItems.schema : prop.items,
|
|
156
|
+
...(wasOptional ? { type: ['array', 'null'] } : {}),
|
|
129
157
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
:
|
|
158
|
+
widenedHere = wasOptional
|
|
159
|
+
childMap = nestedItems?.nullWidening
|
|
160
|
+
? { items: nestedItems.nullWidening }
|
|
161
|
+
: undefined
|
|
133
162
|
} else if (wasOptional) {
|
|
134
|
-
// Make optional fields nullable by adding null to the type
|
|
163
|
+
// Make optional fields nullable by adding null to the type. Mark
|
|
164
|
+
// `widenedHere` only where we actually add `null`; a field already
|
|
165
|
+
// typed nullable (`.nullish()`) is left as-is and keeps its null.
|
|
135
166
|
if (prop.type && !Array.isArray(prop.type)) {
|
|
136
|
-
properties[propName] = {
|
|
137
|
-
|
|
138
|
-
type: [prop.type, 'null'],
|
|
139
|
-
}
|
|
167
|
+
properties[propName] = { ...prop, type: [prop.type, 'null'] }
|
|
168
|
+
widenedHere = true
|
|
140
169
|
} else if (Array.isArray(prop.type) && !prop.type.includes('null')) {
|
|
141
|
-
properties[propName] = {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
170
|
+
properties[propName] = { ...prop, type: [...prop.type, 'null'] }
|
|
171
|
+
widenedHere = true
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (widenedHere || childMap) {
|
|
176
|
+
propertyMaps[propName] = {
|
|
177
|
+
...(childMap ?? {}),
|
|
178
|
+
...(widenedHere ? { widened: true } : {}),
|
|
145
179
|
}
|
|
146
180
|
}
|
|
147
181
|
}
|
|
@@ -151,17 +185,23 @@ function makeStructuredOutputCompatible(
|
|
|
151
185
|
result.required = allPropertyNames
|
|
152
186
|
// additionalProperties must be false
|
|
153
187
|
result.additionalProperties = false
|
|
188
|
+
if (Object.keys(propertyMaps).length > 0) map.properties = propertyMaps
|
|
154
189
|
}
|
|
155
190
|
|
|
156
191
|
// Handle array types with object items
|
|
157
192
|
if (result.type === 'array' && result.items) {
|
|
158
193
|
const items = Array.isArray(result.items) ? result.items[0] : result.items
|
|
159
194
|
if (items) {
|
|
160
|
-
|
|
195
|
+
const nestedItems = makeStructuredOutputCompatible(
|
|
196
|
+
items,
|
|
197
|
+
items.required || [],
|
|
198
|
+
)
|
|
199
|
+
result.items = nestedItems.schema
|
|
200
|
+
if (nestedItems.nullWidening) map.items = nestedItems.nullWidening
|
|
161
201
|
}
|
|
162
202
|
}
|
|
163
203
|
|
|
164
|
-
return result
|
|
204
|
+
return { schema: result, nullWidening: pruneMap(map) }
|
|
165
205
|
}
|
|
166
206
|
|
|
167
207
|
/**
|
|
@@ -179,6 +219,48 @@ export interface ConvertSchemaOptions {
|
|
|
179
219
|
forStructuredOutput?: boolean
|
|
180
220
|
}
|
|
181
221
|
|
|
222
|
+
/**
|
|
223
|
+
* Normalize any supported schema input to a typed, UN-widened `JSONSchema` —
|
|
224
|
+
* the shared first half of conversion, before any structured-output widening.
|
|
225
|
+
*
|
|
226
|
+
* - Standard JSON Schemas are rebuilt structurally (dropping `$schema`, which
|
|
227
|
+
* LLM providers ignore) and given the explicit `type`/`properties`/`required`
|
|
228
|
+
* defaults object shapes need downstream.
|
|
229
|
+
* - Plain `JSONSchema` inputs are rebuilt into the typed view; non-object inputs
|
|
230
|
+
* are surfaced untouched (they can't be widened).
|
|
231
|
+
* - Standard Schema validators lacking a `~standard.jsonSchema` converter throw
|
|
232
|
+
* with actionable guidance, rather than shipping `{ '~standard': … }` to the
|
|
233
|
+
* provider and producing an opaque downstream error.
|
|
234
|
+
*/
|
|
235
|
+
function toTypedJsonSchema(schema: SchemaInput): JSONSchema | undefined {
|
|
236
|
+
if (isStandardJSONSchema(schema)) {
|
|
237
|
+
const jsonSchema = schema['~standard'].jsonSchema.input({
|
|
238
|
+
target: 'draft-07',
|
|
239
|
+
})
|
|
240
|
+
const result: JSONSchema = toJsonSchema(jsonSchema)
|
|
241
|
+
if ('properties' in result && !result.type) result.type = 'object'
|
|
242
|
+
if (result.type === 'object' && !('properties' in result)) {
|
|
243
|
+
result.properties = {}
|
|
244
|
+
}
|
|
245
|
+
if (result.type === 'object' && !('required' in result)) {
|
|
246
|
+
result.required = []
|
|
247
|
+
}
|
|
248
|
+
return result
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isStandardSchema(schema)) {
|
|
252
|
+
throw new Error(
|
|
253
|
+
'Schema is a Standard Schema validator but does not expose a JSON Schema ' +
|
|
254
|
+
'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +
|
|
255
|
+
'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +
|
|
256
|
+
'`@valibot/to-json-schema` before passing it as `outputSchema`.',
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
if (typeof schema !== 'object') return schema
|
|
261
|
+
return toJsonSchema(schema)
|
|
262
|
+
}
|
|
263
|
+
|
|
182
264
|
/**
|
|
183
265
|
* Converts a Standard JSON Schema compliant schema or plain JSONSchema to JSON Schema format
|
|
184
266
|
* compatible with LLM providers.
|
|
@@ -247,77 +329,48 @@ export function convertSchemaToJsonSchema(
|
|
|
247
329
|
|
|
248
330
|
const { forStructuredOutput = false } = options
|
|
249
331
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// drops the `$schema` key which LLM providers don't need.
|
|
259
|
-
let result: JSONSchema = toJsonSchema(jsonSchema)
|
|
260
|
-
|
|
261
|
-
// Ensure object schemas always have type: "object"
|
|
262
|
-
// If it has properties (even empty), it should be an object type
|
|
263
|
-
if ('properties' in result && !result.type) {
|
|
264
|
-
result.type = 'object'
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Ensure properties exists for object types (even if empty)
|
|
268
|
-
if (result.type === 'object' && !('properties' in result)) {
|
|
269
|
-
result.properties = {}
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
// Ensure required exists for object types (even if empty array)
|
|
273
|
-
if (result.type === 'object' && !('required' in result)) {
|
|
274
|
-
result.required = []
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
// Apply structured output transformation if requested
|
|
278
|
-
if (forStructuredOutput) {
|
|
279
|
-
result = makeStructuredOutputCompatible(result, result.required || [])
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
return result
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
// Detect Standard Schema validators (Zod, ArkType, Valibot, …) that don't
|
|
286
|
-
// expose a `~standard.jsonSchema` converter. These would otherwise fall
|
|
287
|
-
// through to the JSONSchema pass-through below and ship `{ '~standard': … }`
|
|
288
|
-
// straight to the LLM provider, producing an opaque downstream error. Fail
|
|
289
|
-
// fast with actionable guidance instead.
|
|
290
|
-
if (isStandardSchema(schema)) {
|
|
291
|
-
throw new Error(
|
|
292
|
-
'Schema is a Standard Schema validator but does not expose a JSON Schema ' +
|
|
293
|
-
'converter on `~standard.jsonSchema`. Use Zod v4.2+, ArkType v2.1.28+, ' +
|
|
294
|
-
'or wrap a Valibot schema with `toStandardJsonSchema()` from ' +
|
|
295
|
-
'`@valibot/to-json-schema` before passing it as `outputSchema`.',
|
|
296
|
-
)
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
// If it's not a Standard JSON Schema, assume it's already a JSONSchema and pass through
|
|
300
|
-
// Still apply structured output transformation if requested
|
|
301
|
-
|
|
302
|
-
// At this branch, `schema` is the plain `JSONSchema` arm of `SchemaInput`
|
|
303
|
-
// (the two `~standard` arms were handled above). When no transformation
|
|
304
|
-
// is requested we pass the schema through by reference to preserve
|
|
305
|
-
// identity for callers that compare via `===`.
|
|
306
|
-
if (typeof schema !== 'object') {
|
|
307
|
-
// The SchemaInput union is object-shaped on every arm; if we ever hit a
|
|
308
|
-
// non-object here, propagate it untouched and let the downstream
|
|
309
|
-
// provider error loudly rather than silently widen.
|
|
332
|
+
// Plain-JSONSchema passthrough: with no widening requested, return the schema
|
|
333
|
+
// by reference so callers comparing via `===` keep identity. Only the widening
|
|
334
|
+
// path needs the rebuilt, normalized view from `toTypedJsonSchema`.
|
|
335
|
+
if (
|
|
336
|
+
!forStructuredOutput &&
|
|
337
|
+
!isStandardJSONSchema(schema) &&
|
|
338
|
+
!isStandardSchema(schema)
|
|
339
|
+
) {
|
|
310
340
|
return schema
|
|
311
341
|
}
|
|
312
342
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
343
|
+
const base = toTypedJsonSchema(schema)
|
|
344
|
+
// Non-object inputs can't be widened; surface them untouched.
|
|
345
|
+
if (!base || typeof base !== 'object') return base
|
|
346
|
+
if (!forStructuredOutput) return base
|
|
347
|
+
return makeStructuredOutputCompatible(base, base.required || []).schema
|
|
348
|
+
}
|
|
319
349
|
|
|
320
|
-
|
|
350
|
+
/**
|
|
351
|
+
* Convert a schema for structured output AND capture the {@link NullWideningMap}
|
|
352
|
+
* recording every `null` the strict-mode widening synthesized. The map lets the
|
|
353
|
+
* caller undo that widening on the provider's response (via `undoNullWidening`)
|
|
354
|
+
* before validating against the original schema — optional fields read back as
|
|
355
|
+
* absent while genuine `.nullable()` nulls survive. The map is `undefined` when
|
|
356
|
+
* the schema isn't a widenable object or when no field needed widening.
|
|
357
|
+
*/
|
|
358
|
+
export function convertSchemaForStructuredOutput(
|
|
359
|
+
schema: SchemaInput | undefined,
|
|
360
|
+
): {
|
|
361
|
+
jsonSchema: JSONSchema | undefined
|
|
362
|
+
nullWideningMap: NullWideningMap | undefined
|
|
363
|
+
} {
|
|
364
|
+
if (!schema) return { jsonSchema: undefined, nullWideningMap: undefined }
|
|
365
|
+
const base = toTypedJsonSchema(schema)
|
|
366
|
+
if (!base || typeof base !== 'object') {
|
|
367
|
+
return { jsonSchema: base, nullWideningMap: undefined }
|
|
368
|
+
}
|
|
369
|
+
const { schema: jsonSchema, nullWidening } = makeStructuredOutputCompatible(
|
|
370
|
+
base,
|
|
371
|
+
base.required || [],
|
|
372
|
+
)
|
|
373
|
+
return { jsonSchema, nullWideningMap: nullWidening }
|
|
321
374
|
}
|
|
322
375
|
|
|
323
376
|
/**
|
|
@@ -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',
|