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.
- package/README.md +145 -18
- package/dist/classes/ToolCall.js +18 -10
- package/dist/classes/message/AssistantMessage.d.ts +2 -0
- package/dist/classes/message/ToolMessage.js +13 -10
- 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.d.ts +2 -0
- package/dist/clients/google.js +125 -3
- 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
|
@@ -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
|
|
467
|
+
Smoltalk has one registration entry point per capability:
|
|
475
468
|
|
|
476
469
|
```ts
|
|
477
470
|
// example: skip-typecheck
|
|
478
471
|
import {
|
|
479
|
-
success,
|
|
480
|
-
registerProvider,
|
|
481
|
-
|
|
482
|
-
|
|
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
|
|
508
|
-
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.
|
|
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:
|
package/dist/classes/ToolCall.js
CHANGED
|
@@ -79,17 +79,25 @@ export class ToolCall {
|
|
|
79
79
|
};
|
|
80
80
|
}
|
|
81
81
|
toGoogle() {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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/
|
|
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[];
|