@tanstack/ai 0.43.0 → 0.44.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.
Files changed (70) hide show
  1. package/dist/esm/activities/chat/messages.js +21 -8
  2. package/dist/esm/activities/chat/messages.js.map +1 -1
  3. package/dist/esm/activities/embed/adapter.d.ts +69 -0
  4. package/dist/esm/activities/embed/adapter.js +23 -0
  5. package/dist/esm/activities/embed/adapter.js.map +1 -0
  6. package/dist/esm/activities/embed/index.d.ts +117 -0
  7. package/dist/esm/activities/embed/index.js +166 -0
  8. package/dist/esm/activities/embed/index.js.map +1 -0
  9. package/dist/esm/activities/error-payload.d.ts +8 -0
  10. package/dist/esm/activities/error-payload.js +29 -17
  11. package/dist/esm/activities/error-payload.js.map +1 -1
  12. package/dist/esm/activities/generateAudio/index.d.ts +12 -0
  13. package/dist/esm/activities/generateAudio/index.js +19 -6
  14. package/dist/esm/activities/generateAudio/index.js.map +1 -1
  15. package/dist/esm/activities/generateImage/index.d.ts +12 -0
  16. package/dist/esm/activities/generateImage/index.js +21 -7
  17. package/dist/esm/activities/generateImage/index.js.map +1 -1
  18. package/dist/esm/activities/generateSpeech/index.d.ts +17 -1
  19. package/dist/esm/activities/generateSpeech/index.js +19 -6
  20. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  21. package/dist/esm/activities/generateTranscription/index.d.ts +17 -1
  22. package/dist/esm/activities/generateTranscription/index.js +19 -6
  23. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  24. package/dist/esm/activities/generateVideo/index.d.ts +18 -0
  25. package/dist/esm/activities/generateVideo/index.js +54 -15
  26. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  27. package/dist/esm/activities/index.d.ts +8 -2
  28. package/dist/esm/activities/index.js +11 -7
  29. package/dist/esm/activities/middleware/types.d.ts +1 -1
  30. package/dist/esm/activities/rerank/adapter.d.ts +63 -0
  31. package/dist/esm/activities/rerank/adapter.js +23 -0
  32. package/dist/esm/activities/rerank/adapter.js.map +1 -0
  33. package/dist/esm/activities/rerank/index.d.ts +92 -0
  34. package/dist/esm/activities/rerank/index.js +163 -0
  35. package/dist/esm/activities/rerank/index.js.map +1 -0
  36. package/dist/esm/activities/summarize/index.d.ts +17 -1
  37. package/dist/esm/activities/summarize/index.js +19 -5
  38. package/dist/esm/activities/summarize/index.js.map +1 -1
  39. package/dist/esm/index.d.ts +7 -2
  40. package/dist/esm/index.js +5 -1
  41. package/dist/esm/middlewares/otel.d.ts +3 -1
  42. package/dist/esm/middlewares/otel.js +25 -6
  43. package/dist/esm/middlewares/otel.js.map +1 -1
  44. package/dist/esm/types.d.ts +195 -0
  45. package/dist/esm/utilities/activity-abort.d.ts +53 -0
  46. package/dist/esm/utilities/activity-abort.js +150 -0
  47. package/dist/esm/utilities/activity-abort.js.map +1 -0
  48. package/dist/esm/utilities/embedding-input.d.ts +32 -0
  49. package/dist/esm/utilities/embedding-input.js +61 -0
  50. package/dist/esm/utilities/embedding-input.js.map +1 -0
  51. package/package.json +3 -3
  52. package/src/activities/chat/messages.ts +30 -1
  53. package/src/activities/embed/adapter.ts +112 -0
  54. package/src/activities/embed/index.ts +318 -0
  55. package/src/activities/error-payload.ts +41 -9
  56. package/src/activities/generateAudio/index.ts +47 -5
  57. package/src/activities/generateImage/index.ts +48 -5
  58. package/src/activities/generateSpeech/index.ts +52 -9
  59. package/src/activities/generateTranscription/index.ts +52 -9
  60. package/src/activities/generateVideo/index.ts +131 -33
  61. package/src/activities/index.ts +44 -0
  62. package/src/activities/middleware/types.ts +2 -0
  63. package/src/activities/rerank/adapter.ts +90 -0
  64. package/src/activities/rerank/index.ts +302 -0
  65. package/src/activities/summarize/index.ts +59 -19
  66. package/src/index.ts +19 -0
  67. package/src/middlewares/otel.ts +60 -8
  68. package/src/types.ts +219 -0
  69. package/src/utilities/activity-abort.ts +197 -0
  70. package/src/utilities/embedding-input.ts +83 -0
package/src/types.ts CHANGED
@@ -376,6 +376,11 @@ export interface ModelMessage<
376
376
  * resume the SAME message bubble in place (see `@tanstack/ai-persistence`).
377
377
  */
378
378
  id?: string
379
+ /**
380
+ * Optional message creation timestamp. When present, message converters
381
+ * preserve it across persist → hydrate round-trips.
382
+ */
383
+ createdAt?: Date
379
384
  }
380
385
 
381
386
  /**
@@ -1992,6 +1997,12 @@ export interface SummarizationOptions<
1992
1997
  * call logger.request() before the SDK call and logger.errors() in catch blocks.
1993
1998
  */
1994
1999
  logger: InternalLogger
2000
+ /**
2001
+ * Effective abort signal composed by the activity from caller `abortSignal`
2002
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2003
+ * supported. Request-specific — never store on a global client config.
2004
+ */
2005
+ abortSignal?: AbortSignal
1995
2006
  }
1996
2007
 
1997
2008
  export interface SummarizationResult {
@@ -2001,6 +2012,71 @@ export interface SummarizationResult {
2001
2012
  usage: TokenUsage
2002
2013
  }
2003
2014
 
2015
+ // ============================================================================
2016
+ // Rerank Types
2017
+ // ============================================================================
2018
+
2019
+ /**
2020
+ * Options passed to a {@link RerankAdapter}. Documents reach the adapter
2021
+ * already serialized to strings — the `rerank()` activity stringifies object
2022
+ * documents and maps results back to the original elements, so adapters never
2023
+ * deal with the caller's document type.
2024
+ */
2025
+ export interface RerankOptions<
2026
+ TProviderOptions extends object = Record<string, unknown>,
2027
+ > {
2028
+ model: string
2029
+ /** The search query documents are scored against. */
2030
+ query: string
2031
+ /** Documents to rerank, pre-serialized to strings by the activity. */
2032
+ documents: Array<string>
2033
+ /** Return only the top N results. Passed through to the provider. */
2034
+ topN?: number
2035
+ /** Provider-specific options forwarded by the rerank() activity. */
2036
+ modelOptions?: TProviderOptions
2037
+ /** Forwarded to the provider request for cancellation. */
2038
+ abortSignal?: AbortSignal
2039
+ /**
2040
+ * Internal logger threaded from the rerank() entry point. Adapters must call
2041
+ * logger.request() before the provider call and logger.errors() in catch
2042
+ * blocks.
2043
+ */
2044
+ logger: InternalLogger
2045
+ }
2046
+
2047
+ /**
2048
+ * Provider-level rerank result. Adapters return scored indices into the
2049
+ * (serialized) `documents` array plus usage — never the documents themselves.
2050
+ * The activity attaches the original documents.
2051
+ */
2052
+ export interface RerankAdapterResult {
2053
+ id: string
2054
+ /** Scored results, highest relevance first, as indices into `documents`. */
2055
+ ranking: Array<{ index: number; score: number }>
2056
+ usage: TokenUsage
2057
+ }
2058
+
2059
+ /**
2060
+ * Public result of the `rerank()` activity, generic over the caller's document
2061
+ * element type so `document` / `rerankedDocuments` carry the original values
2062
+ * (strings or objects), not their serialized form.
2063
+ */
2064
+ export interface RerankResult<TDocument = string> {
2065
+ id: string
2066
+ model: string
2067
+ /** Scored results, highest relevance first. */
2068
+ ranking: Array<{ index: number; score: number; document: TDocument }>
2069
+ /** The documents reordered by relevance — `ranking.map(r => r.document)`. */
2070
+ rerankedDocuments: Array<TDocument>
2071
+ /**
2072
+ * Usage for the request. Rerank typically bills in provider-defined "search
2073
+ * units" (`usage.unitsBilled`) rather than tokens. Some providers (e.g.
2074
+ * OpenRouter) may also report `totalTokens` and `cost`; Cohere reports only
2075
+ * search units and leaves the token counts at 0.
2076
+ */
2077
+ usage: TokenUsage
2078
+ }
2079
+
2004
2080
  // ============================================================================
2005
2081
  // Image Generation Types
2006
2082
  // ============================================================================
@@ -2129,6 +2205,12 @@ export interface ImageGenerationOptions<
2129
2205
  * call logger.request() before the SDK call and logger.errors() in catch blocks.
2130
2206
  */
2131
2207
  logger: InternalLogger
2208
+ /**
2209
+ * Effective abort signal composed by the activity from caller `abortSignal`
2210
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2211
+ * supported. Request-specific — never store on a global client config.
2212
+ */
2213
+ abortSignal?: AbortSignal
2132
2214
  }
2133
2215
 
2134
2216
  /**
@@ -2242,6 +2324,12 @@ export interface AudioGenerationOptions<
2242
2324
  * catch blocks.
2243
2325
  */
2244
2326
  logger: InternalLogger
2327
+ /**
2328
+ * Effective abort signal composed by the activity from caller `abortSignal`
2329
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2330
+ * supported. Request-specific — never store on a global client config.
2331
+ */
2332
+ abortSignal?: AbortSignal
2245
2333
  }
2246
2334
 
2247
2335
  /**
@@ -2311,6 +2399,12 @@ export interface VideoGenerationOptions<
2311
2399
  * call logger.request() before the SDK call and logger.errors() in catch blocks.
2312
2400
  */
2313
2401
  logger: InternalLogger
2402
+ /**
2403
+ * Effective abort signal composed by the activity from caller `abortSignal`
2404
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2405
+ * supported. Request-specific — never store on a global client config.
2406
+ */
2407
+ abortSignal?: AbortSignal
2314
2408
  }
2315
2409
 
2316
2410
  /**
@@ -2396,6 +2490,12 @@ export interface TTSOptions<TProviderOptions extends object = object> {
2396
2490
  * catch blocks.
2397
2491
  */
2398
2492
  logger: InternalLogger
2493
+ /**
2494
+ * Effective abort signal composed by the activity from caller `abortSignal`
2495
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2496
+ * supported. Request-specific — never store on a global client config.
2497
+ */
2498
+ abortSignal?: AbortSignal
2399
2499
  }
2400
2500
 
2401
2501
  /**
@@ -2456,6 +2556,12 @@ export interface TranscriptionOptions<
2456
2556
  * in catch blocks.
2457
2557
  */
2458
2558
  logger: InternalLogger
2559
+ /**
2560
+ * Effective abort signal composed by the activity from caller `abortSignal`
2561
+ * and/or `timeout`. Adapters should forward this to the provider SDK when
2562
+ * supported. Request-specific — never store on a global client config.
2563
+ */
2564
+ abortSignal?: AbortSignal
2459
2565
  }
2460
2566
 
2461
2567
  /**
@@ -2512,6 +2618,119 @@ export interface TranscriptionResult {
2512
2618
  artifacts?: Array<PersistedArtifactRef>
2513
2619
  }
2514
2620
 
2621
+ // ============================================================================
2622
+ // Embedding Types
2623
+ // ============================================================================
2624
+
2625
+ /**
2626
+ * Input modalities an embedding model can accept. Unlike
2627
+ * {@link MediaPromptModality}, `'text'` is listed explicitly because
2628
+ * text-only embedding models are the common case and the modality list
2629
+ * drives compile-time narrowing of {@link EmbeddingInputItem}.
2630
+ */
2631
+ export type EmbeddingModality = 'text' | 'image'
2632
+
2633
+ /**
2634
+ * Per-model map from model name to the input modalities it accepts, used as
2635
+ * an adapter type parameter (`TModelInputModalitiesByName`). Models absent
2636
+ * from the map fall back to the unconstrained {@link EmbeddingInputItem}.
2637
+ */
2638
+ export type EmbeddingModelInputModalitiesByName = Record<
2639
+ string,
2640
+ ReadonlyArray<EmbeddingModality>
2641
+ >
2642
+
2643
+ /**
2644
+ * A fused multi-part embedding item: all parts are embedded together into a
2645
+ * single vector (e.g. a product photo plus its caption). Written as a nested
2646
+ * array of content parts — the same `Array<ContentPart>` convention chat
2647
+ * messages use — so a fused item is visually distinct from the top-level
2648
+ * `input` list, where each element produces its own vector. Supported by
2649
+ * multimodal embedding models such as Cohere embed-v4 and Amazon Titan
2650
+ * Multimodal.
2651
+ */
2652
+ export type EmbeddingContentParts = Array<TextPart | ImagePart>
2653
+
2654
+ /**
2655
+ * One embeddable item, producing exactly one vector. A bare string is
2656
+ * shorthand for a text part; a nested {@link EmbeddingContentParts} array
2657
+ * fuses its parts into a single vector. Note that a bare array at the top
2658
+ * level of `input` is the *list of items* (one vector each) — fuse by
2659
+ * nesting, e.g. `input: [[textPart, imagePart]]`.
2660
+ */
2661
+ export type EmbeddingInputItem =
2662
+ | string
2663
+ | TextPart
2664
+ | ImagePart
2665
+ | EmbeddingContentParts
2666
+
2667
+ /** Maps an embedding modality to the item types it admits. @internal */
2668
+ interface EmbeddingItemByModality {
2669
+ text: TextPart
2670
+ image: ImagePart | EmbeddingContentParts
2671
+ }
2672
+
2673
+ /**
2674
+ * Embedding item type narrowed to the modalities a specific model supports.
2675
+ * `EmbeddingInputItemFor<'text'>` (a text-only model) is `string | TextPart`;
2676
+ * `'text' | 'image'` additionally admits image parts and fused
2677
+ * {@link EmbeddingContentParts} arrays. Used by the activity option types
2678
+ * together with the adapter's per-model modality map so unsupported inputs
2679
+ * fail at compile time.
2680
+ */
2681
+ export type EmbeddingInputItemFor<
2682
+ TModalities extends EmbeddingModality = EmbeddingModality,
2683
+ > = string | TextPart | EmbeddingItemByModality[TModalities]
2684
+
2685
+ /**
2686
+ * Options for embedding generation, as received by adapters. The `embed()`
2687
+ * entry point normalizes a single input item to an array before calling the
2688
+ * adapter, so `input` is always an array here.
2689
+ */
2690
+ export interface EmbeddingOptions<TProviderOptions extends object = object> {
2691
+ /** The model to use for embedding generation */
2692
+ model: string
2693
+ /** The items to embed — one vector per item */
2694
+ input: Array<EmbeddingInputItem>
2695
+ /**
2696
+ * Requested output dimensionality. Adapters for models with fixed
2697
+ * dimensions throw a clear runtime error when this is set.
2698
+ */
2699
+ dimensions?: number
2700
+ /** Model-specific options for embedding generation */
2701
+ modelOptions?: TProviderOptions
2702
+ /**
2703
+ * Internal logger threaded from the embed() entry point. Adapters must
2704
+ * call logger.request() before the SDK call and logger.errors() in catch
2705
+ * blocks.
2706
+ */
2707
+ logger: InternalLogger
2708
+ }
2709
+
2710
+ /**
2711
+ * A single embedding vector.
2712
+ */
2713
+ export interface Embedding {
2714
+ /** The embedding vector */
2715
+ vector: Array<number>
2716
+ /** Position of the source item in the (normalized) input array */
2717
+ index: number
2718
+ }
2719
+
2720
+ /**
2721
+ * Result of embedding generation.
2722
+ */
2723
+ export interface EmbeddingResult {
2724
+ /** Unique identifier for the generation */
2725
+ id: string
2726
+ /** Model used for generation */
2727
+ model: string
2728
+ /** One embedding per input item, in input order */
2729
+ embeddings: Array<Embedding>
2730
+ /** Token usage information (if provided by the adapter) */
2731
+ usage?: TokenUsage
2732
+ }
2733
+
2515
2734
  /**
2516
2735
  * Default metadata type for adapters that don't define custom metadata.
2517
2736
  * Uses unknown for all modalities.
@@ -0,0 +1,197 @@
1
+ /**
2
+ * Shared abort/timeout composition for media (and summarize) activities.
3
+ *
4
+ * Callers pass optional `timeout` and/or `abortSignal` on activity options.
5
+ * Core composes them into one effective signal, races the adapter call so a
6
+ * hung provider still rejects, clears timeout resources on settle, and
7
+ * classifies aborts so lifecycle middleware gets `onAbort` rather than
8
+ * `onError`.
9
+ */
10
+
11
+ const ABORT_ERROR_NAMES = new Set([
12
+ 'AbortError',
13
+ 'TimeoutError',
14
+ 'APIUserAbortError',
15
+ 'RequestAbortedError',
16
+ ])
17
+
18
+ /**
19
+ * Combine two optional AbortSignals into one that aborts when either does.
20
+ * Returns the other signal directly when one is absent or already aborted.
21
+ * First abort wins and preserves its reason.
22
+ *
23
+ * Manual implementation — `AbortSignal.any` requires Node >= 20.3.
24
+ */
25
+ export function combineAbortSignals(
26
+ a: AbortSignal | undefined,
27
+ b: AbortSignal | undefined,
28
+ ): AbortSignal | undefined {
29
+ if (!a) return b
30
+ if (!b) return a
31
+ if (a.aborted) return a
32
+ if (b.aborted) return b
33
+ const controller = new AbortController()
34
+ const onAbort = (source: AbortSignal) => () => {
35
+ controller.abort(source.reason)
36
+ }
37
+ a.addEventListener('abort', onAbort(a), { once: true })
38
+ b.addEventListener('abort', onAbort(b), { once: true })
39
+ return controller.signal
40
+ }
41
+
42
+ function createTimeoutReason(ms: number): Error {
43
+ if (typeof DOMException !== 'undefined') {
44
+ return new DOMException(`Activity timed out after ${ms}ms`, 'TimeoutError')
45
+ }
46
+ const err = new Error(`Activity timed out after ${ms}ms`)
47
+ err.name = 'TimeoutError'
48
+ return err
49
+ }
50
+
51
+ /** Normalize an abort reason into an Error the activity can reject with. */
52
+ export function toAbortError(reason: unknown): Error {
53
+ if (reason instanceof Error) return reason
54
+ if (typeof reason === 'string' && reason.length > 0) {
55
+ const err = new Error(reason)
56
+ err.name = 'AbortError'
57
+ return err
58
+ }
59
+ const err = new Error('The operation was aborted')
60
+ err.name = 'AbortError'
61
+ return err
62
+ }
63
+
64
+ export interface ActivityAbortControls {
65
+ /** Effective signal, or `undefined` when neither timeout nor caller signal. */
66
+ signal: AbortSignal | undefined
67
+ /** Clear the timeout timer if one was set. Idempotent. */
68
+ clear: () => void
69
+ }
70
+
71
+ /**
72
+ * Compose an activity-level timeout with a caller AbortSignal.
73
+ *
74
+ * - No SDK-wide default timeout; omit both for unlimited wait.
75
+ * - First of caller cancellation or timeout wins and keeps its reason.
76
+ * - Call `clear()` when the activity settles (success or failure) so timers
77
+ * do not leak.
78
+ */
79
+ export function createActivityAbortControls(options: {
80
+ abortSignal?: AbortSignal
81
+ timeout?: number
82
+ }): ActivityAbortControls {
83
+ let timeoutId: ReturnType<typeof setTimeout> | undefined
84
+ let timeoutSignal: AbortSignal | undefined
85
+
86
+ if (options.timeout !== undefined) {
87
+ if (!Number.isFinite(options.timeout) || options.timeout < 0) {
88
+ throw new Error(
89
+ `Invalid activity timeout: expected a non-negative finite number, got ${String(options.timeout)}`,
90
+ )
91
+ }
92
+ const controller = new AbortController()
93
+ timeoutSignal = controller.signal
94
+ const ms = options.timeout
95
+ timeoutId = setTimeout(() => {
96
+ controller.abort(createTimeoutReason(ms))
97
+ }, ms)
98
+ }
99
+
100
+ const signal = combineAbortSignals(options.abortSignal, timeoutSignal)
101
+
102
+ return {
103
+ signal,
104
+ clear: () => {
105
+ if (timeoutId !== undefined) {
106
+ clearTimeout(timeoutId)
107
+ timeoutId = undefined
108
+ }
109
+ },
110
+ }
111
+ }
112
+
113
+ /**
114
+ * Reject when `signal` aborts, even if the underlying promise ignores it.
115
+ * Ensures activity-level timeouts work for adapters that do not yet forward
116
+ * the signal to the provider SDK.
117
+ *
118
+ * When the signal wins, the adapter promise is observed with an empty handler
119
+ * so a later settle cannot surface as an unhandled rejection.
120
+ */
121
+ export function raceWithAbort<T>(
122
+ promise: Promise<T>,
123
+ signal: AbortSignal | undefined,
124
+ ): Promise<T> {
125
+ if (!signal) return promise
126
+
127
+ const swallow = () => {
128
+ // Observe the adapter promise without acting on its outcome so a late
129
+ // reject after we already aborted cannot become an unhandled rejection.
130
+ promise.then(
131
+ () => undefined,
132
+ () => undefined,
133
+ )
134
+ }
135
+
136
+ if (signal.aborted) {
137
+ swallow()
138
+ return Promise.reject(toAbortError(signal.reason))
139
+ }
140
+
141
+ return new Promise<T>((resolve, reject) => {
142
+ let settled = false
143
+ const onAbort = () => {
144
+ if (settled) return
145
+ settled = true
146
+ cleanup()
147
+ swallow()
148
+ reject(toAbortError(signal.reason))
149
+ }
150
+ const cleanup = () => {
151
+ signal.removeEventListener('abort', onAbort)
152
+ }
153
+ signal.addEventListener('abort', onAbort, { once: true })
154
+ promise.then(
155
+ (value) => {
156
+ if (settled) return
157
+ settled = true
158
+ cleanup()
159
+ resolve(value)
160
+ },
161
+ (error: unknown) => {
162
+ if (settled) return
163
+ settled = true
164
+ cleanup()
165
+ reject(error)
166
+ },
167
+ )
168
+ })
169
+ }
170
+
171
+ /**
172
+ * Whether a thrown value (and optional effective signal) should route to
173
+ * middleware `onAbort` instead of `onError`.
174
+ */
175
+ export function isActivityAbortError(
176
+ error: unknown,
177
+ signal?: AbortSignal,
178
+ ): boolean {
179
+ if (signal?.aborted) return true
180
+ if (!error || typeof error !== 'object') return false
181
+ const name = (error as { name?: unknown }).name
182
+ return typeof name === 'string' && ABORT_ERROR_NAMES.has(name)
183
+ }
184
+
185
+ /** Best-effort string reason for {@link GenerationAbortInfo}. */
186
+ export function abortReasonMessage(
187
+ error: unknown,
188
+ signal?: AbortSignal,
189
+ ): string | undefined {
190
+ if (signal?.reason !== undefined) {
191
+ if (typeof signal.reason === 'string') return signal.reason
192
+ if (signal.reason instanceof Error) return signal.reason.message
193
+ }
194
+ if (error instanceof Error) return error.message
195
+ if (typeof error === 'string') return error
196
+ return undefined
197
+ }
@@ -0,0 +1,83 @@
1
+ import type { EmbeddingInputItem, ImagePart } from '../types'
2
+
3
+ /**
4
+ * One embedding input item resolved into its text and image constituents.
5
+ * Produced by {@link resolveEmbeddingInput}; adapters map each entry onto
6
+ * one provider-native input (one vector per entry).
7
+ */
8
+ export interface ResolvedEmbeddingItem {
9
+ /** Text contents of the item, in order (empty for image-only items) */
10
+ texts: Array<string>
11
+ /** Image parts of the item, in order (empty for text-only items) */
12
+ images: Array<ImagePart>
13
+ }
14
+
15
+ function resolveItem(item: EmbeddingInputItem): ResolvedEmbeddingItem {
16
+ if (typeof item === 'string') {
17
+ return { texts: [item], images: [] }
18
+ }
19
+ // A nested array is a fused item: its parts embed together into one vector.
20
+ if (Array.isArray(item)) {
21
+ const resolved: ResolvedEmbeddingItem = { texts: [], images: [] }
22
+ for (const part of item) {
23
+ if (part.type === 'text') {
24
+ resolved.texts.push(part.content)
25
+ } else {
26
+ resolved.images.push(part)
27
+ }
28
+ }
29
+ return resolved
30
+ }
31
+ if (item.type === 'text') {
32
+ return { texts: [item.content], images: [] }
33
+ }
34
+ return { texts: [], images: [item] }
35
+ }
36
+
37
+ /**
38
+ * Resolve each embedding input item into its text and image constituents,
39
+ * preserving input order (result[i] corresponds to input[i] and to the
40
+ * vector at index i).
41
+ */
42
+ export function resolveEmbeddingInput(
43
+ input: Array<EmbeddingInputItem>,
44
+ ): Array<ResolvedEmbeddingItem> {
45
+ return input.map(resolveItem)
46
+ }
47
+
48
+ /**
49
+ * Extract plain text inputs for a text-only embedding model, throwing a
50
+ * uniform error if any item carries an image. The per-model modality typing
51
+ * rejects these at compile time; this guard covers untyped/dynamic callers.
52
+ */
53
+ export function requireTextOnlyEmbeddingInput(
54
+ input: Array<EmbeddingInputItem>,
55
+ provider: string,
56
+ model: string,
57
+ ): Array<string> {
58
+ return resolveEmbeddingInput(input).map((item, index) => {
59
+ if (item.images.length > 0) {
60
+ throw new Error(
61
+ `${provider} model "${model}" only supports text embedding inputs; ` +
62
+ `input item at index ${index} contains an image part`,
63
+ )
64
+ }
65
+ return item.texts.join('\n')
66
+ })
67
+ }
68
+
69
+ /**
70
+ * Count text-only and image-carrying items for observability events. Never
71
+ * exposes input content.
72
+ */
73
+ export function countEmbeddingInputModalities(
74
+ input: Array<EmbeddingInputItem>,
75
+ ): { textInputCount: number; imageInputCount: number } {
76
+ let textInputCount = 0
77
+ let imageInputCount = 0
78
+ for (const item of resolveEmbeddingInput(input)) {
79
+ if (item.images.length > 0) imageInputCount++
80
+ else textInputCount++
81
+ }
82
+ return { textInputCount, imageInputCount }
83
+ }