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.
- package/README.md +141 -7
- package/dist/classes/message/AssistantMessage.d.ts +2 -0
- package/dist/classes/message/UserMessage.d.ts +21 -0
- package/dist/classes/message/UserMessage.js +3 -0
- package/dist/classes/message/contentParts.d.ts +71 -2
- package/dist/classes/message/contentParts.js +6 -0
- package/dist/classes/message/index.d.ts +5 -2
- package/dist/classes/message/index.js +7 -0
- package/dist/classes/message/renderers/AnthropicRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/AnthropicRenderer.js +3 -0
- package/dist/classes/message/renderers/GoogleRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/GoogleRenderer.js +3 -0
- package/dist/classes/message/renderers/JSONRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/JSONRenderer.js +4 -0
- package/dist/classes/message/renderers/OpenAIChatRenderer.d.ts +8 -1
- package/dist/classes/message/renderers/OpenAIChatRenderer.js +18 -0
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.d.ts +2 -1
- package/dist/classes/message/renderers/OpenAIResponsesRenderer.js +3 -0
- package/dist/classes/message/renderers/PartRenderer.d.ts +3 -2
- package/dist/classes/message/renderers/PartRenderer.js +3 -0
- package/dist/client.js +1 -0
- package/dist/clients/anthropic.js +1 -1
- package/dist/clients/baseClient.d.ts +13 -1
- package/dist/clients/baseClient.js +36 -7
- package/dist/clients/google.js +1 -1
- package/dist/clients/ollama.js +1 -1
- package/dist/clients/openai.d.ts +2 -1
- package/dist/clients/openai.js +15 -3
- package/dist/clients/openaiCompat.d.ts +2 -0
- package/dist/clients/openaiCompat.js +5 -0
- package/dist/clients/openaiResponses.js +1 -1
- package/dist/clients/resolveAttachments.d.ts +8 -4
- package/dist/clients/resolveAttachments.js +101 -50
- package/dist/embed.d.ts +4 -0
- package/dist/files.d.ts +1 -1
- package/dist/files.js +1 -1
- package/dist/image/google.js +2 -2
- package/dist/image/openai.js +3 -3
- package/dist/image.d.ts +1 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +7 -1
- package/dist/model.d.ts +15 -4
- package/dist/model.js +48 -7
- package/dist/models.d.ts +143 -19
- package/dist/models.js +137 -30
- package/dist/speech/baseSpeechClient.d.ts +31 -0
- package/dist/speech/baseSpeechClient.js +98 -0
- package/dist/speech/openai.d.ts +6 -0
- package/dist/speech/openai.js +39 -0
- package/dist/speech.d.ts +40 -0
- package/dist/speech.js +57 -0
- package/dist/transcription/baseTranscriptionClient.d.ts +31 -0
- package/dist/transcription/baseTranscriptionClient.js +107 -0
- package/dist/transcription/openai.d.ts +6 -0
- package/dist/transcription/openai.js +59 -0
- package/dist/transcription.d.ts +51 -0
- package/dist/transcription.js +58 -0
- package/dist/types/tokenUsage.d.ts +4 -0
- package/dist/types/tokenUsage.js +4 -0
- package/dist/types.d.ts +3 -0
- package/dist/util/attachments.d.ts +1 -1
- package/dist/util/audioMime.d.ts +9 -0
- package/dist/util/audioMime.js +33 -0
- package/dist/util/{imageRef.d.ts → blobRef.d.ts} +9 -9
- package/dist/util/{imageRef.js → blobRef.js} +6 -13
- package/dist/util/mime.d.ts +21 -0
- package/dist/util/mime.js +52 -0
- package/dist/util/modalities.d.ts +6 -2
- package/dist/util/modalities.js +13 -15
- package/dist/util/provider.d.ts +2 -0
- package/dist/util/provider.js +1 -1
- 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
|
|
467
|
+
Smoltalk has one registration entry point per capability:
|
|
468
468
|
|
|
469
469
|
```ts
|
|
470
470
|
// example: skip-typecheck
|
|
471
471
|
import {
|
|
472
|
-
success,
|
|
473
|
-
registerProvider,
|
|
474
|
-
|
|
475
|
-
|
|
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
|
|
501
|
-
and
|
|
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/
|
|
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
|
|
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/
|
|
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
|
}
|
|
@@ -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[];
|
package/dist/client.js
CHANGED
|
@@ -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,
|
|
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
|
-
*
|
|
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.
|