@tanstack/ai 0.43.1 → 0.44.1

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.
Files changed (73) hide show
  1. package/dist/esm/activities/chat/index.js +18 -0
  2. package/dist/esm/activities/chat/index.js.map +1 -1
  3. package/dist/esm/activities/chat/messages.js +21 -8
  4. package/dist/esm/activities/chat/messages.js.map +1 -1
  5. package/dist/esm/activities/embed/adapter.d.ts +69 -0
  6. package/dist/esm/activities/embed/adapter.js +23 -0
  7. package/dist/esm/activities/embed/adapter.js.map +1 -0
  8. package/dist/esm/activities/embed/index.d.ts +117 -0
  9. package/dist/esm/activities/embed/index.js +166 -0
  10. package/dist/esm/activities/embed/index.js.map +1 -0
  11. package/dist/esm/activities/error-payload.d.ts +8 -0
  12. package/dist/esm/activities/error-payload.js +29 -17
  13. package/dist/esm/activities/error-payload.js.map +1 -1
  14. package/dist/esm/activities/generateAudio/index.d.ts +12 -0
  15. package/dist/esm/activities/generateAudio/index.js +19 -6
  16. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  17. package/dist/esm/activities/generateImage/index.d.ts +12 -0
  18. package/dist/esm/activities/generateImage/index.js +21 -7
  19. package/dist/esm/activities/generateImage/index.js.map +1 -1
  20. package/dist/esm/activities/generateSpeech/index.d.ts +17 -1
  21. package/dist/esm/activities/generateSpeech/index.js +19 -6
  22. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  23. package/dist/esm/activities/generateTranscription/index.d.ts +17 -1
  24. package/dist/esm/activities/generateTranscription/index.js +19 -6
  25. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  26. package/dist/esm/activities/generateVideo/index.d.ts +18 -0
  27. package/dist/esm/activities/generateVideo/index.js +54 -15
  28. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  29. package/dist/esm/activities/index.d.ts +8 -2
  30. package/dist/esm/activities/index.js +11 -7
  31. package/dist/esm/activities/middleware/types.d.ts +1 -1
  32. package/dist/esm/activities/rerank/adapter.d.ts +63 -0
  33. package/dist/esm/activities/rerank/adapter.js +23 -0
  34. package/dist/esm/activities/rerank/adapter.js.map +1 -0
  35. package/dist/esm/activities/rerank/index.d.ts +92 -0
  36. package/dist/esm/activities/rerank/index.js +163 -0
  37. package/dist/esm/activities/rerank/index.js.map +1 -0
  38. package/dist/esm/activities/summarize/index.d.ts +17 -1
  39. package/dist/esm/activities/summarize/index.js +19 -5
  40. package/dist/esm/activities/summarize/index.js.map +1 -1
  41. package/dist/esm/index.d.ts +7 -2
  42. package/dist/esm/index.js +5 -1
  43. package/dist/esm/middlewares/otel.js +20 -2
  44. package/dist/esm/middlewares/otel.js.map +1 -1
  45. package/dist/esm/types.d.ts +195 -0
  46. package/dist/esm/utilities/activity-abort.d.ts +53 -0
  47. package/dist/esm/utilities/activity-abort.js +150 -0
  48. package/dist/esm/utilities/activity-abort.js.map +1 -0
  49. package/dist/esm/utilities/embedding-input.d.ts +32 -0
  50. package/dist/esm/utilities/embedding-input.js +61 -0
  51. package/dist/esm/utilities/embedding-input.js.map +1 -0
  52. package/package.json +3 -3
  53. package/skills/ai-core/media-generation/SKILL.md +55 -23
  54. package/src/activities/chat/index.ts +27 -2
  55. package/src/activities/chat/messages.ts +30 -1
  56. package/src/activities/embed/adapter.ts +112 -0
  57. package/src/activities/embed/index.ts +318 -0
  58. package/src/activities/error-payload.ts +41 -9
  59. package/src/activities/generateAudio/index.ts +47 -5
  60. package/src/activities/generateImage/index.ts +48 -5
  61. package/src/activities/generateSpeech/index.ts +52 -9
  62. package/src/activities/generateTranscription/index.ts +52 -9
  63. package/src/activities/generateVideo/index.ts +131 -33
  64. package/src/activities/index.ts +44 -0
  65. package/src/activities/middleware/types.ts +2 -0
  66. package/src/activities/rerank/adapter.ts +90 -0
  67. package/src/activities/rerank/index.ts +302 -0
  68. package/src/activities/summarize/index.ts +59 -19
  69. package/src/index.ts +19 -0
  70. package/src/middlewares/otel.ts +38 -3
  71. package/src/types.ts +219 -0
  72. package/src/utilities/activity-abort.ts +197 -0
  73. package/src/utilities/embedding-input.ts +83 -0
@@ -184,8 +184,12 @@ function buildUserOrToolMessage(uiMessage: UIMessage): ModelMessage {
184
184
  }
185
185
 
186
186
  return {
187
+ id: uiMessage.id,
187
188
  role: uiMessage.role as 'user' | 'assistant' | 'tool',
188
189
  content: collapseContentParts(contentParts),
190
+ ...(uiMessage.createdAt !== undefined && {
191
+ createdAt: uiMessage.createdAt,
192
+ }),
189
193
  }
190
194
  }
191
195
 
@@ -228,6 +232,8 @@ function isToolCallIncluded(part: ToolCallPart): boolean {
228
232
  * result is emitted as a tool message.
229
233
  */
230
234
  function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
235
+ // A single UI message can fan out into several model messages. Keep the
236
+ // shared UI id on each one so persistence can retain the original identity.
231
237
  const messageList: Array<ModelMessage> = []
232
238
  let current = createSegment()
233
239
  let pendingThinking: Array<{ content: string; signature?: string }> = []
@@ -244,10 +250,14 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
244
250
 
245
251
  if (hasContent || hasToolCalls) {
246
252
  messageList.push({
253
+ id: uiMessage.id,
247
254
  role: 'assistant',
248
255
  content,
249
256
  ...(hasToolCalls && { toolCalls: current.toolCalls }),
250
257
  ...(pendingThinking.length > 0 && { thinking: pendingThinking }),
258
+ ...(uiMessage.createdAt !== undefined && {
259
+ createdAt: uiMessage.createdAt,
260
+ }),
251
261
  })
252
262
  pendingThinking = []
253
263
  }
@@ -288,9 +298,13 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
288
298
  !emittedToolResultIds.has(part.toolCallId)
289
299
  ) {
290
300
  messageList.push({
301
+ id: uiMessage.id,
291
302
  role: 'tool',
292
303
  content: part.content,
293
304
  toolCallId: part.toolCallId,
305
+ ...(uiMessage.createdAt !== undefined && {
306
+ createdAt: uiMessage.createdAt,
307
+ }),
294
308
  })
295
309
  emittedToolResultIds.add(part.toolCallId)
296
310
  }
@@ -347,9 +361,13 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
347
361
  // emit the concrete output regardless of approval metadata.
348
362
  if (part.output !== undefined && !emittedToolResultIds.has(part.id)) {
349
363
  messageList.push({
364
+ id: uiMessage.id,
350
365
  role: 'tool',
351
366
  content: normalizeToolResult(part.output),
352
367
  toolCallId: part.id,
368
+ ...(uiMessage.createdAt !== undefined && {
369
+ createdAt: uiMessage.createdAt,
370
+ }),
353
371
  })
354
372
  emittedToolResultIds.add(part.id)
355
373
  }
@@ -363,6 +381,7 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
363
381
  ) {
364
382
  const approved = part.approval.approved
365
383
  messageList.push({
384
+ id: uiMessage.id,
366
385
  role: 'tool',
367
386
  content: JSON.stringify({
368
387
  approved,
@@ -372,6 +391,9 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
372
391
  : 'User denied this action',
373
392
  }),
374
393
  toolCallId: part.id,
394
+ ...(uiMessage.createdAt !== undefined && {
395
+ createdAt: uiMessage.createdAt,
396
+ }),
375
397
  })
376
398
  emittedToolResultIds.add(part.id)
377
399
  }
@@ -380,8 +402,12 @@ function buildAssistantMessages(uiMessage: UIMessage): Array<ModelMessage> {
380
402
  // If no messages were produced (e.g., empty parts), emit a minimal assistant message
381
403
  if (messageList.length === 0) {
382
404
  messageList.push({
405
+ id: uiMessage.id,
383
406
  role: 'assistant',
384
407
  content: null,
408
+ ...(uiMessage.createdAt !== undefined && {
409
+ createdAt: uiMessage.createdAt,
410
+ }),
385
411
  })
386
412
  }
387
413
 
@@ -469,6 +495,9 @@ export function modelMessageToUIMessage(
469
495
  id: id || generateMessageId(),
470
496
  role: modelMessage.role === 'tool' ? 'assistant' : modelMessage.role,
471
497
  parts,
498
+ ...(modelMessage.createdAt !== undefined && {
499
+ createdAt: modelMessage.createdAt,
500
+ }),
472
501
  }
473
502
  }
474
503
 
@@ -659,7 +688,7 @@ export function normalizeToUIMessage(
659
688
  // ModelMessage - convert to UIMessage
660
689
  return {
661
690
  ...modelMessageToUIMessage(message, generateId()),
662
- createdAt: new Date(),
691
+ createdAt: message.createdAt ?? new Date(),
663
692
  }
664
693
  }
665
694
  }
@@ -0,0 +1,112 @@
1
+ import type {
2
+ EmbeddingModelInputModalitiesByName,
3
+ EmbeddingOptions,
4
+ EmbeddingResult,
5
+ } from '../../types'
6
+
7
+ /**
8
+ * Configuration for embedding adapter instances
9
+ */
10
+ export interface EmbeddingAdapterConfig {
11
+ apiKey?: string
12
+ baseUrl?: string
13
+ timeout?: number
14
+ maxRetries?: number
15
+ headers?: Record<string, string>
16
+ }
17
+
18
+ /**
19
+ * Embedding adapter interface with pre-resolved generics.
20
+ *
21
+ * An adapter is created by a provider function: `provider('model')` → `adapter`
22
+ * All type resolution happens at the provider call site, not in this interface.
23
+ *
24
+ * Generic parameters:
25
+ * - TModel: The specific model name (e.g., 'text-embedding-3-small')
26
+ * - TProviderOptions: Base provider-specific options (already resolved)
27
+ * - TModelProviderOptionsByName: Map from model name to its specific provider options
28
+ * - TModelInputModalitiesByName: Map from model name to the input modalities it
29
+ * accepts (constrains the `input` item types at compile time)
30
+ */
31
+ export interface EmbeddingAdapter<
32
+ TModel extends string = string,
33
+ TProviderOptions extends object = Record<string, unknown>,
34
+ TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
35
+ TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =
36
+ EmbeddingModelInputModalitiesByName,
37
+ > {
38
+ /** Discriminator for adapter kind */
39
+ readonly kind: 'embedding'
40
+ /** Adapter name identifier */
41
+ readonly name: string
42
+ /** The model this adapter is configured for */
43
+ readonly model: TModel
44
+
45
+ /**
46
+ * @internal Type-only properties for inference. Not assigned at runtime.
47
+ */
48
+ '~types': {
49
+ providerOptions: TProviderOptions
50
+ modelProviderOptionsByName: TModelProviderOptionsByName
51
+ modelInputModalitiesByName: TModelInputModalitiesByName
52
+ }
53
+
54
+ /**
55
+ * Generate embeddings for the input items (one vector per item)
56
+ */
57
+ createEmbeddings: (
58
+ options: EmbeddingOptions<TProviderOptions>,
59
+ ) => Promise<EmbeddingResult>
60
+ }
61
+
62
+ /**
63
+ * An EmbeddingAdapter with any/unknown type parameters.
64
+ * Useful as a constraint in generic functions and interfaces.
65
+ */
66
+ export type AnyEmbeddingAdapter = EmbeddingAdapter<any, any, any, any>
67
+
68
+ /**
69
+ * Abstract base class for embedding adapters.
70
+ * Extend this class to implement an embedding adapter for a specific provider.
71
+ *
72
+ * Generic parameters match EmbeddingAdapter - all pre-resolved by the provider function.
73
+ */
74
+ export abstract class BaseEmbeddingAdapter<
75
+ TModel extends string = string,
76
+ TProviderOptions extends object = Record<string, unknown>,
77
+ TModelProviderOptionsByName extends Record<string, any> = Record<string, any>,
78
+ TModelInputModalitiesByName extends EmbeddingModelInputModalitiesByName =
79
+ EmbeddingModelInputModalitiesByName,
80
+ > implements EmbeddingAdapter<
81
+ TModel,
82
+ TProviderOptions,
83
+ TModelProviderOptionsByName,
84
+ TModelInputModalitiesByName
85
+ > {
86
+ readonly kind = 'embedding' as const
87
+ abstract readonly name: string
88
+ readonly model: TModel
89
+
90
+ // Type-only property - never assigned at runtime
91
+ declare '~types': {
92
+ providerOptions: TProviderOptions
93
+ modelProviderOptionsByName: TModelProviderOptionsByName
94
+ modelInputModalitiesByName: TModelInputModalitiesByName
95
+ }
96
+
97
+ protected config: EmbeddingAdapterConfig
98
+
99
+ constructor(model: TModel, config: EmbeddingAdapterConfig = {}) {
100
+ this.config = config
101
+ this.model = model
102
+ }
103
+
104
+ abstract createEmbeddings(
105
+ options: EmbeddingOptions<TProviderOptions>,
106
+ ): Promise<EmbeddingResult>
107
+
108
+ protected generateId(prefix?: string): string {
109
+ const p = prefix ?? this.name
110
+ return `${p}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
111
+ }
112
+ }
@@ -0,0 +1,318 @@
1
+ /**
2
+ * Embed Activity
3
+ *
4
+ * Generates embedding vectors from text and (for multimodal models) image
5
+ * inputs. This is a self-contained module with implementation, types, and JSDoc.
6
+ */
7
+
8
+ import { aiEventClient } from '@tanstack/ai-event-client'
9
+ import { resolveDebugOption } from '../../logger/resolve'
10
+ import {
11
+ createGenerationContext,
12
+ runGenerationError,
13
+ runGenerationFinish,
14
+ runGenerationStart,
15
+ runGenerationUsage,
16
+ } from '../middleware/run'
17
+ import { countEmbeddingInputModalities } from '../../utilities/embedding-input'
18
+ import type { InternalLogger } from '../../logger/internal-logger'
19
+ import type { DebugOption } from '../../logger/types'
20
+ import type { GenerationMiddleware } from '../middleware/types'
21
+ import type { EmbeddingAdapter } from './adapter'
22
+ import type {
23
+ EmbeddingInputItem,
24
+ EmbeddingInputItemFor,
25
+ EmbeddingResult,
26
+ } from '../../types'
27
+
28
+ // ===========================
29
+ // Activity Kind
30
+ // ===========================
31
+
32
+ /** The adapter kind this activity handles */
33
+ export const kind = 'embedding' as const
34
+
35
+ // ===========================
36
+ // Type Extraction Helpers
37
+ // ===========================
38
+
39
+ /**
40
+ * Extract model-specific provider options from an EmbeddingAdapter via ~types.
41
+ * If the model has specific options defined in ModelProviderOptions (and not just via index signature),
42
+ * use those; otherwise fall back to base provider options.
43
+ */
44
+ export type EmbedProviderOptionsForModel<TAdapter, TModel extends string> =
45
+ TAdapter extends EmbeddingAdapter<
46
+ any,
47
+ infer BaseOptions,
48
+ infer ModelOptions,
49
+ any
50
+ >
51
+ ? string extends keyof ModelOptions
52
+ ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions
53
+ BaseOptions
54
+ : // ModelOptions has explicit keys - check if TModel is one of them
55
+ TModel extends keyof ModelOptions
56
+ ? ModelOptions[TModel]
57
+ : BaseOptions
58
+ : object
59
+
60
+ /**
61
+ * Extract the input type a model accepts from an EmbeddingAdapter via ~types.
62
+ * Adapters declare a per-model input-modality map; models in the map get an
63
+ * `input` narrowed to their supported item types (text-only models accept
64
+ * `string | TextPart`), so unsupported items fail at compile time. Adapters
65
+ * without a map fall back to the full EmbeddingInputItem union.
66
+ */
67
+ export type EmbeddingInputForModel<TAdapter, TModel extends string> =
68
+ TAdapter extends EmbeddingAdapter<any, any, any, infer ModsByName>
69
+ ? string extends keyof ModsByName
70
+ ? // No explicit map - accept the full union
71
+ EmbeddingInputItem | Array<EmbeddingInputItem>
72
+ : TModel extends keyof ModsByName
73
+ ?
74
+ | EmbeddingInputItemFor<ModsByName[TModel][number]>
75
+ | Array<EmbeddingInputItemFor<ModsByName[TModel][number]>>
76
+ : EmbeddingInputItem | Array<EmbeddingInputItem>
77
+ : EmbeddingInputItem | Array<EmbeddingInputItem>
78
+
79
+ // ===========================
80
+ // Activity Options Type
81
+ // ===========================
82
+
83
+ /**
84
+ * Options for the embed activity.
85
+ * The model is extracted from the adapter's model property.
86
+ *
87
+ * @template TAdapter - The embedding adapter type
88
+ */
89
+ export type EmbedOptions<
90
+ TAdapter extends EmbeddingAdapter<string, any, any, any>,
91
+ > = {
92
+ /** The embedding adapter to use (must be created with a model) */
93
+ adapter: TAdapter & { kind: typeof kind }
94
+ /**
95
+ * What to embed: a single item or an array of items. Each item in the array
96
+ * produces exactly one vector. An item is a plain string, a text part, an
97
+ * image part, or — for models that embed text and image together — a fused
98
+ * item written as a nested array of parts (`[textPart, imagePart]`), the
99
+ * same `Array<ContentPart>` shape chat messages use. The accepted item types
100
+ * are narrowed per model via the adapter's input-modality map.
101
+ */
102
+ input: EmbeddingInputForModel<TAdapter, TAdapter['model']>
103
+ /**
104
+ * Requested output dimensionality. Supported by models with Matryoshka /
105
+ * configurable dimensions; adapters for fixed-dimension models throw a
106
+ * clear runtime error when this is set.
107
+ */
108
+ dimensions?: number
109
+ /**
110
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
111
+ * silence everything including errors, or a `DebugConfig` object for granular
112
+ * control and/or a custom `Logger`.
113
+ */
114
+ debug?: DebugOption
115
+ /**
116
+ * Observe-only middleware notified on start, usage, success, and error. Pass
117
+ * `otelMiddleware()` to emit OpenTelemetry spans, or implement the
118
+ * `GenerationMiddleware` contract for a custom backend.
119
+ */
120
+ middleware?: Array<GenerationMiddleware>
121
+ } & ({} extends EmbedProviderOptionsForModel<TAdapter, TAdapter['model']>
122
+ ? {
123
+ /** Provider-specific options for embedding generation */ modelOptions?: EmbedProviderOptionsForModel<
124
+ TAdapter,
125
+ TAdapter['model']
126
+ >
127
+ }
128
+ : {
129
+ /** Provider-specific options for embedding generation */ modelOptions: EmbedProviderOptionsForModel<
130
+ TAdapter,
131
+ TAdapter['model']
132
+ >
133
+ })
134
+
135
+ function createId(prefix: string): string {
136
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
137
+ }
138
+
139
+ // ===========================
140
+ // Activity Implementation
141
+ // ===========================
142
+
143
+ /**
144
+ * Embed activity - generates embedding vectors from text and image inputs.
145
+ *
146
+ * Accepts a single item or an array of items; the result always carries an
147
+ * `embeddings` array with one vector per input item, in input order.
148
+ *
149
+ * @example Embed a single text
150
+ * ```ts
151
+ * import { embed } from '@tanstack/ai'
152
+ * import { openaiEmbedding } from '@tanstack/ai-openai'
153
+ *
154
+ * const result = await embed({
155
+ * adapter: openaiEmbedding('text-embedding-3-small'),
156
+ * input: 'a red guitar',
157
+ * })
158
+ *
159
+ * console.log(result.embeddings[0].vector)
160
+ * ```
161
+ *
162
+ * @example Batch with requested dimensions
163
+ * ```ts
164
+ * const result = await embed({
165
+ * adapter: openaiEmbedding('text-embedding-3-large'),
166
+ * input: ['a red guitar', 'a blue drum kit'],
167
+ * dimensions: 1024,
168
+ * })
169
+ * ```
170
+ *
171
+ * @example Multimodal embedding (text + image fused into one vector)
172
+ * ```ts
173
+ * import { cohereEmbedding } from '@tanstack/ai-cohere'
174
+ *
175
+ * // A nested array of parts fuses them into a single vector. The outer array
176
+ * // is the item list, so this embeds one fused item into one vector.
177
+ * const result = await embed({
178
+ * adapter: cohereEmbedding('embed-v4.0'),
179
+ * input: [
180
+ * [
181
+ * { type: 'text', content: 'product photo' },
182
+ * { type: 'image', source: { type: 'data', value: base64, mimeType: 'image/png' } },
183
+ * ],
184
+ * ],
185
+ * modelOptions: { inputType: 'search_document' },
186
+ * })
187
+ * ```
188
+ */
189
+ export async function embed<
190
+ TAdapter extends EmbeddingAdapter<string, any, any, any>,
191
+ >(options: EmbedOptions<TAdapter>): Promise<EmbeddingResult> {
192
+ const { adapter, middleware } = options
193
+ const model = adapter.model
194
+ const requestId = createId('embedding')
195
+ const startTime = Date.now()
196
+ const logger: InternalLogger = resolveDebugOption(options.debug)
197
+ const modelOptions = (options as { modelOptions?: Record<string, unknown> })
198
+ .modelOptions
199
+
200
+ // Normalize once: adapters always receive an array of items.
201
+ const inputItems: Array<EmbeddingInputItem> = Array.isArray(options.input)
202
+ ? options.input
203
+ : [options.input]
204
+ const { textInputCount, imageInputCount } =
205
+ countEmbeddingInputModalities(inputItems)
206
+
207
+ const mwCtx = createGenerationContext({
208
+ requestId,
209
+ activity: 'embedding',
210
+ provider: adapter.name,
211
+ model,
212
+ modelOptions,
213
+ createId,
214
+ })
215
+
216
+ await runGenerationStart(middleware, mwCtx)
217
+
218
+ aiEventClient.emit('embedding:request:started', {
219
+ requestId,
220
+ provider: adapter.name,
221
+ model,
222
+ inputCount: inputItems.length,
223
+ textInputCount,
224
+ imageInputCount,
225
+ dimensions: options.dimensions,
226
+ modelOptions,
227
+ timestamp: startTime,
228
+ })
229
+
230
+ logger.request(`activity=embed provider=${adapter.name} model=${model}`, {
231
+ provider: adapter.name,
232
+ model,
233
+ })
234
+
235
+ try {
236
+ const result = await adapter.createEmbeddings({
237
+ model,
238
+ input: inputItems,
239
+ dimensions: options.dimensions,
240
+ modelOptions,
241
+ logger,
242
+ })
243
+ const duration = Date.now() - startTime
244
+
245
+ aiEventClient.emit('embedding:request:completed', {
246
+ requestId,
247
+ provider: adapter.name,
248
+ model,
249
+ embeddingCount: result.embeddings.length,
250
+ dimensions: result.embeddings[0]?.vector.length,
251
+ duration,
252
+ modelOptions,
253
+ timestamp: Date.now(),
254
+ })
255
+
256
+ logger.output(`activity=embed count=${result.embeddings.length}`, {
257
+ embeddingCount: result.embeddings.length,
258
+ })
259
+
260
+ if (result.usage) {
261
+ aiEventClient.emit('embedding:usage', {
262
+ requestId,
263
+ model,
264
+ usage: result.usage,
265
+ timestamp: Date.now(),
266
+ })
267
+ await runGenerationUsage(middleware, mwCtx, result.usage)
268
+ }
269
+ await runGenerationFinish(middleware, mwCtx, {
270
+ duration,
271
+ usage: result.usage,
272
+ })
273
+
274
+ return result
275
+ } catch (error) {
276
+ const duration = Date.now() - startTime
277
+ const err = error as Error
278
+ aiEventClient.emit('embedding:request:error', {
279
+ requestId,
280
+ provider: adapter.name,
281
+ model,
282
+ error: { message: err.message, name: err.name },
283
+ duration,
284
+ modelOptions,
285
+ timestamp: Date.now(),
286
+ })
287
+ await runGenerationError(middleware, mwCtx, {
288
+ error,
289
+ duration,
290
+ })
291
+ logger.errors('embed activity failed', {
292
+ error,
293
+ source: 'embed',
294
+ })
295
+ throw error
296
+ }
297
+ }
298
+
299
+ // ===========================
300
+ // Options Factory
301
+ // ===========================
302
+
303
+ /**
304
+ * Create typed options for the embed() function without executing.
305
+ */
306
+ export function createEmbedOptions<
307
+ TAdapter extends EmbeddingAdapter<string, any, any, any>,
308
+ >(options: EmbedOptions<TAdapter>): EmbedOptions<TAdapter> {
309
+ return options
310
+ }
311
+
312
+ // Re-export adapter types
313
+ export type {
314
+ EmbeddingAdapter,
315
+ EmbeddingAdapterConfig,
316
+ AnyEmbeddingAdapter,
317
+ } from './adapter'
318
+ export { BaseEmbeddingAdapter } from './adapter'
@@ -18,6 +18,21 @@ const ABORT_ERROR_NAMES = new Set([
18
18
  'RequestAbortedError',
19
19
  ])
20
20
 
21
+ /**
22
+ * True when a thrown value is an abort-shaped error (DOM `AbortError`, OpenAI
23
+ * `APIUserAbortError`, OpenRouter `RequestAbortedError`) — i.e. user-initiated
24
+ * cancellation rather than a genuine failure. Matches on the error `name` so
25
+ * callers can discriminate aborts without depending on a signal's state or on
26
+ * provider-specific message strings.
27
+ */
28
+ export function isAbortShapedError(error: unknown): boolean {
29
+ if (error && typeof error === 'object') {
30
+ const name = (error as { name?: unknown }).name
31
+ return typeof name === 'string' && ABORT_ERROR_NAMES.has(name)
32
+ }
33
+ return false
34
+ }
35
+
21
36
  // HTTP status codes carried as numbers (e.g. `error.status = 429`) are a
22
37
  // common variant on SDK error classes; coerce so the resulting `code` field
23
38
  // is stable as a string for downstream consumers.
@@ -29,32 +44,49 @@ function normalizeCode(codeField: unknown): string | undefined {
29
44
  return undefined
30
45
  }
31
46
 
47
+ // SDK error classes disagree on where they carry the HTTP status. Most expose a
48
+ // `code` (OpenAI/Anthropic error bodies), but some report it only as a numeric
49
+ // `status` — Google's `@google/genai` `ApiError` sets `status: number` and no
50
+ // `code` at all. Without this fallback such errors reach downstream consumers
51
+ // with `code: undefined`, so a 401/403/404/429 is indistinguishable from an
52
+ // unknown failure and cannot be classified.
53
+ //
54
+ // Only a *numeric* `status` is used: a string `status` is commonly an HTTP
55
+ // reason phrase ("Forbidden") or a symbolic status ("PERMISSION_DENIED"), not
56
+ // the numeric code consumers key on, so forwarding it would be misleading.
57
+ function extractCode(source: {
58
+ code?: unknown
59
+ status?: unknown
60
+ }): string | undefined {
61
+ const fromCode = normalizeCode(source.code)
62
+ if (fromCode !== undefined) return fromCode
63
+ if (typeof source.status === 'number' && Number.isFinite(source.status)) {
64
+ return String(source.status)
65
+ }
66
+ return undefined
67
+ }
68
+
32
69
  export function toRunErrorPayload(
33
70
  error: unknown,
34
71
  fallbackMessage = 'Unknown error occurred',
35
72
  ): { message: string; code: string | undefined } {
36
- if (error && typeof error === 'object') {
37
- const name = (error as { name?: unknown }).name
38
- if (typeof name === 'string' && ABORT_ERROR_NAMES.has(name)) {
39
- return { message: 'Request aborted', code: 'aborted' }
40
- }
73
+ if (isAbortShapedError(error)) {
74
+ return { message: 'Request aborted', code: 'aborted' }
41
75
  }
42
76
  if (error instanceof Error) {
43
- const codeField = (error as Error & { code?: unknown }).code
44
77
  return {
45
78
  message: error.message || fallbackMessage,
46
- code: normalizeCode(codeField),
79
+ code: extractCode(error as Error & { code?: unknown; status?: unknown }),
47
80
  }
48
81
  }
49
82
  if (typeof error === 'object' && error !== null) {
50
83
  const messageField = (error as { message?: unknown }).message
51
- const codeField = (error as { code?: unknown }).code
52
84
  return {
53
85
  message:
54
86
  typeof messageField === 'string' && messageField.length > 0
55
87
  ? messageField
56
88
  : fallbackMessage,
57
- code: normalizeCode(codeField),
89
+ code: extractCode(error as { code?: unknown; status?: unknown }),
58
90
  }
59
91
  }
60
92
  if (typeof error === 'string' && error.length > 0) {