smoltalk 0.8.4 → 0.10.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 (75) hide show
  1. package/README.md +145 -18
  2. package/dist/classes/ToolCall.js +18 -10
  3. package/dist/classes/message/AssistantMessage.d.ts +2 -0
  4. package/dist/classes/message/ToolMessage.js +13 -10
  5. package/dist/classes/message/UserMessage.d.ts +21 -0
  6. package/dist/classes/message/UserMessage.js +3 -0
  7. package/dist/classes/message/contentParts.d.ts +71 -2
  8. package/dist/classes/message/contentParts.js +6 -0
  9. package/dist/classes/message/index.d.ts +5 -2
  10. package/dist/classes/message/index.js +7 -0
  11. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
  12. package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
  13. package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
  14. package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
  15. package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
  16. package/dist/classes/message/renderers/JSONRenderer.js +4 -0
  17. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
  18. package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
  19. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
  20. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
  21. package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
  22. package/dist/classes/message/renderers/PartRenderer.js +3 -0
  23. package/dist/client.js +1 -0
  24. package/dist/clients/anthropic.js +1 -1
  25. package/dist/clients/baseClient.d.ts +13 -1
  26. package/dist/clients/baseClient.js +36 -7
  27. package/dist/clients/google.d.ts +2 -0
  28. package/dist/clients/google.js +125 -3
  29. package/dist/clients/ollama.js +1 -1
  30. package/dist/clients/openai.d.ts +2 -1
  31. package/dist/clients/openai.js +15 -3
  32. package/dist/clients/openaiCompat.d.ts +2 -0
  33. package/dist/clients/openaiCompat.js +5 -0
  34. package/dist/clients/openaiResponses.js +1 -1
  35. package/dist/clients/resolveAttachments.d.ts +8 -4
  36. package/dist/clients/resolveAttachments.js +101 -50
  37. package/dist/embed.d.ts +4 -0
  38. package/dist/files.d.ts +1 -1
  39. package/dist/files.js +1 -1
  40. package/dist/image/google.js +2 -2
  41. package/dist/image/openai.js +3 -3
  42. package/dist/image.d.ts +1 -1
  43. package/dist/index.d.ts +10 -2
  44. package/dist/index.js +7 -1
  45. package/dist/model.d.ts +15 -4
  46. package/dist/model.js +48 -7
  47. package/dist/models.d.ts +143 -19
  48. package/dist/models.js +137 -30
  49. package/dist/speech/baseSpeechClient.d.ts +31 -0
  50. package/dist/speech/baseSpeechClient.js +98 -0
  51. package/dist/speech/openai.d.ts +6 -0
  52. package/dist/speech/openai.js +39 -0
  53. package/dist/speech.d.ts +40 -0
  54. package/dist/speech.js +57 -0
  55. package/dist/transcription/baseTranscriptionClient.d.ts +31 -0
  56. package/dist/transcription/baseTranscriptionClient.js +107 -0
  57. package/dist/transcription/openai.d.ts +6 -0
  58. package/dist/transcription/openai.js +59 -0
  59. package/dist/transcription.d.ts +51 -0
  60. package/dist/transcription.js +58 -0
  61. package/dist/types/tokenUsage.d.ts +4 -0
  62. package/dist/types/tokenUsage.js +4 -0
  63. package/dist/types.d.ts +3 -0
  64. package/dist/util/attachments.d.ts +1 -1
  65. package/dist/util/audioMime.d.ts +9 -0
  66. package/dist/util/audioMime.js +33 -0
  67. package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
  68. package/dist/util/{imageRef.js → blobRef.js} +6 -13
  69. package/dist/util/mime.d.ts +21 -0
  70. package/dist/util/mime.js +52 -0
  71. package/dist/util/modalities.d.ts +6 -2
  72. package/dist/util/modalities.js +13 -15
  73. package/dist/util/provider.d.ts +2 -0
  74. package/dist/util/provider.js +1 -1
  75. package/package.json +1 -1
package/README.md CHANGED
@@ -8,15 +8,6 @@ Smoltalk exposes a common API to different LLM providers, with built-in cost tra
8
8
  pnpm install smoltalk
9
9
  ```
10
10
 
11
- > **Upgrading to 0.6.x?** The flat API-key/host fields on `SmolConfig` have
12
- > been removed in favor of nested `apiKey` and `baseUrl` maps. Migration:
13
- > ```diff
14
- > -{ openAiApiKey: "sk-...", googleApiKey: "...", ollamaHost: "http://..." }
15
- > +{ apiKey: { openAi: "sk-...", google: "..." }, baseUrl: { ollama: "http://..." } }
16
- > ```
17
- > Env-var fallbacks are unchanged (`OPENAI_API_KEY`, `GEMINI_API_KEY`,
18
- > `ANTHROPIC_API_KEY`, `OLLAMA_HOST`).
19
-
20
11
  ## Hello world example
21
12
 
22
13
  ```typescript
@@ -346,7 +337,7 @@ tracking starts working.
346
337
  **One model — `registerTextModel` (recommended).** Register once at startup:
347
338
 
348
339
  ```ts
349
- import { registerTextModel, textSync } from "smoltalk";
340
+ import { registerTextModel, textSync, userMessage } from "smoltalk";
350
341
 
351
342
  registerTextModel({
352
343
  modelName: "my-model",
@@ -359,6 +350,7 @@ registerTextModel({
359
350
  maxOutputTokens: 8192,
360
351
  });
361
352
 
353
+ const messages = [userMessage("hello")];
362
354
  const res = await textSync({
363
355
  model: "my-model",
364
356
  provider: "openai-compat",
@@ -373,7 +365,7 @@ per-tenant rates), pass a minimal blob for a single call. It layers over the
373
365
  baseline exactly like a refresh blob:
374
366
 
375
367
  ```ts
376
- import { textSync, type ModelDataBlob } from "smoltalk";
368
+ import { textSync, userMessage, type ModelDataBlob } from "smoltalk";
377
369
 
378
370
  const modelData: ModelDataBlob = {
379
371
  schemaVersion: 1,
@@ -392,6 +384,7 @@ const modelData: ModelDataBlob = {
392
384
  ],
393
385
  };
394
386
 
387
+ const messages = [userMessage("hello")];
395
388
  await textSync({ model: "my-model", provider: "openai-compat", messages, modelData });
396
389
  ```
397
390
 
@@ -471,20 +464,26 @@ On Google, web search can't be combined with structured output in one call.
471
464
 
472
465
  ## Registering custom providers
473
466
 
474
- Smoltalk has three registration entry points — one per capability:
467
+ Smoltalk has one registration entry point per capability:
475
468
 
476
469
  ```ts
477
470
  // example: skip-typecheck
478
471
  import {
479
- success, // Result helper
480
- registerProvider, // text generation (a class extending BaseClient)
481
- registerEmbeddingProvider, // embeddings (a function)
482
- registerImageProvider, // images (a function)
472
+ success, // Result helper
473
+ registerProvider, // text generation (a class extending BaseClient)
474
+ registerTranscriptionProvider, // speech-to-text (a class extending BaseTranscriptionClient)
475
+ registerSpeechProvider, // text-to-speech (a class extending BaseSpeechClient)
476
+ registerEmbeddingProvider, // embeddings (a function)
477
+ registerImageProvider, // images (a function)
483
478
  } from "smoltalk";
484
479
 
485
480
  // Text: a class extending BaseClient (implements _textSync / _textStream)
486
481
  registerProvider("my-llm", MyTextClient);
487
482
 
483
+ // STT/TTS: classes extending the audio base clients (see "Audio (STT/TTS)")
484
+ registerTranscriptionProvider("my-asr", MyTranscriptionClient);
485
+ registerSpeechProvider("my-tts", MySpeechClient);
486
+
488
487
  // Embeddings: a function
489
488
  registerEmbeddingProvider("my-embed", async (inputs, config) => {
490
489
  // read credentials from config (e.g. config.metadata), call your service
@@ -504,8 +503,136 @@ precedence; a registered name that collides with a built-in is ignored. Custom
504
503
  providers receive the full `config` and read their own credentials from it
505
504
  (e.g. `config.metadata`).
506
505
 
507
- Text is a class (it needs retries, tool-loop detection, streaming); embeddings
508
- and images are one-shot functions.
506
+ Text, transcription, and speech are classes: a base class owns the shared
507
+ behavior (validation, cost, error handling) and the subclass implements only
508
+ the provider call. Embeddings and images are one-shot functions.
509
+
510
+ ## Audio (STT/TTS)
511
+
512
+ Three audio primitives, all OpenAI-only in v1. `transcribe()` (speech-to-text)
513
+ and `speak()` (text-to-speech) are async and return `Result<T>` (never throw).
514
+ `audioPart()` (attach audio to a chat message) is different: it's a
515
+ synchronous plain-object constructor, not a `Result`-returning call — see
516
+ "Audio in chat" below.
517
+
518
+ ### Speech-to-text
519
+
520
+ ```ts
521
+ import { transcribe } from "smoltalk";
522
+
523
+ const result = await transcribe(
524
+ { kind: "path", path: "./meeting.mp3" },
525
+ { model: "whisper-1" },
526
+ );
527
+ if (result.success) {
528
+ console.log(result.value.text);
529
+ }
530
+ ```
531
+
532
+ `whisper-1` is the only baked-in model in v1. Options: `language`, `prompt`,
533
+ `timestampGranularity` (`"segment"` | `"word"`), `maxBytes` (a safety limit —
534
+ the effective cap is the smaller of your limit and the model's declared upload
535
+ cap, 25 MB for `whisper-1`). The result carries `text` plus optional
536
+ `language`, `durationSeconds`, `segments`, `words`, `usage`, and `cost`.
537
+
538
+ Model constraints (accepted MIME types, upload cap, per-minute price) live in
539
+ the model registry, not in code — a model you add via `registerModelData` /
540
+ `config.modelData` is validated against whatever its data block declares, and
541
+ a model with no registry entry skips validation entirely (the provider is then
542
+ the authority).
543
+
544
+ Register a custom provider as a class:
545
+
546
+ ```ts
547
+ // example: skip-typecheck
548
+ import { BaseTranscriptionClient, registerTranscriptionProvider, success } from "smoltalk";
549
+
550
+ class AcmeTranscription extends BaseTranscriptionClient {
551
+ protected async _transcribe(data: Uint8Array, mimeType: string) {
552
+ // call your API with this.config.apiKey; map the response
553
+ return success({ text: "..." });
554
+ }
555
+ }
556
+ registerTranscriptionProvider("acme", AcmeTranscription);
557
+
558
+ // then: transcribe(source, { model: "acme-1", provider: "acme", apiKey: { acme: "..." } })
559
+ ```
560
+
561
+ The base class owns blob loading, model-data validation, cost, and the
562
+ redacting error boundary; `_transcribe()` is only the SDK call + response
563
+ mapping.
564
+
565
+ ### Text-to-speech
566
+
567
+ ```ts
568
+ import { speak } from "smoltalk";
569
+ import { writeFile } from "node:fs/promises";
570
+
571
+ const result = await speak("Hello from smoltalk.", {
572
+ model: "tts-1",
573
+ voice: "alloy",
574
+ });
575
+ if (result.success) {
576
+ await writeFile("out.mp3", result.value.audio); // caller owns the bytes
577
+ }
578
+ ```
579
+
580
+ `tts-1` and `tts-1-hd` are the only baked-in models in v1. `voice` is
581
+ required. Options: `format` (OpenAI accepts `"mp3"` | `"opus"` | `"aac"` |
582
+ `"flac"` | `"wav"` | `"pcm"`, default `"mp3"`; a custom provider may accept
583
+ other strings) and `speed`. Limits are declared per model in the registry —
584
+ for `tts-1`/`tts-1-hd` that's a 4096-code-point input cap, a 0.25–4.0 speed
585
+ range, and the format list above; exceeding any of them returns a `Failure`
586
+ before the request is sent. The returned `audio` is a `Uint8Array` you own —
587
+ write it to disk, stream it, whatever you like. When `format` is `"pcm"`,
588
+ `result.pcm` describes the raw stream (for OpenAI:
589
+ `{ sampleRateHz: 24000, sampleFormat: "s16le", channels: 1 }`).
590
+
591
+ Register a custom provider as a class, mirroring transcription:
592
+
593
+ ```ts
594
+ // example: skip-typecheck
595
+ import { BaseSpeechClient, registerSpeechProvider, success } from "smoltalk";
596
+
597
+ class AcmeSpeech extends BaseSpeechClient {
598
+ protected async _speak(text: string) {
599
+ // call your API with this.config.apiKey / this.config.voice
600
+ return success({ audio: new Uint8Array(), mimeType: "audio/mpeg" });
601
+ }
602
+ }
603
+ registerSpeechProvider("acme", AcmeSpeech);
604
+ ```
605
+
606
+ As with transcription, per-model constraints come from the model registry
607
+ (`registerModelData` / `config.modelData`), so a custom model's caps, speed
608
+ range, and formats are data, not code.
609
+
610
+ ### Audio in chat
611
+
612
+ `audioPart()` attaches an audio clip to a `userMessage`, for models that
613
+ accept audio input directly (as opposed to transcribing it first).
614
+ `audioPart()` itself is a synchronous constructor that builds a content part
615
+ — it always returns an `AudioPart`, never a `Result`, and it can't fail:
616
+
617
+ ```ts
618
+ import { textSync, userMessage, audioPart } from "smoltalk";
619
+
620
+ const messages = [
621
+ userMessage([
622
+ "What's being said in this clip?",
623
+ audioPart({ kind: "path", path: "./clip.wav" }),
624
+ ]),
625
+ ];
626
+
627
+ const resp = await textSync({ messages, model: "gpt-audio-1.5" });
628
+ ```
629
+
630
+ In v1 this only works with `gpt-audio-1.5` on the OpenAI Chat Completions
631
+ provider (not `openai-responses`, and not other providers). Validation
632
+ happens later, when the message is sent via `textSync`/`textStream` — an
633
+ unsupported provider, a model without audio input, or audio that isn't
634
+ `mp3`/`wav` surfaces as a `Failure` from that call, not from `audioPart()`
635
+ itself. Audio is inlined as base64, not uploaded via the Files API.
509
636
 
510
637
  ## Limitations
511
638
  Smoltalk has support for a limited number of providers right now, and is mostly focused on the stateless APIs for text completion, though I plan to add support for more providers as well as image and speech models later. Smoltalk is also a personal project, and there are alternatives backed by companies:
@@ -79,17 +79,25 @@ export class ToolCall {
79
79
  };
80
80
  }
81
81
  toGoogle() {
82
- return {
83
- functionCall: {
84
- name: this.name,
85
- args: this.arguments,
86
- },
87
- // Gemini 3 requires the original thought signature echoed back on the
88
- // function-call part; omitting it fails validation during tool use.
89
- ...(this._thoughtSignature !== undefined && {
90
- thoughtSignature: this._thoughtSignature,
91
- }),
82
+ const functionCall = {
83
+ name: this.name,
84
+ args: this.arguments,
92
85
  };
86
+ // Echo the id when we have one: the Gemini API pairs a functionResponse
87
+ // back to its functionCall by id when present. Omit it when empty — current
88
+ // Gemini 3 preview models issue no ids, and an empty id is not a valid key.
89
+ if (this._id !== "") {
90
+ functionCall.id = this._id;
91
+ }
92
+ const result = {
93
+ functionCall,
94
+ };
95
+ // Gemini 3 requires the original thought signature echoed back on the
96
+ // function-call part; omitting it fails validation during tool use.
97
+ if (this._thoughtSignature !== undefined) {
98
+ result.thoughtSignature = this._thoughtSignature;
99
+ }
100
+ return result;
93
101
  }
94
102
  toOpenAIResponseInputItem() {
95
103
  return {
@@ -31,6 +31,8 @@ export declare const AssistantMessageJSONSchema: z.ZodObject<{
31
31
  outputTokens: z.ZodNumber;
32
32
  cachedInputTokens: z.ZodOptional<z.ZodNumber>;
33
33
  cacheCreationInputTokens: z.ZodOptional<z.ZodNumber>;
34
+ inputAudioTokens: z.ZodOptional<z.ZodNumber>;
35
+ outputAudioTokens: z.ZodOptional<z.ZodNumber>;
34
36
  totalTokens: z.ZodOptional<z.ZodNumber>;
35
37
  }, z.core.$strip>>;
36
38
  cost: z.ZodOptional<z.ZodObject<{
@@ -91,18 +91,21 @@ export class ToolMessage extends BaseMessage {
91
91
  };
92
92
  }
93
93
  toGoogleMessage() {
94
+ const functionResponse = {
95
+ name: this.name,
96
+ response: {
97
+ result: this.content,
98
+ },
99
+ };
100
+ // Echo the id so Gemini can pair this response to its call by id — the
101
+ // documented matching mechanism. Only when non-empty: current Gemini 3
102
+ // preview models issue no ids, and an empty id is not a valid key.
103
+ if (this.tool_call_id !== "") {
104
+ functionResponse.id = this.tool_call_id;
105
+ }
94
106
  return {
95
107
  role: "user",
96
- parts: [
97
- {
98
- functionResponse: {
99
- name: this.name,
100
- response: {
101
- result: this.content,
102
- },
103
- },
104
- },
105
- ],
108
+ parts: [{ functionResponse }],
106
109
  };
107
110
  }
108
111
  toOllamaMessage() {
@@ -65,6 +65,27 @@ export declare const UserMessageJSONSchema: z.ZodObject<{
65
65
  expiresAt: z.ZodOptional<z.ZodNumber>;
66
66
  }, z.core.$strip>], "kind">;
67
67
  filename: z.ZodOptional<z.ZodString>;
68
+ }, z.core.$strip>, z.ZodObject<{
69
+ type: z.ZodLiteral<"audio">;
70
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
71
+ kind: z.ZodLiteral<"bytes">;
72
+ data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
73
+ mimeType: z.ZodString;
74
+ }, z.core.$strip>, z.ZodObject<{
75
+ kind: z.ZodLiteral<"base64">;
76
+ base64: z.ZodString;
77
+ mimeType: z.ZodString;
78
+ }, z.core.$strip>, z.ZodObject<{
79
+ kind: z.ZodLiteral<"path">;
80
+ path: z.ZodString;
81
+ mimeType: z.ZodOptional<z.ZodString>;
82
+ }, z.core.$strip>, z.ZodObject<{
83
+ kind: z.ZodLiteral<"url">;
84
+ url: z.ZodString;
85
+ mimeType: z.ZodOptional<z.ZodString>;
86
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
87
+ }, z.core.$strip>], "kind">;
88
+ filename: z.ZodOptional<z.ZodString>;
68
89
  }, z.core.$strip>], "type">>]>;
69
90
  name: z.ZodOptional<z.ZodString>;
70
91
  rawData: z.ZodOptional<z.ZodAny>;
@@ -108,6 +108,9 @@ export class UserMessage extends BaseMessage {
108
108
  images.push(refToBase64(part.source).base64);
109
109
  continue;
110
110
  }
111
+ if (part.type === "audio") {
112
+ throw new Error("Ollama does not support audio input.");
113
+ }
111
114
  if (part.source.kind === "providerFile") {
112
115
  throw new Error("Ollama does not support provider file references.");
113
116
  }
@@ -1,5 +1,5 @@
1
1
  import { z } from "zod";
2
- import type { ImageRef } from "../../util/imageRef.js";
2
+ import type { ImageRef, BlobRef } from "../../util/blobRef.js";
3
3
  export type TextPart = {
4
4
  type: "text";
5
5
  text: string;
@@ -13,7 +13,12 @@ export type FilePart = {
13
13
  source: AttachmentSource;
14
14
  filename?: string;
15
15
  };
16
- export type UserContentPart = TextPart | ImagePart | FilePart;
16
+ export type AudioPart = {
17
+ type: "audio";
18
+ source: BlobRef;
19
+ filename?: string;
20
+ };
21
+ export type UserContentPart = TextPart | ImagePart | FilePart | AudioPart;
17
22
  /** Normalized user-message content: a plain string or an array of typed parts. */
18
23
  export type UserContent = string | UserContentPart[];
19
24
  /** What callers may pass: a bare string element is sugar for a text part. */
@@ -139,6 +144,28 @@ export declare const FilePartSchema: z.ZodObject<{
139
144
  }, z.core.$strip>], "kind">;
140
145
  filename: z.ZodOptional<z.ZodString>;
141
146
  }, z.core.$strip>;
147
+ export declare const AudioPartSchema: z.ZodObject<{
148
+ type: z.ZodLiteral<"audio">;
149
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
150
+ kind: z.ZodLiteral<"bytes">;
151
+ data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
152
+ mimeType: z.ZodString;
153
+ }, z.core.$strip>, z.ZodObject<{
154
+ kind: z.ZodLiteral<"base64">;
155
+ base64: z.ZodString;
156
+ mimeType: z.ZodString;
157
+ }, z.core.$strip>, z.ZodObject<{
158
+ kind: z.ZodLiteral<"path">;
159
+ path: z.ZodString;
160
+ mimeType: z.ZodOptional<z.ZodString>;
161
+ }, z.core.$strip>, z.ZodObject<{
162
+ kind: z.ZodLiteral<"url">;
163
+ url: z.ZodString;
164
+ mimeType: z.ZodOptional<z.ZodString>;
165
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
166
+ }, z.core.$strip>], "kind">;
167
+ filename: z.ZodOptional<z.ZodString>;
168
+ }, z.core.$strip>;
142
169
  export declare const UserContentPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
143
170
  type: z.ZodLiteral<"text">;
144
171
  text: z.ZodString;
@@ -197,6 +224,27 @@ export declare const UserContentPartSchema: z.ZodDiscriminatedUnion<[z.ZodObject
197
224
  expiresAt: z.ZodOptional<z.ZodNumber>;
198
225
  }, z.core.$strip>], "kind">;
199
226
  filename: z.ZodOptional<z.ZodString>;
227
+ }, z.core.$strip>, z.ZodObject<{
228
+ type: z.ZodLiteral<"audio">;
229
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
230
+ kind: z.ZodLiteral<"bytes">;
231
+ data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
232
+ mimeType: z.ZodString;
233
+ }, z.core.$strip>, z.ZodObject<{
234
+ kind: z.ZodLiteral<"base64">;
235
+ base64: z.ZodString;
236
+ mimeType: z.ZodString;
237
+ }, z.core.$strip>, z.ZodObject<{
238
+ kind: z.ZodLiteral<"path">;
239
+ path: z.ZodString;
240
+ mimeType: z.ZodOptional<z.ZodString>;
241
+ }, z.core.$strip>, z.ZodObject<{
242
+ kind: z.ZodLiteral<"url">;
243
+ url: z.ZodString;
244
+ mimeType: z.ZodOptional<z.ZodString>;
245
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
246
+ }, z.core.$strip>], "kind">;
247
+ filename: z.ZodOptional<z.ZodString>;
200
248
  }, z.core.$strip>], "type">;
201
249
  export declare const UserContentSchema: z.ZodUnion<readonly [z.ZodString, z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
202
250
  type: z.ZodLiteral<"text">;
@@ -256,4 +304,25 @@ export declare const UserContentSchema: z.ZodUnion<readonly [z.ZodString, z.ZodA
256
304
  expiresAt: z.ZodOptional<z.ZodNumber>;
257
305
  }, z.core.$strip>], "kind">;
258
306
  filename: z.ZodOptional<z.ZodString>;
307
+ }, z.core.$strip>, z.ZodObject<{
308
+ type: z.ZodLiteral<"audio">;
309
+ source: z.ZodDiscriminatedUnion<[z.ZodObject<{
310
+ kind: z.ZodLiteral<"bytes">;
311
+ data: z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>;
312
+ mimeType: z.ZodString;
313
+ }, z.core.$strip>, z.ZodObject<{
314
+ kind: z.ZodLiteral<"base64">;
315
+ base64: z.ZodString;
316
+ mimeType: z.ZodString;
317
+ }, z.core.$strip>, z.ZodObject<{
318
+ kind: z.ZodLiteral<"path">;
319
+ path: z.ZodString;
320
+ mimeType: z.ZodOptional<z.ZodString>;
321
+ }, z.core.$strip>, z.ZodObject<{
322
+ kind: z.ZodLiteral<"url">;
323
+ url: z.ZodString;
324
+ mimeType: z.ZodOptional<z.ZodString>;
325
+ timeoutMs: z.ZodOptional<z.ZodNumber>;
326
+ }, z.core.$strip>], "kind">;
327
+ filename: z.ZodOptional<z.ZodString>;
259
328
  }, z.core.$strip>], "type">>]>;
@@ -36,9 +36,15 @@ export const FilePartSchema = z.object({
36
36
  source: AttachmentSourceSchema,
37
37
  filename: z.string().optional(),
38
38
  });
39
+ export const AudioPartSchema = z.object({
40
+ type: z.literal("audio"),
41
+ source: z.discriminatedUnion("kind", [...ImageRefSchema.options]),
42
+ filename: z.string().optional(),
43
+ });
39
44
  export const UserContentPartSchema = z.discriminatedUnion("type", [
40
45
  TextPartSchema,
41
46
  ImagePartSchema,
42
47
  FilePartSchema,
48
+ AudioPartSchema,
43
49
  ]);
44
50
  export const UserContentSchema = z.union([z.string(), z.array(UserContentPartSchema)]);
@@ -8,8 +8,8 @@ import type { AssistantMessageJSON } from "./AssistantMessage.js";
8
8
  import type { DeveloperMessageJSON } from "./DeveloperMessage.js";
9
9
  import type { SystemMessageJSON } from "./SystemMessage.js";
10
10
  import type { ToolMessageJSON } from "./ToolMessage.js";
11
- import { CostEstimate, TextPart, TokenUsage, UserContentInput, ImagePart, FilePart } from "../../types.js";
12
- import type { ImageRef } from "../../util/imageRef.js";
11
+ import { CostEstimate, TextPart, TokenUsage, UserContentInput, ImagePart, FilePart, AudioPart } from "../../types.js";
12
+ import type { ImageRef, BlobRef } from "../../util/blobRef.js";
13
13
  export * from "./AssistantMessage.js";
14
14
  export * from "./BaseMessage.js";
15
15
  export * from "./DeveloperMessage.js";
@@ -26,6 +26,9 @@ export declare function imagePart(source: ImageRef): ImagePart;
26
26
  export declare function filePart(source: ImageRef, options?: {
27
27
  filename?: string;
28
28
  }): FilePart;
29
+ export declare function audioPart(source: BlobRef, options?: {
30
+ filename?: string;
31
+ }): AudioPart;
29
32
  export declare function assistantMessage(content: string | Array<TextPart> | null, options?: {
30
33
  name?: string;
31
34
  audio?: any | null;
@@ -40,6 +40,13 @@ export function filePart(source, options = {}) {
40
40
  }
41
41
  return { type: "file", source, filename: options.filename };
42
42
  }
43
+ export function audioPart(source, options = {}) {
44
+ const part = { type: "audio", source };
45
+ if (options.filename !== undefined) {
46
+ part.filename = options.filename;
47
+ }
48
+ return part;
49
+ }
43
50
  export function assistantMessage(content, options = {}) {
44
51
  return new AssistantMessage(content, options);
45
52
  }
@@ -1,5 +1,5 @@
1
1
  import type { PartRenderer } from "./PartRenderer.js";
2
- import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
2
+ import type { TextPart, ImagePart, FilePart, AudioPart } from "../contentParts.js";
3
3
  /** Renders parts for the Anthropic Messages API. */
4
4
  export declare class AnthropicRenderer implements PartRenderer<any> {
5
5
  text(part: TextPart): {
@@ -40,4 +40,5 @@ export declare class AnthropicRenderer implements PartRenderer<any> {
40
40
  data: string;
41
41
  };
42
42
  };
43
+ audio(_part: AudioPart): any;
43
44
  }
@@ -16,4 +16,7 @@ export class AnthropicRenderer {
16
16
  }
17
17
  return { type: "document", source: anthropicSource(part.source) };
18
18
  }
19
+ audio(_part) {
20
+ throw new Error("Audio input is not supported for this provider in v1.");
21
+ }
19
22
  }
@@ -1,5 +1,5 @@
1
1
  import type { PartRenderer } from "./PartRenderer.js";
2
- import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
2
+ import type { TextPart, ImagePart, FilePart, AudioPart } from "../contentParts.js";
3
3
  /** Renders parts for the Google Gemini API. */
4
4
  export declare class GoogleRenderer implements PartRenderer<any> {
5
5
  text(part: TextPart): {
@@ -31,5 +31,6 @@ export declare class GoogleRenderer implements PartRenderer<any> {
31
31
  };
32
32
  fileData?: undefined;
33
33
  };
34
+ audio(_part: AudioPart): any;
34
35
  private sourcePart;
35
36
  }
@@ -10,6 +10,9 @@ export class GoogleRenderer {
10
10
  file(part) {
11
11
  return this.sourcePart(part.source);
12
12
  }
13
+ audio(_part) {
14
+ throw new Error("Audio input is not supported for this provider in v1.");
15
+ }
13
16
  sourcePart(source) {
14
17
  if (source.kind === "providerFile") {
15
18
  // `fileData` needs both a uri and a mimeType; a ref missing either (e.g. a
@@ -1,8 +1,9 @@
1
1
  import type { PartRenderer } from "./PartRenderer.js";
2
- import type { TextPart, ImagePart, FilePart, UserContentPart } from "../contentParts.js";
2
+ import type { TextPart, ImagePart, FilePart, AudioPart, UserContentPart } from "../contentParts.js";
3
3
  /** Renders parts back to a JSON-safe UserContentPart (for toJSON serialization). */
4
4
  export declare class JSONRenderer implements PartRenderer<UserContentPart> {
5
5
  text(part: TextPart): UserContentPart;
6
6
  image(part: ImagePart): UserContentPart;
7
7
  file(part: FilePart): UserContentPart;
8
+ audio(part: AudioPart): UserContentPart;
8
9
  }
@@ -18,4 +18,8 @@ export class JSONRenderer {
18
18
  file(part) {
19
19
  return { type: "file", source: bytesToBase64(part.source), filename: part.filename };
20
20
  }
21
+ audio(part) {
22
+ // bytesToBase64 only converts `bytes`; other kinds pass through, all within BlobRef.
23
+ return { type: "audio", source: bytesToBase64(part.source), filename: part.filename };
24
+ }
21
25
  }
@@ -1,5 +1,5 @@
1
1
  import type { PartRenderer } from "./PartRenderer.js";
2
- import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
2
+ import type { TextPart, ImagePart, FilePart, AudioPart } from "../contentParts.js";
3
3
  /** Renders parts for the OpenAI Chat Completions API. */
4
4
  export declare class OpenAIChatRenderer implements PartRenderer<any> {
5
5
  text(part: TextPart): {
@@ -27,4 +27,11 @@ export declare class OpenAIChatRenderer implements PartRenderer<any> {
27
27
  file_id?: undefined;
28
28
  };
29
29
  };
30
+ audio(part: AudioPart): {
31
+ type: string;
32
+ input_audio: {
33
+ data: string;
34
+ format: "mp3" | "wav";
35
+ };
36
+ };
30
37
  }
@@ -1,4 +1,11 @@
1
1
  import { refToBase64, toDataUri, openAiImageUrl, attachmentFilename } from "../../../util/attachments.js";
2
+ import { chatAudioFormat } from "../../../util/audioMime.js";
3
+ function requirePreparedAudioPart(part) {
4
+ if (part.source.kind !== "base64") {
5
+ throw new Error("internal: audio source must be prepared as base64 before rendering");
6
+ }
7
+ return part;
8
+ }
2
9
  /** Renders parts for the OpenAI Chat Completions API. */
3
10
  export class OpenAIChatRenderer {
4
11
  text(part) {
@@ -17,4 +24,15 @@ export class OpenAIChatRenderer {
17
24
  const { base64, mimeType } = refToBase64(part.source);
18
25
  return { type: "file", file: { file_data: toDataUri(base64, mimeType), filename: attachmentFilename(part.filename) } };
19
26
  }
27
+ audio(part) {
28
+ const prepared = requirePreparedAudioPart(part);
29
+ const format = chatAudioFormat(prepared.source.mimeType);
30
+ if (!format) {
31
+ throw new Error(`Chat audio supports only mp3/wav; got "${prepared.source.mimeType}".`);
32
+ }
33
+ return {
34
+ type: "input_audio",
35
+ input_audio: { data: prepared.source.base64, format },
36
+ };
37
+ }
20
38
  }
@@ -1,5 +1,5 @@
1
1
  import type { PartRenderer } from "./PartRenderer.js";
2
- import type { TextPart, ImagePart, FilePart } from "../contentParts.js";
2
+ import type { TextPart, ImagePart, FilePart, AudioPart } from "../contentParts.js";
3
3
  /** Renders parts for the OpenAI Responses API. */
4
4
  export declare class OpenAIResponsesRenderer implements PartRenderer<any> {
5
5
  text(part: TextPart): {
@@ -36,4 +36,5 @@ export declare class OpenAIResponsesRenderer implements PartRenderer<any> {
36
36
  file_id?: undefined;
37
37
  file_url?: undefined;
38
38
  };
39
+ audio(_part: AudioPart): any;
39
40
  }
@@ -20,4 +20,7 @@ export class OpenAIResponsesRenderer {
20
20
  const { base64, mimeType } = refToBase64(part.source);
21
21
  return { type: "input_file", file_data: toDataUri(base64, mimeType), filename: attachmentFilename(part.filename) };
22
22
  }
23
+ audio(_part) {
24
+ throw new Error("Audio input is not supported for this provider in v1.");
25
+ }
23
26
  }
@@ -1,14 +1,15 @@
1
- import type { TextPart, ImagePart, FilePart, UserContentPart } from "../contentParts.js";
1
+ import type { TextPart, ImagePart, FilePart, AudioPart, UserContentPart } from "../contentParts.js";
2
2
  /**
3
3
  * A per-provider renderer for user-content parts. Each provider implements one
4
4
  * of these (one class per file, in this directory) so the message class stays
5
5
  * thin: the serializer picks a renderer and walks the parts with {@link renderParts}.
6
- * `text`/`image`/`file` return that provider's native representation of a part.
6
+ * `text`/`image`/`file`/`audio` return that provider's native representation of a part.
7
7
  */
8
8
  export interface PartRenderer<T> {
9
9
  text(part: TextPart): T;
10
10
  image(part: ImagePart): T;
11
11
  file(part: FilePart): T;
12
+ audio(part: AudioPart): T;
12
13
  }
13
14
  /** Walk content parts, dispatching each to the renderer's matching method. */
14
15
  export declare function renderParts<T>(parts: UserContentPart[], renderer: PartRenderer<T>): T[];
@@ -8,6 +8,9 @@ export function renderParts(parts, renderer) {
8
8
  else if (part.type === "image") {
9
9
  out.push(renderer.image(part));
10
10
  }
11
+ else if (part.type === "audio") {
12
+ out.push(renderer.audio(part));
13
+ }
11
14
  else {
12
15
  out.push(renderer.file(part));
13
16
  }