smoltalk 0.9.0 → 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 (72) hide show
  1. package/README.md +141 -7
  2. package/dist/classes/message/AssistantMessage.d.ts +2 -0
  3. package/dist/classes/message/UserMessage.d.ts +21 -0
  4. package/dist/classes/message/UserMessage.js +3 -0
  5. package/dist/classes/message/contentParts.d.ts +71 -2
  6. package/dist/classes/message/contentParts.js +6 -0
  7. package/dist/classes/message/index.d.ts +5 -2
  8. package/dist/classes/message/index.js +7 -0
  9. package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
  10. package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
  11. package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
  12. package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
  13. package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
  14. package/dist/classes/message/renderers/JSONRenderer.js +4 -0
  15. package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
  16. package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
  17. package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
  18. package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
  19. package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
  20. package/dist/classes/message/renderers/PartRenderer.js +3 -0
  21. package/dist/client.js +1 -0
  22. package/dist/clients/anthropic.js +1 -1
  23. package/dist/clients/baseClient.d.ts +13 -1
  24. package/dist/clients/baseClient.js +36 -7
  25. package/dist/clients/google.js +1 -1
  26. package/dist/clients/ollama.js +1 -1
  27. package/dist/clients/openai.d.ts +2 -1
  28. package/dist/clients/openai.js +15 -3
  29. package/dist/clients/openaiCompat.d.ts +2 -0
  30. package/dist/clients/openaiCompat.js +5 -0
  31. package/dist/clients/openaiResponses.js +1 -1
  32. package/dist/clients/resolveAttachments.d.ts +8 -4
  33. package/dist/clients/resolveAttachments.js +101 -50
  34. package/dist/embed.d.ts +4 -0
  35. package/dist/files.d.ts +1 -1
  36. package/dist/files.js +1 -1
  37. package/dist/image/google.js +2 -2
  38. package/dist/image/openai.js +3 -3
  39. package/dist/image.d.ts +1 -1
  40. package/dist/index.d.ts +10 -2
  41. package/dist/index.js +7 -1
  42. package/dist/model.d.ts +15 -4
  43. package/dist/model.js +48 -7
  44. package/dist/models.d.ts +143 -19
  45. package/dist/models.js +137 -30
  46. package/dist/speech/baseSpeechClient.d.ts +31 -0
  47. package/dist/speech/baseSpeechClient.js +98 -0
  48. package/dist/speech/openai.d.ts +6 -0
  49. package/dist/speech/openai.js +39 -0
  50. package/dist/speech.d.ts +40 -0
  51. package/dist/speech.js +57 -0
  52. package/dist/transcription/baseTranscriptionClient.d.ts +31 -0
  53. package/dist/transcription/baseTranscriptionClient.js +107 -0
  54. package/dist/transcription/openai.d.ts +6 -0
  55. package/dist/transcription/openai.js +59 -0
  56. package/dist/transcription.d.ts +51 -0
  57. package/dist/transcription.js +58 -0
  58. package/dist/types/tokenUsage.d.ts +4 -0
  59. package/dist/types/tokenUsage.js +4 -0
  60. package/dist/types.d.ts +3 -0
  61. package/dist/util/attachments.d.ts +1 -1
  62. package/dist/util/audioMime.d.ts +9 -0
  63. package/dist/util/audioMime.js +33 -0
  64. package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
  65. package/dist/util/{imageRef.js → blobRef.js} +6 -13
  66. package/dist/util/mime.d.ts +21 -0
  67. package/dist/util/mime.js +52 -0
  68. package/dist/util/modalities.d.ts +6 -2
  69. package/dist/util/modalities.js +13 -15
  70. package/dist/util/provider.d.ts +2 -0
  71. package/dist/util/provider.js +1 -1
  72. package/package.json +1 -1
package/README.md CHANGED
@@ -464,20 +464,26 @@ On Google, web search can't be combined with structured output in one call.
464
464
 
465
465
  ## Registering custom providers
466
466
 
467
- Smoltalk has three registration entry points — one per capability:
467
+ Smoltalk has one registration entry point per capability:
468
468
 
469
469
  ```ts
470
470
  // example: skip-typecheck
471
471
  import {
472
- success, // Result helper
473
- registerProvider, // text generation (a class extending BaseClient)
474
- registerEmbeddingProvider, // embeddings (a function)
475
- 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)
476
478
  } from "smoltalk";
477
479
 
478
480
  // Text: a class extending BaseClient (implements _textSync / _textStream)
479
481
  registerProvider("my-llm", MyTextClient);
480
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
+
481
487
  // Embeddings: a function
482
488
  registerEmbeddingProvider("my-embed", async (inputs, config) => {
483
489
  // read credentials from config (e.g. config.metadata), call your service
@@ -497,8 +503,136 @@ precedence; a registered name that collides with a built-in is ignored. Custom
497
503
  providers receive the full `config` and read their own credentials from it
498
504
  (e.g. `config.metadata`).
499
505
 
500
- Text is a class (it needs retries, tool-loop detection, streaming); embeddings
501
- 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.
502
636
 
503
637
  ## Limitations
504
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:
@@ -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<{
@@ -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
  }
package/dist/client.js CHANGED
@@ -62,6 +62,7 @@ export function getClient(config) {
62
62
  ...config,
63
63
  apiKey: { ...resolvedKeys, ...config.apiKey },
64
64
  model: modelName,
65
+ provider,
65
66
  };
66
67
  switch (provider) {
67
68
  case "anthropic":
@@ -204,7 +204,7 @@ export class SmolAnthropic extends BaseClient {
204
204
  }
205
205
  this.client = new Anthropic({ apiKey });
206
206
  this.logger = getLogger();
207
- this.model = new Model(config.model, undefined, config.modelData);
207
+ this.model = new Model(config.model, config.provider, config.modelData);
208
208
  }
209
209
  getModel() {
210
210
  return this.model.getModel();
@@ -1,5 +1,11 @@
1
1
  import { StatelogClient } from "../statelogClient.js";
2
2
  import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
3
+ export type ClientAttachmentCapabilities = {
4
+ /** Non-audio attachment modalities this client's serializers can render. */
5
+ inputModalities: readonly ("image" | "pdf")[];
6
+ /** Audio containers (by primary extension) accepted inline; empty = no audio. */
7
+ audioFormats: readonly string[];
8
+ };
3
9
  export declare class BaseClient implements SmolClient {
4
10
  protected config: SmolConfig;
5
11
  protected statelogClient?: StatelogClient;
@@ -14,7 +20,13 @@ export declare class BaseClient implements SmolClient {
14
20
  }): Promise<Result<PromptResult>>;
15
21
  checkMessageLimit(promptConfig: SmolConfig): Result<PromptResult> | null;
16
22
  /**
17
- * Gate on input modalities and resolve any image/PDF attachment refs
23
+ * What this client can accept as attachments. Subclasses override to declare
24
+ * more (or fewer). Checked against the messages, alongside the model's own
25
+ * declared modalities, before any serialization runs.
26
+ */
27
+ protected attachmentCapabilities(): ClientAttachmentCapabilities;
28
+ /**
29
+ * Gate on input modalities and resolve any image/PDF/audio attachment refs
18
30
  * (path/url/bytes → base64) before the synchronous serializers run. Returns
19
31
  * the (possibly rewritten) config on success, or a Failure to surface. Shared
20
32
  * by textSync and textStream so the two paths can't diverge.