@tanstack/ai 0.12.0 → 0.14.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 (93) hide show
  1. package/dist/esm/activities/chat/adapter.d.ts +6 -1
  2. package/dist/esm/activities/chat/adapter.js.map +1 -1
  3. package/dist/esm/activities/chat/index.d.ts +8 -0
  4. package/dist/esm/activities/chat/index.js +78 -17
  5. package/dist/esm/activities/chat/index.js.map +1 -1
  6. package/dist/esm/activities/chat/middleware/compose.d.ts +3 -1
  7. package/dist/esm/activities/chat/middleware/compose.js +79 -1
  8. package/dist/esm/activities/chat/middleware/compose.js.map +1 -1
  9. package/dist/esm/activities/error-payload.d.ts +12 -0
  10. package/dist/esm/activities/error-payload.js +25 -0
  11. package/dist/esm/activities/error-payload.js.map +1 -0
  12. package/dist/esm/activities/generateAudio/adapter.d.ts +62 -0
  13. package/dist/esm/activities/generateAudio/adapter.js +14 -0
  14. package/dist/esm/activities/generateAudio/adapter.js.map +1 -0
  15. package/dist/esm/activities/generateAudio/index.d.ts +74 -0
  16. package/dist/esm/activities/generateAudio/index.js +80 -0
  17. package/dist/esm/activities/generateAudio/index.js.map +1 -0
  18. package/dist/esm/activities/generateImage/adapter.d.ts +1 -1
  19. package/dist/esm/activities/generateImage/adapter.js +1 -1
  20. package/dist/esm/activities/generateImage/adapter.js.map +1 -1
  21. package/dist/esm/activities/generateImage/index.d.ts +7 -0
  22. package/dist/esm/activities/generateImage/index.js +19 -3
  23. package/dist/esm/activities/generateImage/index.js.map +1 -1
  24. package/dist/esm/activities/generateSpeech/adapter.d.ts +1 -1
  25. package/dist/esm/activities/generateSpeech/adapter.js +1 -1
  26. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  27. package/dist/esm/activities/generateSpeech/index.d.ts +10 -3
  28. package/dist/esm/activities/generateSpeech/index.js +32 -3
  29. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  30. package/dist/esm/activities/generateTranscription/adapter.d.ts +1 -1
  31. package/dist/esm/activities/generateTranscription/adapter.js +1 -1
  32. package/dist/esm/activities/generateTranscription/adapter.js.map +1 -1
  33. package/dist/esm/activities/generateTranscription/index.d.ts +10 -3
  34. package/dist/esm/activities/generateTranscription/index.js +43 -13
  35. package/dist/esm/activities/generateTranscription/index.js.map +1 -1
  36. package/dist/esm/activities/generateVideo/index.d.ts +7 -0
  37. package/dist/esm/activities/generateVideo/index.js +55 -13
  38. package/dist/esm/activities/generateVideo/index.js.map +1 -1
  39. package/dist/esm/activities/index.d.ts +5 -2
  40. package/dist/esm/activities/index.js +11 -6
  41. package/dist/esm/activities/index.js.map +1 -1
  42. package/dist/esm/activities/stream-generation-result.js +5 -6
  43. package/dist/esm/activities/stream-generation-result.js.map +1 -1
  44. package/dist/esm/activities/summarize/index.d.ts +7 -0
  45. package/dist/esm/activities/summarize/index.js +54 -19
  46. package/dist/esm/activities/summarize/index.js.map +1 -1
  47. package/dist/esm/adapter-internals.d.ts +4 -0
  48. package/dist/esm/adapter-internals.js +9 -0
  49. package/dist/esm/adapter-internals.js.map +1 -0
  50. package/dist/esm/index.d.ts +5 -2
  51. package/dist/esm/index.js +5 -0
  52. package/dist/esm/index.js.map +1 -1
  53. package/dist/esm/logger/console-logger.d.ts +11 -0
  54. package/dist/esm/logger/console-logger.js +27 -0
  55. package/dist/esm/logger/console-logger.js.map +1 -0
  56. package/dist/esm/logger/internal-logger.d.ts +33 -0
  57. package/dist/esm/logger/internal-logger.js +69 -0
  58. package/dist/esm/logger/internal-logger.js.map +1 -0
  59. package/dist/esm/logger/resolve.d.ts +14 -0
  60. package/dist/esm/logger/resolve.js +54 -0
  61. package/dist/esm/logger/resolve.js.map +1 -0
  62. package/dist/esm/logger/types.d.ts +75 -0
  63. package/dist/esm/stream-to-response.js +3 -8
  64. package/dist/esm/stream-to-response.js.map +1 -1
  65. package/dist/esm/types.d.ts +97 -6
  66. package/package.json +6 -2
  67. package/skills/ai-core/SKILL.md +5 -3
  68. package/skills/ai-core/debug-logging/SKILL.md +263 -0
  69. package/skills/ai-core/media-generation/SKILL.md +154 -10
  70. package/src/activities/chat/adapter.ts +6 -1
  71. package/src/activities/chat/index.ts +104 -22
  72. package/src/activities/chat/middleware/compose.ts +84 -1
  73. package/src/activities/error-payload.ts +35 -0
  74. package/src/activities/generateAudio/adapter.ts +89 -0
  75. package/src/activities/generateAudio/index.ts +224 -0
  76. package/src/activities/generateImage/adapter.ts +1 -1
  77. package/src/activities/generateImage/index.ts +29 -3
  78. package/src/activities/generateSpeech/adapter.ts +1 -1
  79. package/src/activities/generateSpeech/index.ts +51 -9
  80. package/src/activities/generateTranscription/adapter.ts +1 -1
  81. package/src/activities/generateTranscription/index.ts +72 -17
  82. package/src/activities/generateVideo/index.ts +72 -13
  83. package/src/activities/index.ts +22 -0
  84. package/src/activities/stream-generation-result.ts +6 -7
  85. package/src/activities/summarize/index.ts +66 -20
  86. package/src/adapter-internals.ts +8 -0
  87. package/src/index.ts +13 -0
  88. package/src/logger/console-logger.ts +49 -0
  89. package/src/logger/internal-logger.ts +107 -0
  90. package/src/logger/resolve.ts +72 -0
  91. package/src/logger/types.ts +78 -0
  92. package/src/stream-to-response.ts +5 -10
  93. package/src/types.ts +110 -5
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Shared error-narrowing helper for activities that convert thrown values
3
+ * into structured `RUN_ERROR` events.
4
+ *
5
+ * Accepts Error instances, objects with string-ish `message`/`code`, or bare
6
+ * strings; always returns a shape safe to serialize. Never leaks the full
7
+ * error object (which may carry request/response state from an SDK).
8
+ */
9
+ export function toRunErrorPayload(
10
+ error: unknown,
11
+ fallbackMessage = 'Unknown error occurred',
12
+ ): { message: string; code: string | undefined } {
13
+ if (error instanceof Error) {
14
+ const codeField = (error as Error & { code?: unknown }).code
15
+ return {
16
+ message: error.message || fallbackMessage,
17
+ code: typeof codeField === 'string' ? codeField : undefined,
18
+ }
19
+ }
20
+ if (typeof error === 'object' && error !== null) {
21
+ const messageField = (error as { message?: unknown }).message
22
+ const codeField = (error as { code?: unknown }).code
23
+ return {
24
+ message:
25
+ typeof messageField === 'string' && messageField.length > 0
26
+ ? messageField
27
+ : fallbackMessage,
28
+ code: typeof codeField === 'string' ? codeField : undefined,
29
+ }
30
+ }
31
+ if (typeof error === 'string' && error.length > 0) {
32
+ return { message: error, code: undefined }
33
+ }
34
+ return { message: fallbackMessage, code: undefined }
35
+ }
@@ -0,0 +1,89 @@
1
+ import type { AudioGenerationOptions, AudioGenerationResult } from '../../types'
2
+
3
+ /**
4
+ * Configuration for audio generation adapter instances
5
+ */
6
+ export interface AudioAdapterConfig {
7
+ apiKey?: string
8
+ baseUrl?: string
9
+ timeout?: number
10
+ maxRetries?: number
11
+ headers?: Record<string, string>
12
+ }
13
+
14
+ /**
15
+ * Audio generation adapter interface with pre-resolved generics.
16
+ *
17
+ * An adapter is created by a provider function: `provider('model')` → `adapter`
18
+ * All type resolution happens at the provider call site, not in this interface.
19
+ *
20
+ * Generic parameters:
21
+ * - TModel: The specific model name (e.g., 'fal-ai/diffrhythm')
22
+ * - TProviderOptions: Provider-specific options (already resolved)
23
+ */
24
+ export interface AudioAdapter<
25
+ TModel extends string = string,
26
+ TProviderOptions extends object = Record<string, unknown>,
27
+ > {
28
+ /** Discriminator for adapter kind - used to determine API shape */
29
+ readonly kind: 'audio'
30
+ /** Adapter name identifier */
31
+ readonly name: string
32
+ /** The model this adapter is configured for */
33
+ readonly model: TModel
34
+
35
+ /**
36
+ * @internal Type-only properties for inference. Not assigned at runtime.
37
+ */
38
+ '~types': {
39
+ providerOptions: TProviderOptions
40
+ }
41
+
42
+ /**
43
+ * Generate audio from a text prompt
44
+ */
45
+ generateAudio: (
46
+ options: AudioGenerationOptions<TProviderOptions>,
47
+ ) => Promise<AudioGenerationResult>
48
+ }
49
+
50
+ /**
51
+ * An AudioAdapter with any/unknown type parameters.
52
+ * Useful as a constraint in generic functions and interfaces.
53
+ */
54
+ export type AnyAudioAdapter = AudioAdapter<any, any>
55
+
56
+ /**
57
+ * Abstract base class for audio generation adapters.
58
+ * Extend this class to implement an audio adapter for a specific provider.
59
+ *
60
+ * Generic parameters match AudioAdapter - all pre-resolved by the provider function.
61
+ */
62
+ export abstract class BaseAudioAdapter<
63
+ TModel extends string = string,
64
+ TProviderOptions extends object = Record<string, unknown>,
65
+ > implements AudioAdapter<TModel, TProviderOptions> {
66
+ readonly kind = 'audio' as const
67
+ abstract readonly name: string
68
+ readonly model: TModel
69
+
70
+ // Type-only property - never assigned at runtime
71
+ declare '~types': {
72
+ providerOptions: TProviderOptions
73
+ }
74
+
75
+ protected config: AudioAdapterConfig
76
+
77
+ constructor(model: TModel, config: AudioAdapterConfig = {}) {
78
+ this.config = config
79
+ this.model = model
80
+ }
81
+
82
+ abstract generateAudio(
83
+ options: AudioGenerationOptions<TProviderOptions>,
84
+ ): Promise<AudioGenerationResult>
85
+
86
+ protected generateId(): string {
87
+ return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`
88
+ }
89
+ }
@@ -0,0 +1,224 @@
1
+ /**
2
+ * Audio Generation Activity
3
+ *
4
+ * Generates audio (music, sound effects, etc.) from text prompts.
5
+ * This is a self-contained module with implementation, types, and JSDoc.
6
+ */
7
+
8
+ import { aiEventClient } from '@tanstack/ai-event-client'
9
+ import { streamGenerationResult } from '../stream-generation-result.js'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import type { InternalLogger } from '../../logger/internal-logger'
12
+ import type { DebugOption } from '../../logger/types'
13
+ import type { AudioAdapter } from './adapter'
14
+ import type { AudioGenerationResult, StreamChunk } from '../../types'
15
+
16
+ // ===========================
17
+ // Activity Kind
18
+ // ===========================
19
+
20
+ /** The adapter kind this activity handles */
21
+ export const kind = 'audio' as const
22
+
23
+ // ===========================
24
+ // Type Extraction Helpers
25
+ // ===========================
26
+
27
+ /**
28
+ * Extract provider options from an AudioAdapter via ~types.
29
+ */
30
+ export type AudioProviderOptions<TAdapter> =
31
+ TAdapter extends AudioAdapter<any, any>
32
+ ? TAdapter['~types']['providerOptions']
33
+ : object
34
+
35
+ // ===========================
36
+ // Activity Options Type
37
+ // ===========================
38
+
39
+ /**
40
+ * Options for the audio generation activity.
41
+ * The model is extracted from the adapter's model property.
42
+ *
43
+ * @template TAdapter - The audio adapter type
44
+ * @template TStream - Whether to stream the output
45
+ */
46
+ export interface AudioActivityOptions<
47
+ TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,
48
+ TStream extends boolean = false,
49
+ > {
50
+ /** The audio adapter to use (must be created with a model) */
51
+ adapter: TAdapter & { kind: typeof kind }
52
+ /** Text description of the desired audio */
53
+ prompt: string
54
+ /** Desired duration in seconds */
55
+ duration?: number
56
+ /** Provider-specific options for audio generation */
57
+ modelOptions?: AudioProviderOptions<TAdapter>
58
+ /**
59
+ * Whether to stream the generation result.
60
+ * When true, returns an AsyncIterable<StreamChunk> for streaming transport.
61
+ * When false or not provided, returns a Promise<AudioGenerationResult>.
62
+ *
63
+ * @default false
64
+ */
65
+ stream?: TStream
66
+ /**
67
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
68
+ * silence everything including errors, or a `DebugConfig` object for granular
69
+ * control and/or a custom `Logger`.
70
+ */
71
+ debug?: DebugOption
72
+ }
73
+
74
+ // ===========================
75
+ // Activity Result Type
76
+ // ===========================
77
+
78
+ /**
79
+ * Result type for the audio generation activity.
80
+ * - If stream is true: AsyncIterable<StreamChunk>
81
+ * - Otherwise: Promise<AudioGenerationResult>
82
+ */
83
+ export type AudioActivityResult<TStream extends boolean = false> =
84
+ TStream extends true
85
+ ? AsyncIterable<StreamChunk>
86
+ : Promise<AudioGenerationResult>
87
+
88
+ function createId(prefix: string): string {
89
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
90
+ }
91
+
92
+ // ===========================
93
+ // Activity Implementation
94
+ // ===========================
95
+
96
+ /**
97
+ * Audio generation activity - generates audio from text prompts.
98
+ *
99
+ * Uses AI models to create music, sound effects, and other audio content.
100
+ *
101
+ * @example Generate music from a prompt
102
+ * ```ts
103
+ * import { generateAudio } from '@tanstack/ai'
104
+ * import { falAudio } from '@tanstack/ai-fal'
105
+ *
106
+ * const result = await generateAudio({
107
+ * adapter: falAudio('fal-ai/diffrhythm'),
108
+ * prompt: 'An upbeat electronic track with synths',
109
+ * duration: 10
110
+ * })
111
+ *
112
+ * console.log(result.audio.url) // URL to generated audio
113
+ * ```
114
+ */
115
+ export function generateAudio<
116
+ TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,
117
+ TStream extends boolean = false,
118
+ >(
119
+ options: AudioActivityOptions<TAdapter, TStream>,
120
+ ): AudioActivityResult<TStream> {
121
+ if (options.stream) {
122
+ return streamGenerationResult(() =>
123
+ runGenerateAudio(options),
124
+ ) as AudioActivityResult<TStream>
125
+ }
126
+ return runGenerateAudio(options) as AudioActivityResult<TStream>
127
+ }
128
+
129
+ /**
130
+ * Run the core audio generation logic (non-streaming).
131
+ */
132
+ async function runGenerateAudio<
133
+ TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,
134
+ >(
135
+ options: AudioActivityOptions<TAdapter, boolean>,
136
+ ): Promise<AudioGenerationResult> {
137
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
138
+ const model = adapter.model
139
+ const requestId = createId('audio')
140
+ const startTime = Date.now()
141
+ const logger: InternalLogger = resolveDebugOption(options.debug)
142
+ const providerName =
143
+ (adapter as { name?: string; provider?: string }).provider ??
144
+ (adapter as { name?: string }).name ??
145
+ 'unknown'
146
+
147
+ aiEventClient.emit('audio:request:started', {
148
+ requestId,
149
+ provider: adapter.name,
150
+ model,
151
+ prompt: rest.prompt,
152
+ duration: rest.duration,
153
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
154
+ timestamp: startTime,
155
+ })
156
+
157
+ logger.request(`activity=generateAudio provider=${providerName}`, {
158
+ provider: providerName,
159
+ model,
160
+ })
161
+
162
+ try {
163
+ const result = await adapter.generateAudio({ ...rest, model, logger })
164
+ const elapsedMs = Date.now() - startTime
165
+
166
+ aiEventClient.emit('audio:request:completed', {
167
+ requestId,
168
+ provider: adapter.name,
169
+ model,
170
+ audio: result.audio,
171
+ duration: elapsedMs,
172
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
173
+ timestamp: Date.now(),
174
+ })
175
+
176
+ logger.output(`activity=generateAudio provider=${providerName}`, {
177
+ contentType: result.audio.contentType,
178
+ audioDuration: result.audio.duration,
179
+ })
180
+
181
+ return result
182
+ } catch (error) {
183
+ const elapsedMs = Date.now() - startTime
184
+ const err = error as Error
185
+ aiEventClient.emit('audio:request:error', {
186
+ requestId,
187
+ provider: adapter.name,
188
+ model,
189
+ error: { message: err.message, name: err.name },
190
+ duration: elapsedMs,
191
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
192
+ timestamp: Date.now(),
193
+ })
194
+ logger.errors('generateAudio activity failed', {
195
+ error,
196
+ source: 'generateAudio',
197
+ })
198
+ throw error
199
+ }
200
+ }
201
+
202
+ // ===========================
203
+ // Options Factory
204
+ // ===========================
205
+
206
+ /**
207
+ * Create typed options for the generateAudio() function without executing.
208
+ */
209
+ export function createAudioOptions<
210
+ TAdapter extends AudioAdapter<string, AudioProviderOptions<TAdapter>>,
211
+ TStream extends boolean = false,
212
+ >(
213
+ options: AudioActivityOptions<TAdapter, TStream>,
214
+ ): AudioActivityOptions<TAdapter, TStream> {
215
+ return options
216
+ }
217
+
218
+ // Re-export adapter types
219
+ export type {
220
+ AudioAdapter,
221
+ AudioAdapterConfig,
222
+ AnyAudioAdapter,
223
+ } from './adapter'
224
+ export { BaseAudioAdapter } from './adapter'
@@ -96,7 +96,7 @@ export abstract class BaseImageAdapter<
96
96
 
97
97
  protected config: ImageAdapterConfig
98
98
 
99
- constructor(config: ImageAdapterConfig = {}, model: TModel) {
99
+ constructor(model: TModel, config: ImageAdapterConfig = {}) {
100
100
  this.config = config
101
101
  this.model = model
102
102
  }
@@ -7,6 +7,9 @@
7
7
 
8
8
  import { aiEventClient } from '@tanstack/ai-event-client'
9
9
  import { streamGenerationResult } from '../stream-generation-result.js'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import type { InternalLogger } from '../../logger/internal-logger'
12
+ import type { DebugOption } from '../../logger/types'
10
13
  import type { ImageAdapter } from './adapter'
11
14
  import type { ImageGenerationResult, StreamChunk } from '../../types'
12
15
 
@@ -83,6 +86,12 @@ export type ImageActivityOptions<
83
86
  * @default false
84
87
  */
85
88
  stream?: TStream
89
+ /**
90
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
91
+ * silence everything including errors, or a `DebugConfig` object for granular
92
+ * control and/or a custom `Logger`.
93
+ */
94
+ debug?: DebugOption
86
95
  } & ({} extends ImageProviderOptionsForModel<TAdapter, TAdapter['model']>
87
96
  ? {
88
97
  /** Provider-specific options for image generation */ modelOptions?: ImageProviderOptionsForModel<
@@ -188,10 +197,11 @@ async function runGenerateImage<
188
197
  >(
189
198
  options: ImageActivityOptions<TAdapter, boolean>,
190
199
  ): Promise<ImageGenerationResult> {
191
- const { adapter, stream: _stream, ...rest } = options
200
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
192
201
  const model = adapter.model
193
202
  const requestId = createId('image')
194
203
  const startTime = Date.now()
204
+ const logger: InternalLogger = resolveDebugOption(options.debug)
195
205
 
196
206
  aiEventClient.emit('image:request:started', {
197
207
  requestId,
@@ -204,7 +214,13 @@ async function runGenerateImage<
204
214
  timestamp: startTime,
205
215
  })
206
216
 
207
- return adapter.generateImages({ ...rest, model }).then((result) => {
217
+ logger.request(`activity=generateImage provider=${adapter.name}`, {
218
+ provider: adapter.name,
219
+ model,
220
+ })
221
+
222
+ try {
223
+ const result = await adapter.generateImages({ ...rest, model, logger })
208
224
  const duration = Date.now() - startTime
209
225
 
210
226
  aiEventClient.emit('image:request:completed', {
@@ -230,8 +246,18 @@ async function runGenerateImage<
230
246
  })
231
247
  }
232
248
 
249
+ logger.output(`activity=generateImage count=${result.images.length}`, {
250
+ count: result.images.length,
251
+ })
252
+
233
253
  return result
234
- })
254
+ } catch (error) {
255
+ logger.errors('generateImage activity failed', {
256
+ error,
257
+ source: 'generateImage',
258
+ })
259
+ throw error
260
+ }
235
261
  }
236
262
 
237
263
  // ===========================
@@ -72,7 +72,7 @@ export abstract class BaseTTSAdapter<
72
72
 
73
73
  protected config: TTSAdapterConfig
74
74
 
75
- constructor(config: TTSAdapterConfig = {}, model: TModel) {
75
+ constructor(model: TModel, config: TTSAdapterConfig = {}) {
76
76
  this.config = config
77
77
  this.model = model
78
78
  }
@@ -7,6 +7,9 @@
7
7
 
8
8
  import { aiEventClient } from '@tanstack/ai-event-client'
9
9
  import { streamGenerationResult } from '../stream-generation-result.js'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import type { InternalLogger } from '../../logger/internal-logger'
12
+ import type { DebugOption } from '../../logger/types'
10
13
  import type { TTSAdapter } from './adapter'
11
14
  import type { StreamChunk, TTSResult } from '../../types'
12
15
 
@@ -41,7 +44,7 @@ export type TTSProviderOptions<TAdapter> =
41
44
  * @template TStream - Whether to stream the output
42
45
  */
43
46
  export interface TTSActivityOptions<
44
- TAdapter extends TTSAdapter<string, object>,
47
+ TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,
45
48
  TStream extends boolean = false,
46
49
  > {
47
50
  /** The TTS adapter to use (must be created with a model) */
@@ -64,6 +67,12 @@ export interface TTSActivityOptions<
64
67
  * @default false
65
68
  */
66
69
  stream?: TStream
70
+ /**
71
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
72
+ * silence everything including errors, or a `DebugConfig` object for granular
73
+ * control and/or a custom `Logger`.
74
+ */
75
+ debug?: DebugOption
67
76
  }
68
77
 
69
78
  // ===========================
@@ -117,7 +126,7 @@ function createId(prefix: string): string {
117
126
  * ```
118
127
  */
119
128
  export function generateSpeech<
120
- TAdapter extends TTSAdapter<string, object>,
129
+ TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,
121
130
  TStream extends boolean = false,
122
131
  >(options: TTSActivityOptions<TAdapter, TStream>): TTSActivityResult<TStream> {
123
132
  if (options.stream) {
@@ -131,13 +140,18 @@ export function generateSpeech<
131
140
  /**
132
141
  * Run the core TTS generation logic (non-streaming).
133
142
  */
134
- async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
135
- options: TTSActivityOptions<TAdapter, boolean>,
136
- ): Promise<TTSResult> {
137
- const { adapter, stream: _stream, ...rest } = options
143
+ async function runGenerateSpeech<
144
+ TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,
145
+ >(options: TTSActivityOptions<TAdapter, boolean>): Promise<TTSResult> {
146
+ const { adapter, stream: _stream, debug: _debug, ...rest } = options
138
147
  const model = adapter.model
139
148
  const requestId = createId('speech')
140
149
  const startTime = Date.now()
150
+ const logger: InternalLogger = resolveDebugOption(options.debug)
151
+ const providerName =
152
+ (adapter as { name?: string; provider?: string }).provider ??
153
+ (adapter as { name?: string }).name ??
154
+ 'unknown'
141
155
 
142
156
  aiEventClient.emit('speech:request:started', {
143
157
  requestId,
@@ -151,7 +165,13 @@ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
151
165
  timestamp: startTime,
152
166
  })
153
167
 
154
- return adapter.generateSpeech({ ...rest, model }).then((result) => {
168
+ logger.request(`activity=generateSpeech provider=${providerName}`, {
169
+ provider: providerName,
170
+ model,
171
+ })
172
+
173
+ try {
174
+ const result = await adapter.generateSpeech({ ...rest, model, logger })
155
175
  const duration = Date.now() - startTime
156
176
 
157
177
  aiEventClient.emit('speech:request:completed', {
@@ -167,8 +187,30 @@ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
167
187
  timestamp: Date.now(),
168
188
  })
169
189
 
190
+ logger.output(`activity=generateSpeech bytes=${result.audio.length}`, {
191
+ bytes: result.audio.length,
192
+ contentType: result.contentType,
193
+ })
194
+
170
195
  return result
171
- })
196
+ } catch (error) {
197
+ const duration = Date.now() - startTime
198
+ const err = error as Error
199
+ aiEventClient.emit('speech:request:error', {
200
+ requestId,
201
+ provider: adapter.name,
202
+ model,
203
+ error: { message: err.message, name: err.name },
204
+ duration,
205
+ modelOptions: rest.modelOptions as Record<string, unknown> | undefined,
206
+ timestamp: Date.now(),
207
+ })
208
+ logger.errors('generateSpeech activity failed', {
209
+ error,
210
+ source: 'generateSpeech',
211
+ })
212
+ throw error
213
+ }
172
214
  }
173
215
 
174
216
  // ===========================
@@ -179,7 +221,7 @@ async function runGenerateSpeech<TAdapter extends TTSAdapter<string, object>>(
179
221
  * Create typed options for the generateSpeech() function without executing.
180
222
  */
181
223
  export function createSpeechOptions<
182
- TAdapter extends TTSAdapter<string, object>,
224
+ TAdapter extends TTSAdapter<string, TTSProviderOptions<TAdapter>>,
183
225
  TStream extends boolean = false,
184
226
  >(
185
227
  options: TTSActivityOptions<TAdapter, TStream>,
@@ -74,7 +74,7 @@ export abstract class BaseTranscriptionAdapter<
74
74
 
75
75
  protected config: TranscriptionAdapterConfig
76
76
 
77
- constructor(config: TranscriptionAdapterConfig = {}, model: TModel) {
77
+ constructor(model: TModel, config: TranscriptionAdapterConfig = {}) {
78
78
  this.config = config
79
79
  this.model = model
80
80
  }