@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
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/embed/index.ts"],"sourcesContent":["/**\n * Embed Activity\n *\n * Generates embedding vectors from text and (for multimodal models) image\n * inputs. This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport { countEmbeddingInputModalities } from '../../utilities/embedding-input'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { EmbeddingAdapter } from './adapter'\nimport type {\n EmbeddingInputItem,\n EmbeddingInputItemFor,\n EmbeddingResult,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'embedding' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an EmbeddingAdapter via ~types.\n * If the model has specific options defined in ModelProviderOptions (and not just via index signature),\n * use those; otherwise fall back to base provider options.\n */\nexport type EmbedProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<\n any,\n infer BaseOptions,\n infer ModelOptions,\n any\n >\n ? string extends keyof ModelOptions\n ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions\n BaseOptions\n : // ModelOptions has explicit keys - check if TModel is one of them\n TModel extends keyof ModelOptions\n ? ModelOptions[TModel]\n : BaseOptions\n : object\n\n/**\n * Extract the input type a model accepts from an EmbeddingAdapter via ~types.\n * Adapters declare a per-model input-modality map; models in the map get an\n * `input` narrowed to their supported item types (text-only models accept\n * `string | TextPart`), so unsupported items fail at compile time. Adapters\n * without a map fall back to the full EmbeddingInputItem union.\n */\nexport type EmbeddingInputForModel<TAdapter, TModel extends string> =\n TAdapter extends EmbeddingAdapter<any, any, any, infer ModsByName>\n ? string extends keyof ModsByName\n ? // No explicit map - accept the full union\n EmbeddingInputItem | Array<EmbeddingInputItem>\n : TModel extends keyof ModsByName\n ?\n | EmbeddingInputItemFor<ModsByName[TModel][number]>\n | Array<EmbeddingInputItemFor<ModsByName[TModel][number]>>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n : EmbeddingInputItem | Array<EmbeddingInputItem>\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the embed activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The embedding adapter type\n */\nexport type EmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n> = {\n /** The embedding adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /**\n * What to embed: a single item or an array of items. Each item in the array\n * produces exactly one vector. An item is a plain string, a text part, an\n * image part, or — for models that embed text and image together — a fused\n * item written as a nested array of parts (`[textPart, imagePart]`), the\n * same `Array<ContentPart>` shape chat messages use. The accepted item types\n * are narrowed per model via the adapter's input-modality map.\n */\n input: EmbeddingInputForModel<TAdapter, TAdapter['model']>\n /**\n * Requested output dimensionality. Supported by models with Matryoshka /\n * configurable dimensions; adapters for fixed-dimension models throw a\n * clear runtime error when this is set.\n */\n dimensions?: number\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n} & ({} extends EmbedProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for embedding generation */ modelOptions?: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for embedding generation */ modelOptions: EmbedProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Embed activity - generates embedding vectors from text and image inputs.\n *\n * Accepts a single item or an array of items; the result always carries an\n * `embeddings` array with one vector per input item, in input order.\n *\n * @example Embed a single text\n * ```ts\n * import { embed } from '@tanstack/ai'\n * import { openaiEmbedding } from '@tanstack/ai-openai'\n *\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-small'),\n * input: 'a red guitar',\n * })\n *\n * console.log(result.embeddings[0].vector)\n * ```\n *\n * @example Batch with requested dimensions\n * ```ts\n * const result = await embed({\n * adapter: openaiEmbedding('text-embedding-3-large'),\n * input: ['a red guitar', 'a blue drum kit'],\n * dimensions: 1024,\n * })\n * ```\n *\n * @example Multimodal embedding (text + image fused into one vector)\n * ```ts\n * import { cohereEmbedding } from '@tanstack/ai-cohere'\n *\n * // A nested array of parts fuses them into a single vector. The outer array\n * // is the item list, so this embeds one fused item into one vector.\n * const result = await embed({\n * adapter: cohereEmbedding('embed-v4.0'),\n * input: [\n * [\n * { type: 'text', content: 'product photo' },\n * { type: 'image', source: { type: 'data', value: base64, mimeType: 'image/png' } },\n * ],\n * ],\n * modelOptions: { inputType: 'search_document' },\n * })\n * ```\n */\nexport async function embed<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): Promise<EmbeddingResult> {\n const { adapter, middleware } = options\n const model = adapter.model\n const requestId = createId('embedding')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const modelOptions = (options as { modelOptions?: Record<string, unknown> })\n .modelOptions\n\n // Normalize once: adapters always receive an array of items.\n const inputItems: Array<EmbeddingInputItem> = Array.isArray(options.input)\n ? options.input\n : [options.input]\n const { textInputCount, imageInputCount } =\n countEmbeddingInputModalities(inputItems)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'embedding',\n provider: adapter.name,\n model,\n modelOptions,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('embedding:request:started', {\n requestId,\n provider: adapter.name,\n model,\n inputCount: inputItems.length,\n textInputCount,\n imageInputCount,\n dimensions: options.dimensions,\n modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=embed provider=${adapter.name} model=${model}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const result = await adapter.createEmbeddings({\n model,\n input: inputItems,\n dimensions: options.dimensions,\n modelOptions,\n logger,\n })\n const duration = Date.now() - startTime\n\n aiEventClient.emit('embedding:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n embeddingCount: result.embeddings.length,\n dimensions: result.embeddings[0]?.vector.length,\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n\n logger.output(`activity=embed count=${result.embeddings.length}`, {\n embeddingCount: result.embeddings.length,\n })\n\n if (result.usage) {\n aiEventClient.emit('embedding:usage', {\n requestId,\n model,\n usage: result.usage,\n timestamp: Date.now(),\n })\n await runGenerationUsage(middleware, mwCtx, result.usage)\n }\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('embedding:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('embed activity failed', {\n error,\n source: 'embed',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the embed() function without executing.\n */\nexport function createEmbedOptions<\n TAdapter extends EmbeddingAdapter<string, any, any, any>,\n>(options: EmbedOptions<TAdapter>): EmbedOptions<TAdapter> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n EmbeddingAdapter,\n EmbeddingAdapterConfig,\n AnyEmbeddingAdapter,\n} from './adapter'\nexport { BaseEmbeddingAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;AAgCA,IAAa,OAAO;AAsGpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoDA,eAAsB,MAEpB,SAA2D;CAC3D,MAAM,EAAE,SAAS,eAAe;CAChC,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,WAAW;CACtC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAC/D,MAAM,eAAgB,QACnB;CAGH,MAAM,aAAwC,MAAM,QAAQ,QAAQ,KAAK,IACrE,QAAQ,QACR,CAAC,QAAQ,KAAK;CAClB,MAAM,EAAE,gBAAgB,oBACtB,8BAA8B,UAAU;CAE1C,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA;EACA;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,6BAA6B;EAC9C;EACA,UAAU,QAAQ;EAClB;EACA,YAAY,WAAW;EACvB;EACA;EACA,YAAY,QAAQ;EACpB;EACA,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,2BAA2B,QAAQ,KAAK,SAAS,SAAS;EACvE,UAAU,QAAQ;EAClB;CACF,CAAC;CAED,IAAI;EACF,MAAM,SAAS,MAAM,QAAQ,iBAAiB;GAC5C;GACA,OAAO;GACP,YAAY,QAAQ;GACpB;GACA;EACF,CAAC;EACD,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,+BAA+B;GAChD;GACA,UAAU,QAAQ;GAClB;GACA,gBAAgB,OAAO,WAAW;GAClC,YAAY,OAAO,WAAW,EAAE,EAAE,OAAO;GACzC;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,OAAO,OAAO,wBAAwB,OAAO,WAAW,UAAU,EAChE,gBAAgB,OAAO,WAAW,OACpC,CAAC;EAED,IAAI,OAAO,OAAO;GAChB,cAAc,KAAK,mBAAmB;IACpC;IACA;IACA,OAAO,OAAO;IACd,WAAW,KAAK,IAAI;GACtB,CAAC;GACD,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1D;EACA,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,MAAM,MAAM;EACZ,cAAc,KAAK,2BAA2B;GAC5C;GACA,UAAU,QAAQ;GAClB;GACA,OAAO;IAAE,SAAS,IAAI;IAAS,MAAM,IAAI;GAAK;GAC9C;GACA;GACA,WAAW,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,OAAO,yBAAyB;GACrC;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,mBAEd,SAAyD;CACzD,OAAO;AACT"}
@@ -1,3 +1,11 @@
1
+ /**
2
+ * True when a thrown value is an abort-shaped error (DOM `AbortError`, OpenAI
3
+ * `APIUserAbortError`, OpenRouter `RequestAbortedError`) — i.e. user-initiated
4
+ * cancellation rather than a genuine failure. Matches on the error `name` so
5
+ * callers can discriminate aborts without depending on a signal's state or on
6
+ * provider-specific message strings.
7
+ */
8
+ export declare function isAbortShapedError(error: unknown): boolean;
1
9
  export declare function toRunErrorPayload(error: unknown, fallbackMessage?: string): {
2
10
  message: string;
3
11
  code: string | undefined;
@@ -18,31 +18,43 @@ var ABORT_ERROR_NAMES = /* @__PURE__ */ new Set([
18
18
  "APIUserAbortError",
19
19
  "RequestAbortedError"
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
+ function isAbortShapedError(error) {
29
+ if (error && typeof error === "object") {
30
+ const name = error.name;
31
+ return typeof name === "string" && ABORT_ERROR_NAMES.has(name);
32
+ }
33
+ return false;
34
+ }
21
35
  function normalizeCode(codeField) {
22
36
  if (typeof codeField === "string") return codeField;
23
37
  if (typeof codeField === "number" && Number.isFinite(codeField)) return String(codeField);
24
38
  }
39
+ function extractCode(source) {
40
+ const fromCode = normalizeCode(source.code);
41
+ if (fromCode !== void 0) return fromCode;
42
+ if (typeof source.status === "number" && Number.isFinite(source.status)) return String(source.status);
43
+ }
25
44
  function toRunErrorPayload(error, fallbackMessage = "Unknown error occurred") {
26
- if (error && typeof error === "object") {
27
- const name = error.name;
28
- if (typeof name === "string" && ABORT_ERROR_NAMES.has(name)) return {
29
- message: "Request aborted",
30
- code: "aborted"
31
- };
32
- }
33
- if (error instanceof Error) {
34
- const codeField = error.code;
35
- return {
36
- message: error.message || fallbackMessage,
37
- code: normalizeCode(codeField)
38
- };
39
- }
45
+ if (isAbortShapedError(error)) return {
46
+ message: "Request aborted",
47
+ code: "aborted"
48
+ };
49
+ if (error instanceof Error) return {
50
+ message: error.message || fallbackMessage,
51
+ code: extractCode(error)
52
+ };
40
53
  if (typeof error === "object" && error !== null) {
41
54
  const messageField = error.message;
42
- const codeField = error.code;
43
55
  return {
44
56
  message: typeof messageField === "string" && messageField.length > 0 ? messageField : fallbackMessage,
45
- code: normalizeCode(codeField)
57
+ code: extractCode(error)
46
58
  };
47
59
  }
48
60
  if (typeof error === "string" && error.length > 0) return {
@@ -87,6 +99,6 @@ function toRunErrorRawEvent(error) {
87
99
  if (e.metadata !== void 0 && e.metadata !== null) return e.metadata;
88
100
  }
89
101
  //#endregion
90
- export { toRunErrorPayload, toRunErrorRawEvent };
102
+ export { isAbortShapedError, toRunErrorPayload, toRunErrorRawEvent };
91
103
 
92
104
  //# sourceMappingURL=error-payload.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"error-payload.js","names":[],"sources":["../../../src/activities/error-payload.ts"],"sourcesContent":["/**\n * Shared error-narrowing helper for activities that convert thrown values\n * into structured `RUN_ERROR` events.\n *\n * Accepts Error instances, objects with string-ish `message`/`code`, or bare\n * strings; always returns a shape safe to serialize. Never leaks the full\n * error object (which may carry request/response state from an SDK).\n *\n * Abort-shaped errors (DOM `AbortError`, OpenAI `APIUserAbortError`,\n * OpenRouter `RequestAbortedError`) are normalized to a stable\n * `{ message: 'Request aborted', code: 'aborted' }` shape so callers can\n * discriminate user-initiated cancellation from other failures without\n * matching on provider-specific message strings.\n */\nconst ABORT_ERROR_NAMES = new Set([\n 'AbortError',\n 'APIUserAbortError',\n 'RequestAbortedError',\n])\n\n// HTTP status codes carried as numbers (e.g. `error.status = 429`) are a\n// common variant on SDK error classes; coerce so the resulting `code` field\n// is stable as a string for downstream consumers.\nfunction normalizeCode(codeField: unknown): string | undefined {\n if (typeof codeField === 'string') return codeField\n if (typeof codeField === 'number' && Number.isFinite(codeField)) {\n return String(codeField)\n }\n return undefined\n}\n\nexport function toRunErrorPayload(\n error: unknown,\n fallbackMessage = 'Unknown error occurred',\n): { message: string; code: string | undefined } {\n if (error && typeof error === 'object') {\n const name = (error as { name?: unknown }).name\n if (typeof name === 'string' && ABORT_ERROR_NAMES.has(name)) {\n return { message: 'Request aborted', code: 'aborted' }\n }\n }\n if (error instanceof Error) {\n const codeField = (error as Error & { code?: unknown }).code\n return {\n message: error.message || fallbackMessage,\n code: normalizeCode(codeField),\n }\n }\n if (typeof error === 'object' && error !== null) {\n const messageField = (error as { message?: unknown }).message\n const codeField = (error as { code?: unknown }).code\n return {\n message:\n typeof messageField === 'string' && messageField.length > 0\n ? messageField\n : fallbackMessage,\n code: normalizeCode(codeField),\n }\n }\n if (typeof error === 'string' && error.length > 0) {\n return { message: error, code: undefined }\n }\n return { message: fallbackMessage, code: undefined }\n}\n\n/**\n * Extract the provider's *structured error body* from a thrown value, to attach\n * as the AG-UI `rawEvent` on a RUN_ERROR event. This is the recoverable upstream\n * detail (provider name, the upstream model's error JSON, rate-limit/overload\n * codes, etc.) that `toRunErrorPayload`'s `{ message, code }` deliberately drops.\n *\n * Security boundary: only known provider-response-body fields are forwarded —\n * never the raw SDK exception object, which can carry request metadata such as\n * auth headers or request ids. The recognized sources, in priority order:\n *\n * - `error.rawEvent` — a provider body an adapter attached explicitly (e.g. the\n * OpenRouter mid-stream `chunk.error`).\n * - `error.error` (object) — the parsed provider response body exposed by SDK\n * `APIError` instances (OpenAI/Anthropic `{ type, message, code, param }`,\n * OpenRouter typed errors whose `.error` carries `.metadata`). This is\n * provider-shaped data, distinct from `.headers` / `.request_id`.\n * - `error.metadata` — OpenRouter's `provider_name` + raw upstream body, when\n * surfaced directly on the thrown error.\n *\n * Returns `undefined` when no structured provider body is present, so callers\n * omit the field entirely rather than setting it to `null`:\n *\n * const rawEvent = toRunErrorRawEvent(error)\n * yield { type: EventType.RUN_ERROR, ..., ...(rawEvent !== undefined && { rawEvent }) }\n */\nexport function toRunErrorRawEvent(error: unknown): unknown {\n if (!error || typeof error !== 'object') return undefined\n const e = error as {\n rawEvent?: unknown\n error?: unknown\n metadata?: unknown\n }\n if (e.rawEvent !== undefined && e.rawEvent !== null) return e.rawEvent\n if (\n e.error !== undefined &&\n e.error !== null &&\n typeof e.error === 'object'\n ) {\n return e.error\n }\n if (e.metadata !== undefined && e.metadata !== null) return e.metadata\n return undefined\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,IAAM,oCAAoB,IAAI,IAAI;CAChC;CACA;CACA;AACF,CAAC;AAKD,SAAS,cAAc,WAAwC;CAC7D,IAAI,OAAO,cAAc,UAAU,OAAO;CAC1C,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAC5D,OAAO,OAAO,SAAS;AAG3B;AAEA,SAAgB,kBACd,OACA,kBAAkB,0BAC6B;CAC/C,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,OAAQ,MAA6B;EAC3C,IAAI,OAAO,SAAS,YAAY,kBAAkB,IAAI,IAAI,GACxD,OAAO;GAAE,SAAS;GAAmB,MAAM;EAAU;CAEzD;CACA,IAAI,iBAAiB,OAAO;EAC1B,MAAM,YAAa,MAAqC;EACxD,OAAO;GACL,SAAS,MAAM,WAAW;GAC1B,MAAM,cAAc,SAAS;EAC/B;CACF;CACA,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,eAAgB,MAAgC;EACtD,MAAM,YAAa,MAA6B;EAChD,OAAO;GACL,SACE,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACtD,eACA;GACN,MAAM,cAAc,SAAS;EAC/B;CACF;CACA,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,OAAO;EAAE,SAAS;EAAO,MAAM,KAAA;CAAU;CAE3C,OAAO;EAAE,SAAS;EAAiB,MAAM,KAAA;CAAU;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO,KAAA;CAChD,MAAM,IAAI;CAKV,IAAI,EAAE,aAAa,KAAA,KAAa,EAAE,aAAa,MAAM,OAAO,EAAE;CAC9D,IACE,EAAE,UAAU,KAAA,KACZ,EAAE,UAAU,QACZ,OAAO,EAAE,UAAU,UAEnB,OAAO,EAAE;CAEX,IAAI,EAAE,aAAa,KAAA,KAAa,EAAE,aAAa,MAAM,OAAO,EAAE;AAEhE"}
1
+ {"version":3,"file":"error-payload.js","names":[],"sources":["../../../src/activities/error-payload.ts"],"sourcesContent":["/**\n * Shared error-narrowing helper for activities that convert thrown values\n * into structured `RUN_ERROR` events.\n *\n * Accepts Error instances, objects with string-ish `message`/`code`, or bare\n * strings; always returns a shape safe to serialize. Never leaks the full\n * error object (which may carry request/response state from an SDK).\n *\n * Abort-shaped errors (DOM `AbortError`, OpenAI `APIUserAbortError`,\n * OpenRouter `RequestAbortedError`) are normalized to a stable\n * `{ message: 'Request aborted', code: 'aborted' }` shape so callers can\n * discriminate user-initiated cancellation from other failures without\n * matching on provider-specific message strings.\n */\nconst ABORT_ERROR_NAMES = new Set([\n 'AbortError',\n 'APIUserAbortError',\n 'RequestAbortedError',\n])\n\n/**\n * True when a thrown value is an abort-shaped error (DOM `AbortError`, OpenAI\n * `APIUserAbortError`, OpenRouter `RequestAbortedError`) — i.e. user-initiated\n * cancellation rather than a genuine failure. Matches on the error `name` so\n * callers can discriminate aborts without depending on a signal's state or on\n * provider-specific message strings.\n */\nexport function isAbortShapedError(error: unknown): boolean {\n if (error && typeof error === 'object') {\n const name = (error as { name?: unknown }).name\n return typeof name === 'string' && ABORT_ERROR_NAMES.has(name)\n }\n return false\n}\n\n// HTTP status codes carried as numbers (e.g. `error.status = 429`) are a\n// common variant on SDK error classes; coerce so the resulting `code` field\n// is stable as a string for downstream consumers.\nfunction normalizeCode(codeField: unknown): string | undefined {\n if (typeof codeField === 'string') return codeField\n if (typeof codeField === 'number' && Number.isFinite(codeField)) {\n return String(codeField)\n }\n return undefined\n}\n\n// SDK error classes disagree on where they carry the HTTP status. Most expose a\n// `code` (OpenAI/Anthropic error bodies), but some report it only as a numeric\n// `status` — Google's `@google/genai` `ApiError` sets `status: number` and no\n// `code` at all. Without this fallback such errors reach downstream consumers\n// with `code: undefined`, so a 401/403/404/429 is indistinguishable from an\n// unknown failure and cannot be classified.\n//\n// Only a *numeric* `status` is used: a string `status` is commonly an HTTP\n// reason phrase (\"Forbidden\") or a symbolic status (\"PERMISSION_DENIED\"), not\n// the numeric code consumers key on, so forwarding it would be misleading.\nfunction extractCode(source: {\n code?: unknown\n status?: unknown\n}): string | undefined {\n const fromCode = normalizeCode(source.code)\n if (fromCode !== undefined) return fromCode\n if (typeof source.status === 'number' && Number.isFinite(source.status)) {\n return String(source.status)\n }\n return undefined\n}\n\nexport function toRunErrorPayload(\n error: unknown,\n fallbackMessage = 'Unknown error occurred',\n): { message: string; code: string | undefined } {\n if (isAbortShapedError(error)) {\n return { message: 'Request aborted', code: 'aborted' }\n }\n if (error instanceof Error) {\n return {\n message: error.message || fallbackMessage,\n code: extractCode(error as Error & { code?: unknown; status?: unknown }),\n }\n }\n if (typeof error === 'object' && error !== null) {\n const messageField = (error as { message?: unknown }).message\n return {\n message:\n typeof messageField === 'string' && messageField.length > 0\n ? messageField\n : fallbackMessage,\n code: extractCode(error as { code?: unknown; status?: unknown }),\n }\n }\n if (typeof error === 'string' && error.length > 0) {\n return { message: error, code: undefined }\n }\n return { message: fallbackMessage, code: undefined }\n}\n\n/**\n * Extract the provider's *structured error body* from a thrown value, to attach\n * as the AG-UI `rawEvent` on a RUN_ERROR event. This is the recoverable upstream\n * detail (provider name, the upstream model's error JSON, rate-limit/overload\n * codes, etc.) that `toRunErrorPayload`'s `{ message, code }` deliberately drops.\n *\n * Security boundary: only known provider-response-body fields are forwarded —\n * never the raw SDK exception object, which can carry request metadata such as\n * auth headers or request ids. The recognized sources, in priority order:\n *\n * - `error.rawEvent` — a provider body an adapter attached explicitly (e.g. the\n * OpenRouter mid-stream `chunk.error`).\n * - `error.error` (object) — the parsed provider response body exposed by SDK\n * `APIError` instances (OpenAI/Anthropic `{ type, message, code, param }`,\n * OpenRouter typed errors whose `.error` carries `.metadata`). This is\n * provider-shaped data, distinct from `.headers` / `.request_id`.\n * - `error.metadata` — OpenRouter's `provider_name` + raw upstream body, when\n * surfaced directly on the thrown error.\n *\n * Returns `undefined` when no structured provider body is present, so callers\n * omit the field entirely rather than setting it to `null`:\n *\n * const rawEvent = toRunErrorRawEvent(error)\n * yield { type: EventType.RUN_ERROR, ..., ...(rawEvent !== undefined && { rawEvent }) }\n */\nexport function toRunErrorRawEvent(error: unknown): unknown {\n if (!error || typeof error !== 'object') return undefined\n const e = error as {\n rawEvent?: unknown\n error?: unknown\n metadata?: unknown\n }\n if (e.rawEvent !== undefined && e.rawEvent !== null) return e.rawEvent\n if (\n e.error !== undefined &&\n e.error !== null &&\n typeof e.error === 'object'\n ) {\n return e.error\n }\n if (e.metadata !== undefined && e.metadata !== null) return e.metadata\n return undefined\n}\n"],"mappings":";;;;;;;;;;;;;;;AAcA,IAAM,oCAAoB,IAAI,IAAI;CAChC;CACA;CACA;AACF,CAAC;;;;;;;;AASD,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,SAAS,OAAO,UAAU,UAAU;EACtC,MAAM,OAAQ,MAA6B;EAC3C,OAAO,OAAO,SAAS,YAAY,kBAAkB,IAAI,IAAI;CAC/D;CACA,OAAO;AACT;AAKA,SAAS,cAAc,WAAwC;CAC7D,IAAI,OAAO,cAAc,UAAU,OAAO;CAC1C,IAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAC5D,OAAO,OAAO,SAAS;AAG3B;AAYA,SAAS,YAAY,QAGE;CACrB,MAAM,WAAW,cAAc,OAAO,IAAI;CAC1C,IAAI,aAAa,KAAA,GAAW,OAAO;CACnC,IAAI,OAAO,OAAO,WAAW,YAAY,OAAO,SAAS,OAAO,MAAM,GACpE,OAAO,OAAO,OAAO,MAAM;AAG/B;AAEA,SAAgB,kBACd,OACA,kBAAkB,0BAC6B;CAC/C,IAAI,mBAAmB,KAAK,GAC1B,OAAO;EAAE,SAAS;EAAmB,MAAM;CAAU;CAEvD,IAAI,iBAAiB,OACnB,OAAO;EACL,SAAS,MAAM,WAAW;EAC1B,MAAM,YAAY,KAAqD;CACzE;CAEF,IAAI,OAAO,UAAU,YAAY,UAAU,MAAM;EAC/C,MAAM,eAAgB,MAAgC;EACtD,OAAO;GACL,SACE,OAAO,iBAAiB,YAAY,aAAa,SAAS,IACtD,eACA;GACN,MAAM,YAAY,KAA6C;EACjE;CACF;CACA,IAAI,OAAO,UAAU,YAAY,MAAM,SAAS,GAC9C,OAAO;EAAE,SAAS;EAAO,MAAM,KAAA;CAAU;CAE3C,OAAO;EAAE,SAAS;EAAiB,MAAM,KAAA;CAAU;AACrD;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BA,SAAgB,mBAAmB,OAAyB;CAC1D,IAAI,CAAC,SAAS,OAAO,UAAU,UAAU,OAAO,KAAA;CAChD,MAAM,IAAI;CAKV,IAAI,EAAE,aAAa,KAAA,KAAa,EAAE,aAAa,MAAM,OAAO,EAAE;CAC9D,IACE,EAAE,UAAU,KAAA,KACZ,EAAE,UAAU,QACZ,OAAO,EAAE,UAAU,UAEnB,OAAO,EAAE;CAEX,IAAI,EAAE,aAAa,KAAA,KAAa,EAAE,aAAa,MAAM,OAAO,EAAE;AAEhE"}
@@ -54,6 +54,18 @@ export interface AudioActivityOptions<TAdapter extends AudioAdapter<string, Audi
54
54
  threadId?: string;
55
55
  /** Stable run id for correlating this run when persisted. */
56
56
  runId?: string;
57
+ /**
58
+ * Maximum duration of this activity invocation in milliseconds.
59
+ * No SDK-wide default — choose a value suitable for the provider and job.
60
+ * Composed with {@link abortSignal}; the first abort wins.
61
+ */
62
+ timeout?: number;
63
+ /**
64
+ * Caller cancellation signal (request disconnects, job/runtime cancellation).
65
+ * Composed with {@link timeout} into an effective signal forwarded to the
66
+ * adapter. Request-specific — not stored on global provider client config.
67
+ */
68
+ abortSignal?: AbortSignal;
57
69
  }
58
70
  /**
59
71
  * Result type for the audio generation activity.
@@ -1,6 +1,7 @@
1
1
  import { resolveDebugOption } from "../../logger/resolve.js";
2
2
  import { streamGenerationResult } from "../stream-generation-result.js";
3
- import { applyGenerationResultTransforms, createGenerationContext, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
3
+ import { applyGenerationResultTransforms, createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
4
+ import { abortReasonMessage, createActivityAbortControls, isActivityAbortError, raceWithAbort } from "../../utilities/activity-abort.js";
4
5
  import "./adapter.js";
5
6
  import { aiEventClient } from "@tanstack/ai-event-client";
6
7
  //#region src/activities/generateAudio/index.ts
@@ -45,11 +46,15 @@ function generateAudio(options) {
45
46
  * Run the core audio generation logic (non-streaming).
46
47
  */
47
48
  async function runGenerateAudio(options) {
48
- const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, ...rest } = options;
49
+ const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, timeout, abortSignal: callerAbortSignal, ...rest } = options;
49
50
  const model = adapter.model;
50
51
  const requestId = createId("audio");
51
52
  const startTime = Date.now();
52
53
  const logger = resolveDebugOption(options.debug);
54
+ const abortControls = createActivityAbortControls({
55
+ timeout,
56
+ abortSignal: callerAbortSignal
57
+ });
53
58
  const providerName = adapter.provider ?? adapter.name ?? "unknown";
54
59
  const mwCtx = createGenerationContext({
55
60
  requestId,
@@ -80,11 +85,14 @@ async function runGenerateAudio(options) {
80
85
  model
81
86
  });
82
87
  try {
83
- const result = await applyGenerationResultTransforms(mwCtx, await adapter.generateAudio({
88
+ const rawResult = await raceWithAbort(adapter.generateAudio({
84
89
  ...rest,
85
90
  model,
86
- logger
87
- }));
91
+ logger,
92
+ ...abortControls.signal ? { abortSignal: abortControls.signal } : {}
93
+ }), abortControls.signal);
94
+ abortControls.clear();
95
+ const result = await applyGenerationResultTransforms(mwCtx, rawResult);
88
96
  const elapsedMs = Date.now() - startTime;
89
97
  aiEventClient.emit("audio:request:completed", {
90
98
  requestId,
@@ -113,6 +121,7 @@ async function runGenerateAudio(options) {
113
121
  });
114
122
  return result;
115
123
  } catch (error) {
124
+ abortControls.clear();
116
125
  const elapsedMs = Date.now() - startTime;
117
126
  const err = error;
118
127
  aiEventClient.emit("audio:request:error", {
@@ -127,7 +136,11 @@ async function runGenerateAudio(options) {
127
136
  modelOptions: rest.modelOptions,
128
137
  timestamp: Date.now()
129
138
  });
130
- await runGenerationError(middleware, mwCtx, {
139
+ if (isActivityAbortError(error, abortControls.signal)) await runGenerationAbort(middleware, mwCtx, {
140
+ reason: abortReasonMessage(error, abortControls.signal),
141
+ duration: elapsedMs
142
+ });
143
+ else await runGenerationError(middleware, mwCtx, {
131
144
  error,
132
145
  duration: elapsedMs
133
146
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateAudio/index.ts"],"sourcesContent":["/**\n * Audio Generation Activity\n *\n * Generates audio (music, sound effects, etc.) from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { AudioAdapter } from './adapter'\nimport type { AudioGenerationResult, StreamChunk } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'audio' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from an AudioAdapter via ~types.\n */\nexport type AudioProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the audio generation activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The audio adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface AudioActivityOptions<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The audio adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Text description of the desired audio */\n prompt: string\n /** Desired duration in seconds */\n duration?: number\n /** Provider-specific options for audio generation */\n modelOptions?: AudioProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<AudioGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the audio generation activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<AudioGenerationResult>\n */\nexport type AudioActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<AudioGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Audio generation activity - generates audio from text prompts.\n *\n * Uses AI models to create music, sound effects, and other audio content.\n *\n * @example Generate music from a prompt\n * ```ts\n * import { generateAudio } from '@tanstack/ai'\n * import { falAudio } from '@tanstack/ai-fal'\n *\n * const result = await generateAudio({\n * adapter: falAudio('fal-ai/diffrhythm'),\n * prompt: 'An upbeat electronic track with synths',\n * duration: 10\n * })\n *\n * console.log(result.audio.url) // URL to generated audio\n * ```\n */\nexport function generateAudio<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: AudioActivityOptions<TAdapter, TStream>,\n): AudioActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateAudio({ ...options, runId: resolved.runId }),\n options,\n ) as AudioActivityResult<TStream>\n }\n return runGenerateAudio(options) as AudioActivityResult<TStream>\n}\n\n/**\n * Run the core audio generation logic (non-streaming).\n */\nasync function runGenerateAudio<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n>(\n options: AudioActivityOptions<TAdapter, boolean>,\n): Promise<AudioGenerationResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('audio')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'audio',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n threadId,\n runId,\n artifactInputs: { prompt: rest.prompt, duration: rest.duration },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('audio:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: rest.prompt,\n duration: rest.duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateAudio provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const rawResult = await adapter.generateAudio({ ...rest, model, logger })\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const elapsedMs = Date.now() - startTime\n\n aiEventClient.emit('audio:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n duration: elapsedMs,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('audio:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateAudio provider=${providerName}`, {\n contentType: result.audio.contentType,\n audioDuration: result.audio.duration,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: elapsedMs,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const elapsedMs = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('audio:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration: elapsedMs,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration: elapsedMs,\n })\n logger.errors('generateAudio activity failed', {\n error,\n source: 'generateAudio',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateAudio() function without executing.\n */\nexport function createAudioOptions<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: AudioActivityOptions<TAdapter, TStream>,\n): AudioActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n AudioAdapter,\n AudioAdapterConfig,\n AnyAudioAdapter,\n} from './adapter'\nexport { BaseAudioAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;AA6BA,IAAa,OAAO;AA8EpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;AAyBA,SAAgB,cAId,SAC8B;CAC9B,IAAI,QAAQ,QACV,OAAO,wBAMJ,aAAa,iBAAiB;EAAE,GAAG;EAAS,OAAO,SAAS;CAAM,CAAC,GACpE,OACF;CAEF,OAAO,iBAAiB,OAAO;AACjC;;;;AAKA,eAAe,iBAGb,SACgC;CAChC,MAAM,EACJ,SACA,QAAQ,SACR,OAAO,QACP,YACA,UACA,OACA,GAAG,SACD;CACJ,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,OAAO;CAClC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAC/D,MAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;CAEF,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA,cAAc,KAAK;EACnB;EACA;EACA,gBAAgB;GAAE,QAAQ,KAAK;GAAQ,UAAU,KAAK;EAAS;EAC/D;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,yBAAyB;EAC1C;EACA,UAAU,QAAQ;EAClB;EACA,QAAQ,KAAK;EACb,UAAU,KAAK;EACf,cAAc,KAAK;EACnB,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,mCAAmC,gBAAgB;EAChE,UAAU;EACV;CACF,CAAC;CAED,IAAI;EAEF,MAAM,SAAS,MAAM,gCAAgC,OAAO,MADpC,QAAQ,cAAc;GAAE,GAAG;GAAM;GAAO;EAAO,CAAC,CACH;EACrE,MAAM,YAAY,KAAK,IAAI,IAAI;EAE/B,cAAc,KAAK,2BAA2B;GAC5C;GACA,UAAU,QAAQ;GAClB;GACA,OAAO,OAAO;GACd,UAAU;GACV,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,IAAI,OAAO,OACT,cAAc,KAAK,eAAe;GAChC;GACA;GACA,OAAO,OAAO;GACd,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAGH,OAAO,OAAO,mCAAmC,gBAAgB;GAC/D,aAAa,OAAO,MAAM;GAC1B,eAAe,OAAO,MAAM;EAC9B,CAAC;EAED,IAAI,OAAO,OAAO,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1E,MAAM,oBAAoB,YAAY,OAAO;GAC3C,UAAU;GACV,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,YAAY,KAAK,IAAI,IAAI;EAC/B,MAAM,MAAM;EACZ,cAAc,KAAK,uBAAuB;GACxC;GACA,UAAU,QAAQ;GAClB;GACA,OAAO;IAAE,SAAS,IAAI;IAAS,MAAM,IAAI;GAAK;GAC9C,UAAU;GACV,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA,UAAU;EACZ,CAAC;EACD,OAAO,OAAO,iCAAiC;GAC7C;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,mBAId,SACyC;CACzC,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateAudio/index.ts"],"sourcesContent":["/**\n * Audio Generation Activity\n *\n * Generates audio (music, sound effects, etc.) from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport {\n abortReasonMessage,\n createActivityAbortControls,\n isActivityAbortError,\n raceWithAbort,\n} from '../../utilities/activity-abort'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { AudioAdapter } from './adapter'\nimport type { AudioGenerationResult, StreamChunk } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'audio' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from an AudioAdapter via ~types.\n */\nexport type AudioProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the audio generation activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The audio adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface AudioActivityOptions<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The audio adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** Text description of the desired audio */\n prompt: string\n /** Desired duration in seconds */\n duration?: number\n /** Provider-specific options for audio generation */\n modelOptions?: AudioProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<AudioGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n /**\n * Maximum duration of this activity invocation in milliseconds.\n * No SDK-wide default — choose a value suitable for the provider and job.\n * Composed with {@link abortSignal}; the first abort wins.\n */\n timeout?: number\n /**\n * Caller cancellation signal (request disconnects, job/runtime cancellation).\n * Composed with {@link timeout} into an effective signal forwarded to the\n * adapter. Request-specific — not stored on global provider client config.\n */\n abortSignal?: AbortSignal\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the audio generation activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<AudioGenerationResult>\n */\nexport type AudioActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<AudioGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Audio generation activity - generates audio from text prompts.\n *\n * Uses AI models to create music, sound effects, and other audio content.\n *\n * @example Generate music from a prompt\n * ```ts\n * import { generateAudio } from '@tanstack/ai'\n * import { falAudio } from '@tanstack/ai-fal'\n *\n * const result = await generateAudio({\n * adapter: falAudio('fal-ai/diffrhythm'),\n * prompt: 'An upbeat electronic track with synths',\n * duration: 10\n * })\n *\n * console.log(result.audio.url) // URL to generated audio\n * ```\n */\nexport function generateAudio<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: AudioActivityOptions<TAdapter, TStream>,\n): AudioActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateAudio({ ...options, runId: resolved.runId }),\n options,\n ) as AudioActivityResult<TStream>\n }\n return runGenerateAudio(options) as AudioActivityResult<TStream>\n}\n\n/**\n * Run the core audio generation logic (non-streaming).\n */\nasync function runGenerateAudio<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n>(\n options: AudioActivityOptions<TAdapter, boolean>,\n): Promise<AudioGenerationResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n timeout,\n abortSignal: callerAbortSignal,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('audio')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'audio',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n threadId,\n runId,\n artifactInputs: { prompt: rest.prompt, duration: rest.duration },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('audio:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: rest.prompt,\n duration: rest.duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateAudio provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const rawResult = await raceWithAbort(\n adapter.generateAudio({\n ...rest,\n model,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n abortControls.clear()\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const elapsedMs = Date.now() - startTime\n\n aiEventClient.emit('audio:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n duration: elapsedMs,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('audio:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateAudio provider=${providerName}`, {\n contentType: result.audio.contentType,\n audioDuration: result.audio.duration,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration: elapsedMs,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n abortControls.clear()\n const elapsedMs = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('audio:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration: elapsedMs,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration: elapsedMs,\n })\n } else {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration: elapsedMs,\n })\n }\n logger.errors('generateAudio activity failed', {\n error,\n source: 'generateAudio',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateAudio() function without executing.\n */\nexport function createAudioOptions<\n TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: AudioActivityOptions<TAdapter, TStream>,\n): AudioActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n AudioAdapter,\n AudioAdapterConfig,\n AnyAudioAdapter,\n} from './adapter'\nexport { BaseAudioAdapter } from './adapter'\n"],"mappings":"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"}
@@ -73,6 +73,18 @@ export type ImageActivityOptions<TAdapter extends ImageAdapter<string, any, any,
73
73
  threadId?: string;
74
74
  /** Stable run id for correlating this run when persisted. */
75
75
  runId?: string;
76
+ /**
77
+ * Maximum duration of this activity invocation in milliseconds.
78
+ * No SDK-wide default — choose a value suitable for the provider and job.
79
+ * Composed with {@link abortSignal}; the first abort wins.
80
+ */
81
+ timeout?: number;
82
+ /**
83
+ * Caller cancellation signal (request disconnects, job/runtime cancellation).
84
+ * Composed with {@link timeout} into an effective signal forwarded to the
85
+ * adapter. Request-specific — not stored on global provider client config.
86
+ */
87
+ abortSignal?: AbortSignal;
76
88
  } & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']> ? {
77
89
  /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<TAdapter, TAdapter['model']>;
78
90
  } : {
@@ -1,6 +1,7 @@
1
1
  import { resolveDebugOption } from "../../logger/resolve.js";
2
2
  import { streamGenerationResult } from "../stream-generation-result.js";
3
- import { applyGenerationResultTransforms, createGenerationContext, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
3
+ import { applyGenerationResultTransforms, createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
4
+ import { abortReasonMessage, createActivityAbortControls, isActivityAbortError, raceWithAbort } from "../../utilities/activity-abort.js";
4
5
  import { resolveMediaPrompt } from "../../utilities/media-prompt.js";
5
6
  import "./adapter.js";
6
7
  import { aiEventClient } from "@tanstack/ai-event-client";
@@ -73,11 +74,15 @@ function generateImage(options) {
73
74
  * Contains all devtools event emission logic.
74
75
  */
75
76
  async function runGenerateImage(options) {
76
- const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, ...rest } = options;
77
+ const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, timeout, abortSignal: callerAbortSignal, ...rest } = options;
77
78
  const model = adapter.model;
78
79
  const requestId = createId("image");
79
80
  const startTime = Date.now();
80
81
  const logger = resolveDebugOption(options.debug);
82
+ const abortControls = createActivityAbortControls({
83
+ timeout,
84
+ abortSignal: callerAbortSignal
85
+ });
81
86
  const mwCtx = createGenerationContext({
82
87
  requestId,
83
88
  activity: "image",
@@ -109,11 +114,14 @@ async function runGenerateImage(options) {
109
114
  model
110
115
  });
111
116
  try {
112
- const result = await applyGenerationResultTransforms(mwCtx, await adapter.generateImages({
117
+ const rawResult = await raceWithAbort(adapter.generateImages({
113
118
  ...rest,
114
119
  model,
115
- logger
116
- }));
120
+ logger,
121
+ ...abortControls.signal ? { abortSignal: abortControls.signal } : {}
122
+ }), abortControls.signal);
123
+ abortControls.clear();
124
+ const result = await applyGenerationResultTransforms(mwCtx, rawResult);
117
125
  const duration = Date.now() - startTime;
118
126
  aiEventClient.emit("image:request:completed", {
119
127
  requestId,
@@ -142,9 +150,15 @@ async function runGenerateImage(options) {
142
150
  });
143
151
  return result;
144
152
  } catch (error) {
145
- await runGenerationError(middleware, mwCtx, {
153
+ abortControls.clear();
154
+ const duration = Date.now() - startTime;
155
+ if (isActivityAbortError(error, abortControls.signal)) await runGenerationAbort(middleware, mwCtx, {
156
+ reason: abortReasonMessage(error, abortControls.signal),
157
+ duration
158
+ });
159
+ else await runGenerationError(middleware, mwCtx, {
146
160
  error,
147
- duration: Date.now() - startTime
161
+ duration
148
162
  });
149
163
  logger.errors("generateImage activity failed", {
150
164
  error,
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateImage/index.ts"],"sourcesContent":["/**\n * Image Activity\n *\n * Generates images from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport { resolveMediaPrompt } from '../../utilities/media-prompt'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { ImageAdapter } from './adapter'\nimport type {\n ImageGenerationResult,\n MediaPrompt,\n MediaPromptFor,\n StreamChunk,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'image' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an ImageAdapter via ~types.\n * If the model has specific options defined in ModelProviderOptions (and not just via index signature),\n * use those; otherwise fall back to base provider options.\n */\nexport type ImageProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, infer BaseOptions, infer ModelOptions, any>\n ? string extends keyof ModelOptions\n ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions\n BaseOptions\n : // ModelOptions has explicit keys - check if TModel is one of them\n TModel extends keyof ModelOptions\n ? ModelOptions[TModel]\n : BaseOptions\n : object\n\n/**\n * Extract model-specific size options from an ImageAdapter via ~types.\n * If the model has specific sizes defined, use those; otherwise fall back to string.\n */\nexport type ImageSizeForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, infer SizeByName>\n ? string extends keyof SizeByName\n ? // SizeByName has index signature - fall back to string\n string\n : // SizeByName has explicit keys - check if TModel is one of them\n TModel extends keyof SizeByName\n ? SizeByName[TModel]\n : string\n : string\n\n/**\n * Extract the prompt type a model accepts from an ImageAdapter via ~types.\n * Adapters declare a per-model input-modality map; models in the map get a\n * `prompt` narrowed to text + their supported part types (text-only models\n * accept `string | Array<TextPart>`), so unsupported media parts fail at\n * compile time. Adapters without a map fall back to the full MediaPrompt.\n */\nexport type ImagePromptForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, any, infer ModsByName>\n ? string extends keyof ModsByName\n ? // No explicit map - accept the full union\n MediaPrompt\n : TModel extends keyof ModsByName\n ? MediaPromptFor<ModsByName[TModel][number]>\n : MediaPrompt\n : MediaPrompt\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the image activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The image adapter type\n * @template TStream - Whether to stream the output\n */\nexport type ImageActivityOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n> = {\n /** The image adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /**\n * Description of the desired image(s). Either a plain string, or — for\n * models that support image-conditioned generation — an ordered array of\n * content parts interleaving text with image inputs (image-to-image,\n * reference-guided, edit, multi-reference). Media parts may carry\n * `metadata.role` (`'reference' | 'mask' | 'control' | 'character'`) to\n * disambiguate intent. The accepted part types are narrowed per model via\n * the adapter's input-modality map.\n */\n prompt: ImagePromptForModel<TAdapter, TAdapter['model']>\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: ImageSizeForModel<TAdapter, TAdapter['model']>\n /**\n * Whether to stream the image generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<ImageGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n} & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for image generation */ modelOptions: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the image activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<ImageGenerationResult>\n */\nexport type ImageActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<ImageGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Image activity - generates images from text prompts.\n *\n * Uses AI image generation models to create images based on natural language descriptions.\n *\n * @example Generate a single image\n * ```ts\n * import { generateImage } from '@tanstack/ai'\n * import { openaiImage } from '@tanstack/ai-openai'\n *\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A serene mountain landscape at sunset'\n * })\n *\n * console.log(result.images[0].url)\n * ```\n *\n * @example Generate multiple images\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-2'),\n * prompt: 'A cute robot mascot',\n * numberOfImages: 4,\n * size: '512x512'\n * })\n *\n * result.images.forEach((image, i) => {\n * console.log(`Image ${i + 1}: ${image.url}`)\n * })\n * ```\n *\n * @example With provider-specific options\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A professional headshot photo',\n * size: '1024x1024',\n * modelOptions: {\n * quality: 'hd',\n * style: 'natural'\n * }\n * })\n * ```\n */\nexport function generateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateImage({ ...options, runId: resolved.runId }),\n options,\n ) as ImageActivityResult<TStream>\n }\n\n return runGenerateImage(options) as ImageActivityResult<TStream>\n}\n\n/**\n * Internal implementation of image generation (always non-streaming).\n * Contains all devtools event emission logic.\n */\nasync function runGenerateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n>(\n options: ImageActivityOptions<TAdapter, boolean>,\n): Promise<ImageGenerationResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('image')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'image',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n threadId,\n runId,\n artifactInputs: { prompt: rest.prompt },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n // Devtools events carry the flattened prompt text plus media-part counts —\n // the wire payload stays `prompt: string` regardless of the prompt shape.\n const resolved = resolveMediaPrompt(rest.prompt)\n\n aiEventClient.emit('image:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: resolved.text,\n numberOfImages: rest.numberOfImages,\n size: rest.size,\n ...(resolved.images.length > 0 && {\n imageInputCount: resolved.images.length,\n }),\n ...(resolved.videos.length > 0 && {\n videoInputCount: resolved.videos.length,\n }),\n ...(resolved.audios.length > 0 && {\n audioInputCount: resolved.audios.length,\n }),\n modelOptions: rest.modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateImage provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const rawResult = await adapter.generateImages({ ...rest, model, logger })\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('image:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the\n // wire shape on the devtools event is a plain optional pair. Use\n // conditional spreads so the emitted record only sets the field actually\n // present — `exactOptionalPropertyTypes` rejects `field: undefined`\n // against `field?: string` targets.\n images: result.images.map((image) => ({\n url: image.url,\n b64Json: image.b64Json,\n })),\n duration,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('image:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateImage count=${result.images.length}`, {\n count: result.images.length,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration: Date.now() - startTime,\n })\n logger.errors('generateImage activity failed', {\n error,\n source: 'generateImage',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateImage() function without executing.\n */\nexport function createImageOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n ImageAdapter,\n ImageAdapterConfig,\n AnyImageAdapter,\n} from './adapter'\nexport { BaseImageAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;;AAmCA,IAAa,OAAO;AAyIpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDA,SAAgB,cAId,SAC8B;CAC9B,IAAI,QAAQ,QACV,OAAO,wBAMJ,aAAa,iBAAiB;EAAE,GAAG;EAAS,OAAO,SAAS;CAAM,CAAC,GACpE,OACF;CAGF,OAAO,iBAAiB,OAAO;AACjC;;;;;AAMA,eAAe,iBAGb,SACgC;CAChC,MAAM,EACJ,SACA,QAAQ,SACR,OAAO,QACP,YACA,UACA,OACA,GAAG,SACD;CACJ,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,OAAO;CAClC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAE/D,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA,cAAc,KAAK;EACnB;EACA;EACA,gBAAgB,EAAE,QAAQ,KAAK,OAAO;EACtC;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAI1C,MAAM,WAAW,mBAAmB,KAAK,MAAM;CAE/C,cAAc,KAAK,yBAAyB;EAC1C;EACA,UAAU,QAAQ;EAClB;EACA,QAAQ,SAAS;EACjB,gBAAgB,KAAK;EACrB,MAAM,KAAK;EACX,GAAI,SAAS,OAAO,SAAS,KAAK,EAChC,iBAAiB,SAAS,OAAO,OACnC;EACA,GAAI,SAAS,OAAO,SAAS,KAAK,EAChC,iBAAiB,SAAS,OAAO,OACnC;EACA,GAAI,SAAS,OAAO,SAAS,KAAK,EAChC,iBAAiB,SAAS,OAAO,OACnC;EACA,cAAc,KAAK;EACnB,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,mCAAmC,QAAQ,QAAQ;EAChE,UAAU,QAAQ;EAClB;CACF,CAAC;CAED,IAAI;EAEF,MAAM,SAAS,MAAM,gCAAgC,OAAO,MADpC,QAAQ,eAAe;GAAE,GAAG;GAAM;GAAO;EAAO,CAAC,CACJ;EACrE,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,2BAA2B;GAC5C;GACA,UAAU,QAAQ;GAClB;GAMA,QAAQ,OAAO,OAAO,KAAK,WAAW;IACpC,KAAK,MAAM;IACX,SAAS,MAAM;GACjB,EAAE;GACF;GACA,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,IAAI,OAAO,OACT,cAAc,KAAK,eAAe;GAChC;GACA;GACA,OAAO,OAAO;GACd,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAGH,OAAO,OAAO,gCAAgC,OAAO,OAAO,UAAU,EACpE,OAAO,OAAO,OAAO,OACvB,CAAC;EAED,IAAI,OAAO,OAAO,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1E,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA,UAAU,KAAK,IAAI,IAAI;EACzB,CAAC;EACD,OAAO,OAAO,iCAAiC;GAC7C;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,mBAId,SACyC;CACzC,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateImage/index.ts"],"sourcesContent":["/**\n * Image Activity\n *\n * Generates images from text prompts.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport {\n abortReasonMessage,\n createActivityAbortControls,\n isActivityAbortError,\n raceWithAbort,\n} from '../../utilities/activity-abort'\nimport { resolveMediaPrompt } from '../../utilities/media-prompt'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { ImageAdapter } from './adapter'\nimport type {\n ImageGenerationResult,\n MediaPrompt,\n MediaPromptFor,\n StreamChunk,\n} from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'image' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract model-specific provider options from an ImageAdapter via ~types.\n * If the model has specific options defined in ModelProviderOptions (and not just via index signature),\n * use those; otherwise fall back to base provider options.\n */\nexport type ImageProviderOptionsForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, infer BaseOptions, infer ModelOptions, any>\n ? string extends keyof ModelOptions\n ? // ModelOptions is Record<string, unknown> or has index signature - use BaseOptions\n BaseOptions\n : // ModelOptions has explicit keys - check if TModel is one of them\n TModel extends keyof ModelOptions\n ? ModelOptions[TModel]\n : BaseOptions\n : object\n\n/**\n * Extract model-specific size options from an ImageAdapter via ~types.\n * If the model has specific sizes defined, use those; otherwise fall back to string.\n */\nexport type ImageSizeForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, infer SizeByName>\n ? string extends keyof SizeByName\n ? // SizeByName has index signature - fall back to string\n string\n : // SizeByName has explicit keys - check if TModel is one of them\n TModel extends keyof SizeByName\n ? SizeByName[TModel]\n : string\n : string\n\n/**\n * Extract the prompt type a model accepts from an ImageAdapter via ~types.\n * Adapters declare a per-model input-modality map; models in the map get a\n * `prompt` narrowed to text + their supported part types (text-only models\n * accept `string | Array<TextPart>`), so unsupported media parts fail at\n * compile time. Adapters without a map fall back to the full MediaPrompt.\n */\nexport type ImagePromptForModel<TAdapter, TModel extends string> =\n TAdapter extends ImageAdapter<any, any, any, any, infer ModsByName>\n ? string extends keyof ModsByName\n ? // No explicit map - accept the full union\n MediaPrompt\n : TModel extends keyof ModsByName\n ? MediaPromptFor<ModsByName[TModel][number]>\n : MediaPrompt\n : MediaPrompt\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the image activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The image adapter type\n * @template TStream - Whether to stream the output\n */\nexport type ImageActivityOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n> = {\n /** The image adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /**\n * Description of the desired image(s). Either a plain string, or — for\n * models that support image-conditioned generation — an ordered array of\n * content parts interleaving text with image inputs (image-to-image,\n * reference-guided, edit, multi-reference). Media parts may carry\n * `metadata.role` (`'reference' | 'mask' | 'control' | 'character'`) to\n * disambiguate intent. The accepted part types are narrowed per model via\n * the adapter's input-modality map.\n */\n prompt: ImagePromptForModel<TAdapter, TAdapter['model']>\n /** Number of images to generate (default: 1) */\n numberOfImages?: number\n /** Image size in WIDTHxHEIGHT format (e.g., \"1024x1024\") */\n size?: ImageSizeForModel<TAdapter, TAdapter['model']>\n /**\n * Whether to stream the image generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<ImageGenerationResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n /**\n * Maximum duration of this activity invocation in milliseconds.\n * No SDK-wide default — choose a value suitable for the provider and job.\n * Composed with {@link abortSignal}; the first abort wins.\n */\n timeout?: number\n /**\n * Caller cancellation signal (request disconnects, job/runtime cancellation).\n * Composed with {@link timeout} into an effective signal forwarded to the\n * adapter. Request-specific — not stored on global provider client config.\n */\n abortSignal?: AbortSignal\n} & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>\n ? {\n /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n }\n : {\n /** Provider-specific options for image generation */ modelOptions: ImageProviderOptionsForModel<\n TAdapter,\n TAdapter['model']\n >\n })\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the image activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<ImageGenerationResult>\n */\nexport type ImageActivityResult<TStream extends boolean = false> =\n TStream extends true\n ? AsyncIterable<StreamChunk>\n : Promise<ImageGenerationResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * Image activity - generates images from text prompts.\n *\n * Uses AI image generation models to create images based on natural language descriptions.\n *\n * @example Generate a single image\n * ```ts\n * import { generateImage } from '@tanstack/ai'\n * import { openaiImage } from '@tanstack/ai-openai'\n *\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A serene mountain landscape at sunset'\n * })\n *\n * console.log(result.images[0].url)\n * ```\n *\n * @example Generate multiple images\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-2'),\n * prompt: 'A cute robot mascot',\n * numberOfImages: 4,\n * size: '512x512'\n * })\n *\n * result.images.forEach((image, i) => {\n * console.log(`Image ${i + 1}: ${image.url}`)\n * })\n * ```\n *\n * @example With provider-specific options\n * ```ts\n * const result = await generateImage({\n * adapter: openaiImage('dall-e-3'),\n * prompt: 'A professional headshot photo',\n * size: '1024x1024',\n * modelOptions: {\n * quality: 'hd',\n * style: 'natural'\n * }\n * })\n * ```\n */\nexport function generateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateImage({ ...options, runId: resolved.runId }),\n options,\n ) as ImageActivityResult<TStream>\n }\n\n return runGenerateImage(options) as ImageActivityResult<TStream>\n}\n\n/**\n * Internal implementation of image generation (always non-streaming).\n * Contains all devtools event emission logic.\n */\nasync function runGenerateImage<\n TAdapter extends ImageAdapter<string, any, any, any>,\n>(\n options: ImageActivityOptions<TAdapter, boolean>,\n): Promise<ImageGenerationResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n timeout,\n abortSignal: callerAbortSignal,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('image')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'image',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n threadId,\n runId,\n artifactInputs: { prompt: rest.prompt },\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n // Devtools events carry the flattened prompt text plus media-part counts —\n // the wire payload stays `prompt: string` regardless of the prompt shape.\n const resolved = resolveMediaPrompt(rest.prompt)\n\n aiEventClient.emit('image:request:started', {\n requestId,\n provider: adapter.name,\n model,\n prompt: resolved.text,\n numberOfImages: rest.numberOfImages,\n size: rest.size,\n ...(resolved.images.length > 0 && {\n imageInputCount: resolved.images.length,\n }),\n ...(resolved.videos.length > 0 && {\n videoInputCount: resolved.videos.length,\n }),\n ...(resolved.audios.length > 0 && {\n audioInputCount: resolved.audios.length,\n }),\n modelOptions: rest.modelOptions,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateImage provider=${adapter.name}`, {\n provider: adapter.name,\n model,\n })\n\n try {\n const rawResult = await raceWithAbort(\n adapter.generateImages({\n ...rest,\n model,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n abortControls.clear()\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('image:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n // GeneratedImage is a discriminated `{ url } | { b64Json }` union, but the\n // wire shape on the devtools event is a plain optional pair. Use\n // conditional spreads so the emitted record only sets the field actually\n // present — `exactOptionalPropertyTypes` rejects `field: undefined`\n // against `field?: string` targets.\n images: result.images.map((image) => ({\n url: image.url,\n b64Json: image.b64Json,\n })),\n duration,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('image:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateImage count=${result.images.length}`, {\n count: result.images.length,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n abortControls.clear()\n const duration = Date.now() - startTime\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration,\n })\n } else {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n }\n logger.errors('generateImage activity failed', {\n error,\n source: 'generateImage',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateImage() function without executing.\n */\nexport function createImageOptions<\n TAdapter extends ImageAdapter<string, any, any, any>,\n TStream extends boolean = false,\n>(\n options: ImageActivityOptions<TAdapter, TStream>,\n): ImageActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type {\n ImageAdapter,\n ImageAdapterConfig,\n AnyImageAdapter,\n} from './adapter'\nexport { BaseImageAdapter } from './adapter'\n"],"mappings":"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"}
@@ -7,7 +7,11 @@ export declare const kind: "tts";
7
7
  /**
8
8
  * Extract provider options from a TTSAdapter via ~types.
9
9
  */
10
- export type TTSProviderOptions<TAdapter> = TAdapter extends TTSAdapter<any, any> ? TAdapter['~types']['providerOptions'] : object;
10
+ export type TTSProviderOptions<TAdapter> = TAdapter extends {
11
+ '~types': {
12
+ providerOptions: infer P extends object;
13
+ };
14
+ } ? P : object;
11
15
  /**
12
16
  * Options for the TTS activity.
13
17
  * The model is extracted from the adapter's model property.
@@ -54,6 +58,18 @@ export interface TTSActivityOptions<TAdapter extends TTSAdapter<string, TTSProvi
54
58
  threadId?: string;
55
59
  /** Stable run id for correlating this run when persisted. */
56
60
  runId?: string;
61
+ /**
62
+ * Maximum duration of this activity invocation in milliseconds.
63
+ * No SDK-wide default — choose a value suitable for the provider and job.
64
+ * Composed with {@link abortSignal}; the first abort wins.
65
+ */
66
+ timeout?: number;
67
+ /**
68
+ * Caller cancellation signal (request disconnects, job/runtime cancellation).
69
+ * Composed with {@link timeout} into an effective signal forwarded to the
70
+ * adapter. Request-specific — not stored on global provider client config.
71
+ */
72
+ abortSignal?: AbortSignal;
57
73
  }
58
74
  /**
59
75
  * Result type for the TTS activity.
@@ -1,6 +1,7 @@
1
1
  import { resolveDebugOption } from "../../logger/resolve.js";
2
2
  import { streamGenerationResult } from "../stream-generation-result.js";
3
- import { applyGenerationResultTransforms, createGenerationContext, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
3
+ import { applyGenerationResultTransforms, createGenerationContext, runGenerationAbort, runGenerationError, runGenerationFinish, runGenerationStart, runGenerationUsage } from "../middleware/run.js";
4
+ import { abortReasonMessage, createActivityAbortControls, isActivityAbortError, raceWithAbort } from "../../utilities/activity-abort.js";
4
5
  import "./adapter.js";
5
6
  import { aiEventClient } from "@tanstack/ai-event-client";
6
7
  //#region src/activities/generateSpeech/index.ts
@@ -56,11 +57,15 @@ function generateSpeech(options) {
56
57
  * Run the core TTS generation logic (non-streaming).
57
58
  */
58
59
  async function runGenerateSpeech(options) {
59
- const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, ...rest } = options;
60
+ const { adapter, stream: _stream, debug: _debug, middleware, threadId, runId, timeout, abortSignal: callerAbortSignal, ...rest } = options;
60
61
  const model = adapter.model;
61
62
  const requestId = createId("speech");
62
63
  const startTime = Date.now();
63
64
  const logger = resolveDebugOption(options.debug);
65
+ const abortControls = createActivityAbortControls({
66
+ timeout,
67
+ abortSignal: callerAbortSignal
68
+ });
64
69
  const providerName = adapter.provider ?? adapter.name ?? "unknown";
65
70
  const mwCtx = createGenerationContext({
66
71
  requestId,
@@ -95,11 +100,14 @@ async function runGenerateSpeech(options) {
95
100
  model
96
101
  });
97
102
  try {
98
- const result = await applyGenerationResultTransforms(mwCtx, await adapter.generateSpeech({
103
+ const rawResult = await raceWithAbort(adapter.generateSpeech({
99
104
  ...rest,
100
105
  model,
101
- logger
102
- }));
106
+ logger,
107
+ ...abortControls.signal ? { abortSignal: abortControls.signal } : {}
108
+ }), abortControls.signal);
109
+ abortControls.clear();
110
+ const result = await applyGenerationResultTransforms(mwCtx, rawResult);
103
111
  const duration = Date.now() - startTime;
104
112
  aiEventClient.emit("speech:request:completed", {
105
113
  requestId,
@@ -131,6 +139,7 @@ async function runGenerateSpeech(options) {
131
139
  });
132
140
  return result;
133
141
  } catch (error) {
142
+ abortControls.clear();
134
143
  const duration = Date.now() - startTime;
135
144
  const err = error;
136
145
  aiEventClient.emit("speech:request:error", {
@@ -145,7 +154,11 @@ async function runGenerateSpeech(options) {
145
154
  modelOptions: rest.modelOptions,
146
155
  timestamp: Date.now()
147
156
  });
148
- await runGenerationError(middleware, mwCtx, {
157
+ if (isActivityAbortError(error, abortControls.signal)) await runGenerationAbort(middleware, mwCtx, {
158
+ reason: abortReasonMessage(error, abortControls.signal),
159
+ duration
160
+ });
161
+ else await runGenerationError(middleware, mwCtx, {
149
162
  error,
150
163
  duration
151
164
  });
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateSpeech/index.ts"],"sourcesContent":["/**\n * TTS Activity\n *\n * Generates speech audio from text using text-to-speech models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { TTSAdapter } from './adapter'\nimport type { StreamChunk, TTSResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'tts' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TTSAdapter via ~types.\n */\nexport type TTSProviderOptions<TAdapter> =\n TAdapter extends TTSAdapter<any, any>\n ? TAdapter['~types']['providerOptions']\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the TTS activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The TTS adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TTSActivityOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The TTS adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Provider-specific options for TTS generation */\n modelOptions?: TTSProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TTSResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the TTS activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TTSResult>\n */\nexport type TTSActivityResult<TStream extends boolean = false> =\n TStream extends true ? AsyncIterable<StreamChunk> : Promise<TTSResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * TTS activity - generates speech from text.\n *\n * Uses AI text-to-speech models to create audio from natural language text.\n *\n * @example Generate speech from text\n * ```ts\n * import { generateSpeech } from '@tanstack/ai'\n * import { openaiSpeech } from '@tanstack/ai-openai'\n *\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1-hd'),\n * text: 'Hello, welcome to TanStack AI!',\n * voice: 'nova'\n * })\n *\n * console.log(result.audio) // base64-encoded audio\n * ```\n *\n * @example With format and speed options\n * ```ts\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1'),\n * text: 'This is slower speech.',\n * voice: 'alloy',\n * format: 'wav',\n * speed: 0.8\n * })\n * ```\n */\nexport function generateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateSpeech({ ...options, runId: resolved.runId }),\n options,\n ) as TTSActivityResult<TStream>\n }\n return runGenerateSpeech(options) as TTSActivityResult<TStream>\n}\n\n/**\n * Run the core TTS generation logic (non-streaming).\n */\nasync function runGenerateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n>(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('speech')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'tts',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n artifactInputs: {\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n },\n threadId,\n runId,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('speech:request:started', {\n requestId,\n provider: adapter.name,\n model,\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateSpeech provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const rawResult = await adapter.generateSpeech({ ...rest, model, logger })\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('speech:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n format: result.format,\n audioDuration: result.duration,\n contentType: result.contentType,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('speech:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {\n bytes: result.audio.length,\n contentType: result.contentType,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('speech:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n logger.errors('generateSpeech activity failed', {\n error,\n source: 'generateSpeech',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateSpeech() function without executing.\n */\nexport function createSpeechOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: TTSActivityOptions<TAdapter, TStream>,\n): TTSActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type { TTSAdapter, TTSAdapterConfig, AnyTTSAdapter } from './adapter'\nexport { BaseTTSAdapter } from './adapter'\n"],"mappings":";;;;;;;;;;;;;AA6BA,IAAa,OAAO;AA+EpB,SAAS,SAAS,QAAwB;CACxC,OAAO,GAAG,OAAO,GAAG,KAAK,IAAI,EAAE,GAAG,KAAK,OAAO,CAAC,CAAC,SAAS,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC;AACzE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoCA,SAAgB,eAGd,SAA4E;CAC5E,IAAI,QAAQ,QACV,OAAO,wBAMJ,aAAa,kBAAkB;EAAE,GAAG;EAAS,OAAO,SAAS;CAAM,CAAC,GACrE,OACF;CAEF,OAAO,kBAAkB,OAAO;AAClC;;;;AAKA,eAAe,kBAEb,SAAoE;CACpE,MAAM,EACJ,SACA,QAAQ,SACR,OAAO,QACP,YACA,UACA,OACA,GAAG,SACD;CACJ,MAAM,QAAQ,QAAQ;CACtB,MAAM,YAAY,SAAS,QAAQ;CACnC,MAAM,YAAY,KAAK,IAAI;CAC3B,MAAM,SAAyB,mBAAmB,QAAQ,KAAK;CAC/D,MAAM,eACH,QAAiD,YACjD,QAA8B,QAC/B;CAEF,MAAM,QAAQ,wBAAwB;EACpC;EACA,UAAU;EACV,UAAU,QAAQ;EAClB;EACA,cAAc,KAAK;EACnB,gBAAgB;GACd,MAAM,KAAK;GACX,OAAO,KAAK;GACZ,QAAQ,KAAK;GACb,OAAO,KAAK;EACd;EACA;EACA;EACA;CACF,CAAC;CAED,MAAM,mBAAmB,YAAY,KAAK;CAE1C,cAAc,KAAK,0BAA0B;EAC3C;EACA,UAAU,QAAQ;EAClB;EACA,MAAM,KAAK;EACX,OAAO,KAAK;EACZ,QAAQ,KAAK;EACb,OAAO,KAAK;EACZ,cAAc,KAAK;EACnB,WAAW;CACb,CAAC;CAED,OAAO,QAAQ,oCAAoC,gBAAgB;EACjE,UAAU;EACV;CACF,CAAC;CAED,IAAI;EAEF,MAAM,SAAS,MAAM,gCAAgC,OAAO,MADpC,QAAQ,eAAe;GAAE,GAAG;GAAM;GAAO;EAAO,CAAC,CACJ;EACrE,MAAM,WAAW,KAAK,IAAI,IAAI;EAE9B,cAAc,KAAK,4BAA4B;GAC7C;GACA,UAAU,QAAQ;GAClB;GACA,OAAO,OAAO;GACd,QAAQ,OAAO;GACf,eAAe,OAAO;GACtB,aAAa,OAAO;GACpB;GACA,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAED,IAAI,OAAO,OACT,cAAc,KAAK,gBAAgB;GACjC;GACA;GACA,OAAO,OAAO;GACd,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EAGH,OAAO,OAAO,iCAAiC,OAAO,MAAM,UAAU;GACpE,OAAO,OAAO,MAAM;GACpB,aAAa,OAAO;EACtB,CAAC;EAED,IAAI,OAAO,OAAO,MAAM,mBAAmB,YAAY,OAAO,OAAO,KAAK;EAC1E,MAAM,oBAAoB,YAAY,OAAO;GAC3C;GACA,OAAO,OAAO;EAChB,CAAC;EAED,OAAO;CACT,SAAS,OAAO;EACd,MAAM,WAAW,KAAK,IAAI,IAAI;EAC9B,MAAM,MAAM;EACZ,cAAc,KAAK,wBAAwB;GACzC;GACA,UAAU,QAAQ;GAClB;GACA,OAAO;IAAE,SAAS,IAAI;IAAS,MAAM,IAAI;GAAK;GAC9C;GACA,cAAc,KAAK;GACnB,WAAW,KAAK,IAAI;EACtB,CAAC;EACD,MAAM,mBAAmB,YAAY,OAAO;GAC1C;GACA;EACF,CAAC;EACD,OAAO,OAAO,kCAAkC;GAC9C;GACA,QAAQ;EACV,CAAC;EACD,MAAM;CACR;AACF;;;;AASA,SAAgB,oBAId,SACuC;CACvC,OAAO;AACT"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../../src/activities/generateSpeech/index.ts"],"sourcesContent":["/**\n * TTS Activity\n *\n * Generates speech audio from text using text-to-speech models.\n * This is a self-contained module with implementation, types, and JSDoc.\n */\n\nimport { aiEventClient } from '@tanstack/ai-event-client'\nimport { streamGenerationResult } from '../stream-generation-result.js'\nimport { resolveDebugOption } from '../../logger/resolve'\nimport {\n applyGenerationResultTransforms,\n createGenerationContext,\n runGenerationAbort,\n runGenerationError,\n runGenerationFinish,\n runGenerationStart,\n runGenerationUsage,\n} from '../middleware/run'\nimport {\n abortReasonMessage,\n createActivityAbortControls,\n isActivityAbortError,\n raceWithAbort,\n} from '../../utilities/activity-abort'\nimport type { InternalLogger } from '../../logger/internal-logger'\nimport type { DebugOption } from '../../logger/types'\nimport type { GenerationMiddleware } from '../middleware/types'\nimport type { TTSAdapter } from './adapter'\nimport type { StreamChunk, TTSResult } from '../../types'\n\n// ===========================\n// Activity Kind\n// ===========================\n\n/** The adapter kind this activity handles */\nexport const kind = 'tts' as const\n\n// ===========================\n// Type Extraction Helpers\n// ===========================\n\n/**\n * Extract provider options from a TTSAdapter via ~types.\n */\nexport type TTSProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P extends object }\n}\n ? P\n : object\n\n// ===========================\n// Activity Options Type\n// ===========================\n\n/**\n * Options for the TTS activity.\n * The model is extracted from the adapter's model property.\n *\n * @template TAdapter - The TTS adapter type\n * @template TStream - Whether to stream the output\n */\nexport interface TTSActivityOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n> {\n /** The TTS adapter to use (must be created with a model) */\n adapter: TAdapter & { kind: typeof kind }\n /** The text to convert to speech */\n text: string\n /** The voice to use for generation */\n voice?: string\n /** The output audio format */\n format?: 'mp3' | 'opus' | 'aac' | 'flac' | 'wav' | 'pcm'\n /** The speed of the generated audio (0.25 to 4.0) */\n speed?: number\n /** Provider-specific options for TTS generation */\n modelOptions?: TTSProviderOptions<TAdapter>\n /**\n * Whether to stream the generation result.\n * When true, returns an AsyncIterable<StreamChunk> for streaming transport.\n * When false or not provided, returns a Promise<TTSResult>.\n *\n * @default false\n */\n stream?: TStream\n /**\n * Enable debug logging. Pass `true` to enable all categories, `false` to\n * silence everything including errors, or a `DebugConfig` object for granular\n * control and/or a custom `Logger`.\n */\n debug?: DebugOption\n /**\n * Observe-only middleware notified on start, usage, success, and error. Pass\n * `otelMiddleware()` to emit OpenTelemetry spans, or implement the\n * `GenerationMiddleware` contract for a custom backend.\n */\n middleware?: Array<GenerationMiddleware>\n /** Stable conversation/thread id for correlating this run when persisted. */\n threadId?: string\n /** Stable run id for correlating this run when persisted. */\n runId?: string\n /**\n * Maximum duration of this activity invocation in milliseconds.\n * No SDK-wide default — choose a value suitable for the provider and job.\n * Composed with {@link abortSignal}; the first abort wins.\n */\n timeout?: number\n /**\n * Caller cancellation signal (request disconnects, job/runtime cancellation).\n * Composed with {@link timeout} into an effective signal forwarded to the\n * adapter. Request-specific — not stored on global provider client config.\n */\n abortSignal?: AbortSignal\n}\n\n// ===========================\n// Activity Result Type\n// ===========================\n\n/**\n * Result type for the TTS activity.\n * - If stream is true: AsyncIterable<StreamChunk>\n * - Otherwise: Promise<TTSResult>\n */\nexport type TTSActivityResult<TStream extends boolean = false> =\n TStream extends true ? AsyncIterable<StreamChunk> : Promise<TTSResult>\n\nfunction createId(prefix: string): string {\n return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`\n}\n\n// ===========================\n// Activity Implementation\n// ===========================\n\n/**\n * TTS activity - generates speech from text.\n *\n * Uses AI text-to-speech models to create audio from natural language text.\n *\n * @example Generate speech from text\n * ```ts\n * import { generateSpeech } from '@tanstack/ai'\n * import { openaiSpeech } from '@tanstack/ai-openai'\n *\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1-hd'),\n * text: 'Hello, welcome to TanStack AI!',\n * voice: 'nova'\n * })\n *\n * console.log(result.audio) // base64-encoded audio\n * ```\n *\n * @example With format and speed options\n * ```ts\n * const result = await generateSpeech({\n * adapter: openaiSpeech('tts-1'),\n * text: 'This is slower speech.',\n * voice: 'alloy',\n * format: 'wav',\n * speed: 0.8\n * })\n * ```\n */\nexport function generateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {\n if (options.stream) {\n return streamGenerationResult(\n // Only `runId` is taken from the resolved wire identity. `threadId` stays\n // the CALLER's: `streamGenerationResult` mints one for the RUN_* chunks\n // when none was passed, and spreading that over the options would hand\n // middleware a thread id known to nobody, which persistence would then\n // file the run under. Matches `generateVideo`.\n (resolved) => runGenerateSpeech({ ...options, runId: resolved.runId }),\n options,\n ) as TTSActivityResult<TStream>\n }\n return runGenerateSpeech(options) as TTSActivityResult<TStream>\n}\n\n/**\n * Run the core TTS generation logic (non-streaming).\n */\nasync function runGenerateSpeech<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n>(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {\n const {\n adapter,\n stream: _stream,\n debug: _debug,\n middleware,\n threadId,\n runId,\n timeout,\n abortSignal: callerAbortSignal,\n ...rest\n } = options\n const model = adapter.model\n const requestId = createId('speech')\n const startTime = Date.now()\n const logger: InternalLogger = resolveDebugOption(options.debug)\n const abortControls = createActivityAbortControls({\n timeout,\n abortSignal: callerAbortSignal,\n })\n const providerName =\n (adapter as { name?: string; provider?: string }).provider ??\n (adapter as { name?: string }).name ??\n 'unknown'\n\n const mwCtx = createGenerationContext({\n requestId,\n activity: 'tts',\n provider: adapter.name,\n model,\n modelOptions: rest.modelOptions,\n artifactInputs: {\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n },\n threadId,\n runId,\n createId,\n })\n\n await runGenerationStart(middleware, mwCtx)\n\n aiEventClient.emit('speech:request:started', {\n requestId,\n provider: adapter.name,\n model,\n text: rest.text,\n voice: rest.voice,\n format: rest.format,\n speed: rest.speed,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: startTime,\n })\n\n logger.request(`activity=generateSpeech provider=${providerName}`, {\n provider: providerName,\n model,\n })\n\n try {\n const rawResult = await raceWithAbort(\n adapter.generateSpeech({\n ...rest,\n model,\n logger,\n ...(abortControls.signal ? { abortSignal: abortControls.signal } : {}),\n }),\n abortControls.signal,\n )\n abortControls.clear()\n const result = await applyGenerationResultTransforms(mwCtx, rawResult)\n const duration = Date.now() - startTime\n\n aiEventClient.emit('speech:request:completed', {\n requestId,\n provider: adapter.name,\n model,\n audio: result.audio,\n format: result.format,\n audioDuration: result.duration,\n contentType: result.contentType,\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n\n if (result.usage) {\n aiEventClient.emit('speech:usage', {\n requestId,\n model,\n usage: result.usage,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n }\n\n logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {\n bytes: result.audio.length,\n contentType: result.contentType,\n })\n\n if (result.usage) await runGenerationUsage(middleware, mwCtx, result.usage)\n await runGenerationFinish(middleware, mwCtx, {\n duration,\n usage: result.usage,\n })\n\n return result\n } catch (error) {\n abortControls.clear()\n const duration = Date.now() - startTime\n const err = error as Error\n aiEventClient.emit('speech:request:error', {\n requestId,\n provider: adapter.name,\n model,\n error: { message: err.message, name: err.name },\n duration,\n modelOptions: rest.modelOptions as Record<string, unknown> | undefined,\n timestamp: Date.now(),\n })\n if (isActivityAbortError(error, abortControls.signal)) {\n await runGenerationAbort(middleware, mwCtx, {\n reason: abortReasonMessage(error, abortControls.signal),\n duration,\n })\n } else {\n await runGenerationError(middleware, mwCtx, {\n error,\n duration,\n })\n }\n logger.errors('generateSpeech activity failed', {\n error,\n source: 'generateSpeech',\n })\n throw error\n }\n}\n\n// ===========================\n// Options Factory\n// ===========================\n\n/**\n * Create typed options for the generateSpeech() function without executing.\n */\nexport function createSpeechOptions<\n TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,\n TStream extends boolean = false,\n>(\n options: TTSActivityOptions<TAdapter, TStream>,\n): TTSActivityOptions<TAdapter, TStream> {\n return options\n}\n\n// Re-export adapter types\nexport type { TTSAdapter, TTSAdapterConfig, AnyTTSAdapter } from './adapter'\nexport { BaseTTSAdapter } from './adapter'\n"],"mappings":"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"}