@tanstack/ai 0.55.0 → 0.57.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 (50) hide show
  1. package/README.md +42 -16
  2. package/dist/esm/activities/chat/tools/tool-calls.js +1 -0
  3. package/dist/esm/activities/chat/tools/tool-calls.js.map +1 -1
  4. package/dist/esm/activities/evaluate/adapter.d.ts +160 -0
  5. package/dist/esm/activities/evaluate/adapter.js +23 -0
  6. package/dist/esm/activities/evaluate/adapter.js.map +1 -0
  7. package/dist/esm/activities/evaluate/index.d.ts +255 -0
  8. package/dist/esm/activities/evaluate/index.js +317 -0
  9. package/dist/esm/activities/evaluate/index.js.map +1 -0
  10. package/dist/esm/activities/generateSpeech/adapter.d.ts +39 -1
  11. package/dist/esm/activities/generateSpeech/adapter.js.map +1 -1
  12. package/dist/esm/activities/generateSpeech/index.d.ts +55 -5
  13. package/dist/esm/activities/generateSpeech/index.js +53 -3
  14. package/dist/esm/activities/generateSpeech/index.js.map +1 -1
  15. package/dist/esm/activities/generateVoice/adapter.d.ts +62 -0
  16. package/dist/esm/activities/generateVoice/adapter.js +23 -0
  17. package/dist/esm/activities/generateVoice/adapter.js.map +1 -0
  18. package/dist/esm/activities/generateVoice/index.d.ts +133 -0
  19. package/dist/esm/activities/generateVoice/index.js +184 -0
  20. package/dist/esm/activities/generateVoice/index.js.map +1 -0
  21. package/dist/esm/activities/index.d.ts +10 -4
  22. package/dist/esm/activities/index.js +14 -10
  23. package/dist/esm/activities/middleware/types.d.ts +1 -1
  24. package/dist/esm/client.d.ts +3 -2
  25. package/dist/esm/client.js +21 -3
  26. package/dist/esm/client.js.map +1 -1
  27. package/dist/esm/index.d.ts +4 -2
  28. package/dist/esm/index.js +5 -2
  29. package/dist/esm/middlewares/otel.js +2 -0
  30. package/dist/esm/middlewares/otel.js.map +1 -1
  31. package/dist/esm/realtime/index.d.ts +1 -1
  32. package/dist/esm/realtime/index.js +1 -1
  33. package/dist/esm/realtime/index.js.map +1 -1
  34. package/dist/esm/types.d.ts +225 -2
  35. package/package.json +3 -3
  36. package/skills/ai-core/media-generation/SKILL.md +132 -6
  37. package/src/activities/chat/tools/tool-calls.ts +9 -0
  38. package/src/activities/evaluate/adapter.ts +212 -0
  39. package/src/activities/evaluate/index.ts +614 -0
  40. package/src/activities/generateSpeech/adapter.ts +47 -1
  41. package/src/activities/generateSpeech/index.ts +149 -8
  42. package/src/activities/generateVoice/adapter.ts +89 -0
  43. package/src/activities/generateVoice/index.ts +371 -0
  44. package/src/activities/index.ts +69 -0
  45. package/src/activities/middleware/types.ts +2 -0
  46. package/src/client.ts +35 -8
  47. package/src/index.ts +21 -0
  48. package/src/middlewares/otel.ts +2 -0
  49. package/src/realtime/index.ts +1 -1
  50. package/src/types.ts +246 -2
@@ -0,0 +1,614 @@
1
+ /**
2
+ * Evaluate Activity
3
+ *
4
+ * Asks typed questions about a shared state and returns values your code can
5
+ * branch on. This is a self-contained module with implementation, types, and
6
+ * JSDoc.
7
+ */
8
+
9
+ import { aiEventClient } from '@tanstack/ai-event-client'
10
+ import { resolveDebugOption } from '../../logger/resolve'
11
+ import { isAbortShapedError } from '../error-payload'
12
+ import {
13
+ createGenerationContext,
14
+ runGenerationAbort,
15
+ runGenerationError,
16
+ runGenerationFinish,
17
+ runGenerationStart,
18
+ runGenerationUsage,
19
+ } from '../middleware/run'
20
+ import type { InternalLogger } from '../../logger/internal-logger'
21
+ import type { DebugOption } from '../../logger/types'
22
+ import type { TokenUsage } from '../../types'
23
+ import type { GenerationMiddleware } from '../middleware/types'
24
+ import type {
25
+ EvaluateAdapter,
26
+ EvaluateInstructions,
27
+ EvaluateState,
28
+ WireAnswer,
29
+ WireChoiceAnswer,
30
+ WireNoulAnswer,
31
+ WireQuestion,
32
+ WireScoreAnswer,
33
+ WireScoreQuestion,
34
+ } from './adapter'
35
+
36
+ // ===========================
37
+ // Activity Kind
38
+ // ===========================
39
+
40
+ /** The adapter kind this activity handles */
41
+ export const kind = 'evaluate' as const
42
+
43
+ /** Question key reserved for `result.meta`. */
44
+ const RESERVED_QUESTION_KEY = 'meta' as const
45
+
46
+ // ===========================
47
+ // Type Extraction Helpers
48
+ // ===========================
49
+
50
+ /** Extract provider options from an EvaluateAdapter via ~types */
51
+ export type EvaluateProviderOptions<TAdapter> = TAdapter extends {
52
+ '~types': { providerOptions: infer P extends object }
53
+ }
54
+ ? P
55
+ : object
56
+
57
+ // ===========================
58
+ // Unified answers
59
+ // ===========================
60
+
61
+ /**
62
+ * Public choice answer. `.value` is the selected option key.
63
+ */
64
+ export interface ChoiceAnswer<TValue extends string = string> {
65
+ type: 'choice'
66
+ value: TValue
67
+ /** P(selected option). */
68
+ probability: number
69
+ confidence: number
70
+ probabilities: Record<TValue, number>
71
+ }
72
+
73
+ /**
74
+ * Public score answer. `.value` is the nearest level label.
75
+ * `.score` is the raw TypeSafe fraction.
76
+ */
77
+ export interface ScoreAnswer<TLevel extends string = string> {
78
+ type: 'score'
79
+ value: TLevel
80
+ /** P(nearest level). */
81
+ probability: number
82
+ confidence: number
83
+ score: number
84
+ legend: Record<string, string>
85
+ probabilities: Record<string, number>
86
+ }
87
+
88
+ /**
89
+ * Public yes/no answer. `.value` is `true` when P(true) is 0.5 or more.
90
+ * There is no `.confidence`.
91
+ */
92
+ export interface BooleanAnswer {
93
+ type: 'boolean'
94
+ value: boolean
95
+ /** P(true), from the wire `noul` field. */
96
+ probability: number
97
+ }
98
+
99
+ export interface EvaluateResultMeta {
100
+ /** Resolved model id from the provider. */
101
+ model: string
102
+ usage: TokenUsage
103
+ }
104
+
105
+ /**
106
+ * Map a helper question (or wire question) to its public answer type.
107
+ */
108
+ export type InferEvaluateAnswer<TQuestion> = TQuestion extends {
109
+ type: 'choice'
110
+ criteria: infer TCriteria
111
+ }
112
+ ? TCriteria extends Record<string, string | null>
113
+ ? ChoiceAnswer<Extract<keyof TCriteria, string>>
114
+ : ChoiceAnswer
115
+ : TQuestion extends { type: 'score'; criteria: infer TLevels }
116
+ ? TLevels extends ReadonlyArray<string>
117
+ ? ScoreAnswer<TLevels[number] & string>
118
+ : ScoreAnswer
119
+ : TQuestion extends { type: 'noul' }
120
+ ? BooleanAnswer
121
+ : never
122
+
123
+ /**
124
+ * Result of `decide()`. Each question key is a top-level answer.
125
+ * `meta` holds the resolved model id and usage.
126
+ */
127
+ export type EvaluateResult<TQuestions extends Record<string, WireQuestion>> = {
128
+ [K in keyof TQuestions as K extends typeof RESERVED_QUESTION_KEY
129
+ ? never
130
+ : K]: InferEvaluateAnswer<TQuestions[K]>
131
+ } & {
132
+ meta: EvaluateResultMeta
133
+ }
134
+
135
+ // ===========================
136
+ // Activity Options Types
137
+ // ===========================
138
+
139
+ /**
140
+ * Options for the evaluate activity. The model is extracted from the
141
+ * adapter's model property.
142
+ *
143
+ * @template TAdapter - The evaluate adapter type
144
+ * @template TQuestions - The questions object passed to `decide`
145
+ */
146
+ export interface EvaluateActivityOptions<
147
+ TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,
148
+ TQuestions extends Record<string, WireQuestion>,
149
+ > {
150
+ /** The evaluate adapter to use (must be created with a model) */
151
+ adapter: TAdapter & { kind: typeof kind }
152
+ /** Shared state every question judges. A JSON array is one state, not a batch. */
153
+ state: EvaluateState
154
+ /**
155
+ * Questions built with `choice`, `score`, and `boolean`.
156
+ * The key `meta` is reserved.
157
+ */
158
+ questions: TQuestions
159
+ /** Provider-specific options */
160
+ modelOptions?: EvaluateProviderOptions<TAdapter>
161
+ /** Forwarded to the provider request for cancellation. */
162
+ abortSignal?: AbortSignal
163
+ /**
164
+ * Observe-only middleware notified on start, usage, success, abort, and
165
+ * error. Pass `otelMiddleware()` to emit OpenTelemetry spans, or implement
166
+ * the `GenerationMiddleware` contract for a custom backend.
167
+ */
168
+ middleware?: Array<GenerationMiddleware>
169
+ /**
170
+ * Enable debug logging. Pass `true` to enable all categories, `false` to
171
+ * silence everything including errors, or a `DebugConfig` object for granular
172
+ * control and/or a custom `Logger`.
173
+ */
174
+ debug?: DebugOption
175
+ }
176
+
177
+ // ===========================
178
+ // Helper Functions
179
+ // ===========================
180
+
181
+ function createId(prefix: string): string {
182
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`
183
+ }
184
+
185
+ function isAbortError(error: unknown, signal?: AbortSignal): boolean {
186
+ // Prefer the error's own identity over the signal state. A genuine
187
+ // cancellation throws an abort-shaped error (DOM `AbortError`, the OpenRouter
188
+ // SDK's `RequestAbortedError`, ...). Classifying on `signal.aborted` alone
189
+ // would misroute a real failure to the abort hook whenever a shared signal
190
+ // happens to already be aborted, hiding it from `onError` observers.
191
+ if (isAbortShapedError(error)) return true
192
+ // Fall back to signal state only for non-Error throws we can't otherwise
193
+ // identify; a real Error with a non-abort name is never an abort.
194
+ return error instanceof Error ? false : signal?.aborted === true
195
+ }
196
+
197
+ function questionKeys(questions: Record<string, WireQuestion>) {
198
+ return Object.keys(questions)
199
+ }
200
+
201
+ function assertQuestions(questions: Record<string, WireQuestion>) {
202
+ const keys = questionKeys(questions)
203
+ if (keys.length === 0) {
204
+ throw new Error('decide() requires at least one question')
205
+ }
206
+ if (Object.hasOwn(questions, RESERVED_QUESTION_KEY)) {
207
+ throw new Error('decide() reserves the question key "meta"')
208
+ }
209
+ return keys
210
+ }
211
+
212
+ function mapChoiceAnswer(wire: WireChoiceAnswer, key: string) {
213
+ const probability = wire.probabilities[wire.choice]
214
+ if (typeof probability !== 'number') {
215
+ throw new Error(
216
+ `decide(): missing probability for choice "${wire.choice}" on "${key}"`,
217
+ )
218
+ }
219
+ return {
220
+ type: 'choice' as const,
221
+ value: wire.choice,
222
+ probability,
223
+ confidence: wire.confidence,
224
+ probabilities: wire.probabilities,
225
+ }
226
+ }
227
+
228
+ function mapScoreAnswer(
229
+ question: WireScoreQuestion,
230
+ wire: WireScoreAnswer,
231
+ key: string,
232
+ ) {
233
+ const levels = question.criteria
234
+ if (levels.length < 2) {
235
+ throw new Error(
236
+ `decide(): score question "${key}" needs at least two levels`,
237
+ )
238
+ }
239
+ const lastIndex = levels.length - 1
240
+ const rounded = Math.round(wire.score)
241
+ const nearestIndex =
242
+ rounded < 0 ? 0 : rounded > lastIndex ? lastIndex : rounded
243
+ const value = levels[nearestIndex]
244
+ if (value === undefined) {
245
+ throw new Error(
246
+ `decide(): score question "${key}" has no level at index ${nearestIndex}`,
247
+ )
248
+ }
249
+ const probability = wire.probabilities[String(nearestIndex)]
250
+ if (typeof probability !== 'number') {
251
+ throw new Error(
252
+ `decide(): missing probability for score level ${nearestIndex} on "${key}"`,
253
+ )
254
+ }
255
+ return {
256
+ type: 'score' as const,
257
+ value,
258
+ probability,
259
+ confidence: wire.confidence,
260
+ score: wire.score,
261
+ legend: wire.legend,
262
+ probabilities: wire.probabilities,
263
+ }
264
+ }
265
+
266
+ function mapBooleanAnswer(wire: WireNoulAnswer) {
267
+ return {
268
+ type: 'boolean' as const,
269
+ value: wire.noul >= 0.5,
270
+ probability: wire.noul,
271
+ }
272
+ }
273
+
274
+ function mapWireAnswer(question: WireQuestion, wire: WireAnswer, key: string) {
275
+ switch (question.type) {
276
+ case 'choice': {
277
+ if (wire.type !== 'choice') {
278
+ throw new Error(
279
+ `decide(): expected choice answer for "${key}", got ${wire.type}`,
280
+ )
281
+ }
282
+ return mapChoiceAnswer(wire, key)
283
+ }
284
+ case 'score': {
285
+ if (wire.type !== 'score') {
286
+ throw new Error(
287
+ `decide(): expected score answer for "${key}", got ${wire.type}`,
288
+ )
289
+ }
290
+ return mapScoreAnswer(question, wire, key)
291
+ }
292
+ case 'noul': {
293
+ if (wire.type !== 'noul') {
294
+ throw new Error(
295
+ `decide(): expected noul answer for "${key}", got ${wire.type}`,
296
+ )
297
+ }
298
+ return mapBooleanAnswer(wire)
299
+ }
300
+ }
301
+ }
302
+
303
+ function mapAnswers<TQuestions extends Record<string, WireQuestion>>(
304
+ questions: TQuestions,
305
+ wireAnswers: Record<string, WireAnswer>,
306
+ ) {
307
+ const answers = {} as {
308
+ [K in keyof TQuestions]: InferEvaluateAnswer<TQuestions[K]>
309
+ }
310
+ const keys = Object.keys(questions) as Array<keyof TQuestions>
311
+ for (const key of keys) {
312
+ const question = questions[key]
313
+ const wire = wireAnswers[String(key)]
314
+ if (question === undefined) {
315
+ throw new Error(`decide(): missing question "${String(key)}"`)
316
+ }
317
+ if (wire === undefined) {
318
+ throw new Error(`decide(): missing answer for question "${String(key)}"`)
319
+ }
320
+ answers[key] = mapWireAnswer(
321
+ question,
322
+ wire,
323
+ String(key),
324
+ ) as InferEvaluateAnswer<TQuestions[typeof key]>
325
+ }
326
+ return answers
327
+ }
328
+
329
+ function withMeta<TAnswers extends object>(
330
+ answers: TAnswers,
331
+ meta: EvaluateResultMeta,
332
+ ) {
333
+ return { ...answers, meta }
334
+ }
335
+
336
+ // ===========================
337
+ // Question helpers
338
+ // ===========================
339
+
340
+ /**
341
+ * Build a choice question. The model picks one key from `options`.
342
+ *
343
+ * Option keys become the union on `.value`. Use `null` when a key needs no
344
+ * extra description. On the wire, `options` is sent as TypeSafe `criteria`.
345
+ *
346
+ * @param options.instructions What the model should decide.
347
+ * @param options.options Map of option key to description, or `null`.
348
+ *
349
+ * @example
350
+ * ```ts
351
+ * const queue = choice({
352
+ * instructions: 'Which team should handle this ticket?',
353
+ * options: {
354
+ * billing: 'Payments, invoices, refunds',
355
+ * tech: 'Bugs, outages, integrations',
356
+ * sales: 'Pricing, upgrades, new accounts',
357
+ * },
358
+ * })
359
+ * ```
360
+ */
361
+ export function choice<
362
+ const TOptions extends Record<string, string | null>,
363
+ >(options: { instructions: EvaluateInstructions; options: TOptions }) {
364
+ return {
365
+ type: 'choice' as const,
366
+ instructions: options.instructions,
367
+ criteria: options.options,
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Build a score question. The model rates `state` on ordered `levels`.
373
+ *
374
+ * You must pass at least two levels. `.value` is the nearest level label.
375
+ * The raw fraction stays on `.score`. On the wire, `levels` is sent as
376
+ * TypeSafe `criteria`.
377
+ *
378
+ * @param options.instructions What the model should rate.
379
+ * @param options.levels Ordered labels, lowest first. At least two.
380
+ *
381
+ * @example
382
+ * ```ts
383
+ * const urgency = score({
384
+ * instructions: 'How urgent is this ticket?',
385
+ * levels: ['low', 'medium', 'high'],
386
+ * })
387
+ * ```
388
+ */
389
+ export function score<const TLevels extends ReadonlyArray<string>>(options: {
390
+ instructions: EvaluateInstructions
391
+ levels: TLevels
392
+ }) {
393
+ if (options.levels.length < 2) {
394
+ throw new Error('score() requires at least two levels')
395
+ }
396
+ return {
397
+ type: 'score' as const,
398
+ instructions: options.instructions,
399
+ criteria: options.levels,
400
+ }
401
+ }
402
+
403
+ /**
404
+ * Build a yes/no question.
405
+ *
406
+ * `.value` is `true` when P(true) is 0.5 or more. There is no `.confidence`.
407
+ * On the wire, the type is TypeSafe `noul`.
408
+ *
409
+ * @param options.instructions The yes/no question to judge.
410
+ * @param options.criteria Optional descriptions of yes and no.
411
+ *
412
+ * @example
413
+ * ```ts
414
+ * const refund = boolean({
415
+ * instructions: 'Is the customer asking for a refund?',
416
+ * })
417
+ * ```
418
+ */
419
+ export function boolean(options: {
420
+ instructions: EvaluateInstructions
421
+ criteria?: {
422
+ true?: string
423
+ false?: string
424
+ }
425
+ }) {
426
+ if (options.criteria === undefined) {
427
+ return {
428
+ type: 'noul' as const,
429
+ instructions: options.instructions,
430
+ }
431
+ }
432
+ return {
433
+ type: 'noul' as const,
434
+ instructions: options.instructions,
435
+ criteria: options.criteria,
436
+ }
437
+ }
438
+
439
+ // ===========================
440
+ // Activity Implementation
441
+ // ===========================
442
+
443
+ /**
444
+ * Ask typed questions about `state` and get answers your code can branch on.
445
+ *
446
+ * You have state (a ticket, a record, a log) and you need typed answers, not
447
+ * prose. Pass questions built with `choice`, `score`, and `boolean`. Then
448
+ * branch on `result.queue.value` in ordinary TypeScript.
449
+ *
450
+ * The question key `meta` is reserved. Throws if `questions` is empty or uses
451
+ * that key.
452
+ *
453
+ * @param options.adapter Evaluate adapter created with a model.
454
+ * @param options.state Shared state every question judges.
455
+ * @param options.questions Questions built with `choice`, `score`, `boolean`.
456
+ * @param options.modelOptions Provider-specific options.
457
+ * @param options.abortSignal Cancels the in-flight request.
458
+ * @param options.middleware Observe-only generation middleware.
459
+ * @param options.debug Debug logging option.
460
+ *
461
+ * @example Route a support ticket
462
+ * ```ts
463
+ * import { decide, choice, score, boolean } from '@tanstack/ai'
464
+ * import { typesafeDecider } from '@tanstack/ai-typesafe'
465
+ *
466
+ * const result = await decide({
467
+ * adapter: typesafeDecider('jev-latest'),
468
+ * state: ticket,
469
+ * questions: {
470
+ * queue: choice({
471
+ * instructions: 'Which team should handle this ticket?',
472
+ * options: {
473
+ * billing: 'Payments, invoices, refunds',
474
+ * tech: 'Bugs, outages, integrations',
475
+ * sales: 'Pricing, upgrades, new accounts',
476
+ * },
477
+ * }),
478
+ * urgency: score({
479
+ * instructions: 'How urgent is this ticket?',
480
+ * levels: ['low', 'medium', 'high'],
481
+ * }),
482
+ * refund: boolean({
483
+ * instructions: 'Is the customer asking for a refund?',
484
+ * }),
485
+ * },
486
+ * })
487
+ *
488
+ * result.queue.value
489
+ * result.meta.model
490
+ * result.meta.usage
491
+ * ```
492
+ */
493
+ export async function decide<
494
+ TAdapter extends EvaluateAdapter<string, EvaluateProviderOptions<TAdapter>>,
495
+ TQuestions extends Record<string, WireQuestion>,
496
+ >(options: EvaluateActivityOptions<TAdapter, TQuestions>) {
497
+ const {
498
+ adapter,
499
+ state,
500
+ questions,
501
+ modelOptions,
502
+ abortSignal,
503
+ middleware,
504
+ debug,
505
+ } = options
506
+ const model = adapter.model
507
+ const keys = assertQuestions(questions)
508
+ const requestId = createId('evaluate')
509
+ const startTime = Date.now()
510
+ const logger: InternalLogger = resolveDebugOption(debug)
511
+
512
+ const mwCtx = createGenerationContext({
513
+ requestId,
514
+ activity: 'evaluate',
515
+ provider: adapter.name,
516
+ model,
517
+ modelOptions,
518
+ createId,
519
+ })
520
+
521
+ await runGenerationStart(middleware, mwCtx)
522
+
523
+ aiEventClient.emit('evaluate:request:started', {
524
+ requestId,
525
+ provider: adapter.name,
526
+ model,
527
+ questionCount: keys.length,
528
+ timestamp: startTime,
529
+ })
530
+
531
+ logger.request(`activity=evaluate provider=${adapter.name}`, {
532
+ provider: adapter.name,
533
+ model,
534
+ questionCount: keys.length,
535
+ })
536
+
537
+ try {
538
+ const result = await adapter.evaluate({
539
+ model,
540
+ state,
541
+ questions,
542
+ modelOptions,
543
+ abortSignal,
544
+ logger,
545
+ })
546
+
547
+ const answers = mapAnswers(questions, result.answers)
548
+ const duration = Date.now() - startTime
549
+
550
+ aiEventClient.emit('evaluate:request:completed', {
551
+ requestId,
552
+ provider: adapter.name,
553
+ model: result.model,
554
+ questionCount: keys.length,
555
+ duration,
556
+ timestamp: Date.now(),
557
+ })
558
+
559
+ aiEventClient.emit('evaluate:usage', {
560
+ requestId,
561
+ model: result.model,
562
+ usage: result.usage,
563
+ timestamp: Date.now(),
564
+ })
565
+
566
+ logger.output(`activity=evaluate answers=${keys.length}`, {
567
+ answerCount: keys.length,
568
+ })
569
+
570
+ await runGenerationUsage(middleware, mwCtx, result.usage)
571
+ await runGenerationFinish(middleware, mwCtx, {
572
+ duration,
573
+ usage: result.usage,
574
+ })
575
+
576
+ return withMeta(answers, {
577
+ model: result.model,
578
+ usage: result.usage,
579
+ })
580
+ } catch (error) {
581
+ const duration = Date.now() - startTime
582
+ if (isAbortError(error, abortSignal)) {
583
+ await runGenerationAbort(middleware, mwCtx, {
584
+ reason: error instanceof Error ? error.message : undefined,
585
+ duration,
586
+ })
587
+ } else {
588
+ await runGenerationError(middleware, mwCtx, { error, duration })
589
+ }
590
+ logger.errors('evaluate activity failed', { error, source: 'evaluate' })
591
+ throw error
592
+ }
593
+ }
594
+
595
+ // Re-export adapter types
596
+ export type {
597
+ EvaluateAdapter,
598
+ EvaluateAdapterConfig,
599
+ AnyEvaluateAdapter,
600
+ EvaluateOptions,
601
+ EvaluateAdapterResult,
602
+ EvaluateState,
603
+ EvaluateInstructions,
604
+ EvaluateJsonValue,
605
+ WireQuestion,
606
+ WireAnswer,
607
+ WireChoiceQuestion,
608
+ WireScoreQuestion,
609
+ WireNoulQuestion,
610
+ WireChoiceAnswer,
611
+ WireScoreAnswer,
612
+ WireNoulAnswer,
613
+ } from './adapter'
614
+ export { BaseEvaluateAdapter } from './adapter'
@@ -1,4 +1,26 @@
1
- import type { TTSOptions, TTSResult } from '../../types'
1
+ import type {
2
+ ListVoicesOptions,
3
+ ListVoicesResult,
4
+ TTSOptions,
5
+ TTSResult,
6
+ } from '../../types'
7
+
8
+ /**
9
+ * What a TTS adapter can do beyond a single voice reading a single string.
10
+ *
11
+ * Declared statically so `generateSpeech()` can reject an unsupported request
12
+ * before it reaches the provider, instead of surfacing a provider 422.
13
+ */
14
+ export interface TTSCapabilities {
15
+ /**
16
+ * Maximum number of distinct voices accepted across `turns`
17
+ * (ElevenLabs 10, Gemini 2). Omit it when the adapter has no dialogue
18
+ * endpoint — then `turns` is rejected outright.
19
+ */
20
+ maxSpeakers?: number
21
+ /** Set when the adapter can honour `timestamps: true`. */
22
+ timestamps?: boolean
23
+ }
2
24
 
3
25
  /**
4
26
  * Configuration for TTS adapter instances
@@ -31,6 +53,11 @@ export interface TTSAdapter<
31
53
  readonly name: string
32
54
  /** The model this adapter is configured for */
33
55
  readonly model: TModel
56
+ /**
57
+ * Optional static capability declaration. Absent means "single voice, no
58
+ * timestamps" — the contract every adapter had before dialogue existed.
59
+ */
60
+ readonly capabilities?: TTSCapabilities
34
61
 
35
62
  /**
36
63
  * @internal Type-only properties for inference. Not assigned at runtime.
@@ -43,6 +70,18 @@ export interface TTSAdapter<
43
70
  * Generate speech from text
44
71
  */
45
72
  generateSpeech: (options: TTSOptions<TProviderOptions>) => Promise<TTSResult>
73
+
74
+ /**
75
+ * List the voices this account can use.
76
+ *
77
+ * Optional, because only some providers have a catalog worth querying at
78
+ * runtime. A provider whose voices are a fixed list known at build time
79
+ * publishes that list from its own package instead (`GeminiTTSVoices`, or
80
+ * the `OpenAITTSVoice` union), which is strictly better than a network
81
+ * call. Implement this only when the catalog is per-account and can change,
82
+ * which is the case wherever `generateVoice()` can add to it.
83
+ */
84
+ listVoices?: (options?: ListVoicesOptions) => Promise<ListVoicesResult>
46
85
  }
47
86
 
48
87
  /**
@@ -64,6 +103,7 @@ export abstract class BaseTTSAdapter<
64
103
  readonly kind = 'tts' as const
65
104
  abstract readonly name: string
66
105
  readonly model: TModel
106
+ declare readonly capabilities?: TTSCapabilities
67
107
 
68
108
  // Type-only property - never assigned at runtime
69
109
  declare '~types': {
@@ -81,6 +121,12 @@ export abstract class BaseTTSAdapter<
81
121
  options: TTSOptions<TProviderOptions>,
82
122
  ): Promise<TTSResult>
83
123
 
124
+ /**
125
+ * Not abstract: a provider with a fixed voice list has nothing to query and
126
+ * should not be forced to write a stub.
127
+ */
128
+ listVoices?(options?: ListVoicesOptions): Promise<ListVoicesResult>
129
+
84
130
  protected generateId(): string {
85
131
  return `${this.name}-${Date.now()}-${Math.random().toString(36).substring(7)}`
86
132
  }