smoltalk 0.14.0 → 0.14.2

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 CHANGED
@@ -250,7 +250,7 @@ model ids aren't in the smoltalk registry.
250
250
 
251
251
  | `provider:` | What it is | Required config | Cost source |
252
252
  |-------------|------------|-----------------|-------------|
253
- | `"openrouter"` | OpenRouter.ai aggregator | `apiKey.openRouter` (or `OPENROUTER_API_KEY`) | `usage.cost` (auto-enabled by injecting `usage:{include:true}`) |
253
+ | `"openrouter"` | OpenRouter.ai aggregator | `apiKey.openRouter` (or `OPENROUTER_API_KEY`) | `usage.cost` |
254
254
  | `"deepinfra"` | DeepInfra hosted models | `apiKey.deepInfra` (or `DEEPINFRA_API_KEY`) | `usage.estimated_cost` |
255
255
  | `"litellm"` | Your own LiteLLM proxy | `apiKey.liteLlm` + `baseUrl.liteLlm` (or `LITELLM_API_KEY` / `LITELLM_BASE_URL`) | `x-litellm-response-cost` header (non-stream only) |
256
256
  | `"openai-compat"` | Any OpenAI-shape backend (vLLM, TGI, LM Studio…) | `apiKey.openAiCompat` + `baseUrl.openAiCompat` (or `OPENAI_COMPAT_API_KEY` / `OPENAI_COMPAT_BASE_URL`) | Best-effort: reads `usage.cost`/`estimated_cost`/`cost_usd` if present |
@@ -561,6 +561,10 @@ that exposes OpenAI-shaped `/audio/*` endpoints, use the generic
561
561
  **`openai-compat`** provider with `baseUrl` (mirrors the chat client). Anthropic,
562
562
  OpenRouter, and Ollama have no audio endpoints and return a `Failure`.
563
563
 
564
+ `speak()` also supports **`mlx`**: a local MLX speech server at `baseUrl.mlx`,
565
+ `MLX_BASE_URL`, or `http://127.0.0.1:8080/v1`. No API key, no retries, zero
566
+ cost, and `"wav"` by default.
567
+
564
568
  ```ts
565
569
  // example: skip-typecheck
566
570
  // Groq STT (OpenAI-compatible; provider inferred from the model)
@@ -655,7 +659,11 @@ if (result.success) {
655
659
  `tts-1` and `tts-1-hd` are the only baked-in models in v1. `voice` is
656
660
  required. Options: `format` (OpenAI accepts `"mp3"` | `"opus"` | `"aac"` |
657
661
  `"flac"` | `"wav"` | `"pcm"`, default `"mp3"`; a custom provider may accept
658
- other strings) and `speed`. Limits are declared per model in the registry —
662
+ other strings), `speed`, and `instructions`. `instructions` is free-text
663
+ guidance on how the speech should sound (`"Alarmed and urgent."`). It is passed
664
+ through as-is by the OpenAI-shaped providers (`openai`, `groq`, `openai-compat`,
665
+ `mlx`); whether the model reads it is up to the model (`gpt-4o-mini-tts` does,
666
+ `tts-1` does not). The `google` provider ignores it. Limits are declared per model in the registry —
659
667
  for `tts-1`/`tts-1-hd` that's a 4096-code-point input cap, a 0.25–4.0 speed
660
668
  range, and the format list above; exceeding any of them returns a `Failure`
661
669
  before the request is sent. The returned `audio` is a `Uint8Array` you own —
@@ -260,6 +260,9 @@ export class SmolGoogle extends BaseClient {
260
260
  });
261
261
  });
262
262
  const genConfig = {};
263
+ if (config.maxTokens !== undefined) {
264
+ genConfig.maxOutputTokens = config.maxTokens;
265
+ }
263
266
  if (systemParts.length > 0) {
264
267
  genConfig.systemInstruction = systemParts.join("\n");
265
268
  }
@@ -85,6 +85,9 @@ export class SmolOllama extends BaseClient {
85
85
  if (config.responseFormat) {
86
86
  request.format = responseFormatToJsonSchema(config.responseFormat);
87
87
  }
88
+ if (config.maxTokens !== undefined) {
89
+ request.options = { num_predict: config.maxTokens };
90
+ }
88
91
  Object.assign(request, sanitizeAttributes(config.rawAttributes));
89
92
  this.logger.debug("Sending request to Ollama:", JSON.stringify(redactAttachments(request), null, 2));
90
93
  this.statelogClient?.promptRequest(request);
@@ -151,6 +154,9 @@ export class SmolOllama extends BaseClient {
151
154
  if (config.responseFormat) {
152
155
  request.format = responseFormatToJsonSchema(config.responseFormat);
153
156
  }
157
+ if (config.maxTokens !== undefined) {
158
+ request.options = { num_predict: config.maxTokens };
159
+ }
154
160
  Object.assign(request, sanitizeAttributes(config.rawAttributes));
155
161
  this.logger.debug("Sending streaming request to Ollama:", JSON.stringify(redactAttachments(request), null, 2));
156
162
  this.statelogClient?.promptRequest(request);
@@ -32,11 +32,18 @@ export declare class SmolOpenAi extends BaseClient implements SmolClient {
32
32
  protected resolveCostUsd(_usage: any, _rawResponse?: Response): number | undefined;
33
33
  /**
34
34
  * Extra request body fields injected on every call. Subclasses override to
35
- * add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
36
- * or its `plugins: [{ id: "web" }]` for web search). Merged into the request
35
+ * add provider-specific request shapes (e.g. OpenRouter's
36
+ * `plugins: [{ id: "web" }]` for web search). Merged into the request
37
37
  * after the standard params so it can override them.
38
38
  */
39
39
  protected buildRequestExtras(_config: SmolConfig): Record<string, unknown>;
40
+ /**
41
+ * The request field carrying `config.maxTokens`. OpenAI deprecated
42
+ * `max_tokens` in favor of `max_completion_tokens`, and its reasoning models
43
+ * reject `max_tokens` outright. Compat servers override this to send
44
+ * `max_tokens`, which is the field they reliably accept.
45
+ */
46
+ protected maxTokensParam(config: SmolConfig): Record<string, number>;
40
47
  /**
41
48
  * Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
42
49
  * annotations) from a completion. Subclasses override; default returns none.
@@ -51,13 +51,25 @@ export class SmolOpenAi extends BaseClient {
51
51
  }
52
52
  /**
53
53
  * Extra request body fields injected on every call. Subclasses override to
54
- * add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
55
- * or its `plugins: [{ id: "web" }]` for web search). Merged into the request
54
+ * add provider-specific request shapes (e.g. OpenRouter's
55
+ * `plugins: [{ id: "web" }]` for web search). Merged into the request
56
56
  * after the standard params so it can override them.
57
57
  */
58
58
  buildRequestExtras(_config) {
59
59
  return {};
60
60
  }
61
+ /**
62
+ * The request field carrying `config.maxTokens`. OpenAI deprecated
63
+ * `max_tokens` in favor of `max_completion_tokens`, and its reasoning models
64
+ * reject `max_tokens` outright. Compat servers override this to send
65
+ * `max_tokens`, which is the field they reliably accept.
66
+ */
67
+ maxTokensParam(config) {
68
+ if (config.maxTokens === undefined) {
69
+ return {};
70
+ }
71
+ return { max_completion_tokens: config.maxTokens };
72
+ }
61
73
  /**
62
74
  * Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
63
75
  * annotations) from a completion. Subclasses override; default returns none.
@@ -126,6 +138,7 @@ export class SmolOpenAi extends BaseClient {
126
138
  ...(config.reasoningEffort && {
127
139
  reasoning_effort: config.reasoningEffort,
128
140
  }),
141
+ ...this.maxTokensParam(config),
129
142
  ...sanitizeAttributes(config.rawAttributes),
130
143
  ...this.buildRequestExtras(config),
131
144
  };
@@ -20,5 +20,6 @@ export declare class SmolOpenAiCompat extends SmolOpenAi {
20
20
  apiKey: string;
21
21
  baseURL: string;
22
22
  };
23
+ protected maxTokensParam(config: SmolConfig): Record<string, number>;
23
24
  protected resolveCostUsd(usage: any): number | undefined;
24
25
  }
@@ -30,6 +30,14 @@ export class SmolOpenAiCompat extends SmolOpenAi {
30
30
  }
31
31
  return { apiKey, baseURL };
32
32
  }
33
+ // Compat servers (OpenRouter, DeepInfra, LiteLLM, vLLM, mlx_lm.server) all
34
+ // accept `max_tokens`; `max_completion_tokens` support is patchy.
35
+ maxTokensParam(config) {
36
+ if (config.maxTokens === undefined) {
37
+ return {};
38
+ }
39
+ return { max_tokens: config.maxTokens };
40
+ }
33
41
  resolveCostUsd(usage) {
34
42
  // Try the three common conventions across OpenAI-compatible providers.
35
43
  const c = usage?.cost ?? usage?.estimated_cost ?? usage?.cost_usd;
@@ -5,9 +5,8 @@ import type { SmolConfig, HostedToolResult } from "../types.js";
5
5
  *
6
6
  * - Baked base URL `https://openrouter.ai/api/v1` (override via config.baseUrl.openRouter).
7
7
  * - Key: config.apiKey.openRouter or env OPENROUTER_API_KEY.
8
- * - Cost: reads `usage.cost` (USD). OpenRouter only returns this when
9
- * `usage: { include: true }` is set in the request body, which this client
10
- * injects automatically via buildRequestExtras.
8
+ * - Cost: reads `usage.cost` (USD). OpenRouter includes this in every
9
+ * response (in the final SSE chunk when streaming); no request flag needed.
11
10
  * - Hosted web_search: when config.hostedTools includes "web_search",
12
11
  * injects `plugins: [{ id: "web", max_results: 5 }]` and parses
13
12
  * message.annotations into a HostedToolResult.
@@ -6,9 +6,8 @@ import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
6
6
  *
7
7
  * - Baked base URL `https://openrouter.ai/api/v1` (override via config.baseUrl.openRouter).
8
8
  * - Key: config.apiKey.openRouter or env OPENROUTER_API_KEY.
9
- * - Cost: reads `usage.cost` (USD). OpenRouter only returns this when
10
- * `usage: { include: true }` is set in the request body, which this client
11
- * injects automatically via buildRequestExtras.
9
+ * - Cost: reads `usage.cost` (USD). OpenRouter includes this in every
10
+ * response (in the final SSE chunk when streaming); no request flag needed.
12
11
  * - Hosted web_search: when config.hostedTools includes "web_search",
13
12
  * injects `plugins: [{ id: "web", max_results: 5 }]` and parses
14
13
  * message.annotations into a HostedToolResult.
@@ -27,8 +26,7 @@ export class SmolOpenRouter extends SmolOpenAiCompat {
27
26
  return typeof usage?.cost === "number" ? usage.cost : undefined;
28
27
  }
29
28
  buildRequestExtras(config) {
30
- // OpenRouter only returns usage.cost when this is set.
31
- const extras = { usage: { include: true } };
29
+ const extras = {};
32
30
  if (config.hostedTools?.includes("web_search")) {
33
31
  extras.plugins = [{ id: "web", max_results: 5 }];
34
32
  }
package/dist/models.d.ts CHANGED
@@ -176,6 +176,13 @@ export declare const textToSpeechModels: readonly [{
176
176
  readonly perCharacterCost: 0.00004;
177
177
  readonly maxInputChars: 200;
178
178
  readonly formats: readonly ["wav"];
179
+ }, {
180
+ readonly type: "text-to-speech";
181
+ readonly modelName: "gemini-3.1-flash-tts-preview";
182
+ readonly provider: "google";
183
+ readonly inputTokenCost: 1;
184
+ readonly outputAudioTokenCost: 20;
185
+ readonly formats: readonly ["pcm", "wav"];
179
186
  }, {
180
187
  readonly type: "text-to-speech";
181
188
  readonly modelName: "gemini-2.5-flash-preview-tts";
@@ -688,7 +695,7 @@ export declare const textModels: readonly [{
688
695
  readonly inputTokenCost: 5;
689
696
  readonly cachedInputTokenCost: 0.5;
690
697
  readonly outputTokenCost: 22.5;
691
- readonly thresholdTokens: 200000;
698
+ readonly thresholdTokens: 272000;
692
699
  };
693
700
  readonly reasoning: {
694
701
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -778,7 +785,7 @@ export declare const textModels: readonly [{
778
785
  readonly longContext: {
779
786
  readonly inputTokenCost: 60;
780
787
  readonly outputTokenCost: 270;
781
- readonly thresholdTokens: 200000;
788
+ readonly thresholdTokens: 272000;
782
789
  };
783
790
  readonly reasoning: {
784
791
  readonly levels: readonly ["medium", "high", "xhigh"];
@@ -813,7 +820,7 @@ export declare const textModels: readonly [{
813
820
  readonly inputTokenCost: 10;
814
821
  readonly cachedInputTokenCost: 1;
815
822
  readonly outputTokenCost: 45;
816
- readonly thresholdTokens: 200000;
823
+ readonly thresholdTokens: 272000;
817
824
  };
818
825
  readonly reasoning: {
819
826
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -845,7 +852,7 @@ export declare const textModels: readonly [{
845
852
  readonly longContext: {
846
853
  readonly inputTokenCost: 60;
847
854
  readonly outputTokenCost: 270;
848
- readonly thresholdTokens: 200000;
855
+ readonly thresholdTokens: 272000;
849
856
  };
850
857
  readonly reasoning: {
851
858
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh"];
@@ -866,6 +873,41 @@ export declare const textModels: readonly [{
866
873
  readonly structuredOutput: true;
867
874
  readonly temperatureSupported: false;
868
875
  readonly provider: "openai-responses";
876
+ }, {
877
+ readonly type: "text";
878
+ readonly modelName: "gpt-6-astra";
879
+ readonly description: "GPT-6 Astra is OpenAI's most capable model, built for the hardest end-to-end work: complex reasoning, coding, computer use, research, and document creation. 1M context window. Standard pricing for ≤272K input tokens; prompts above that are billed at 2x input/cache and 1.5x output for the whole request. Knowledge cutoff: April 2026.";
880
+ readonly maxInputTokens: 1050000;
881
+ readonly maxOutputTokens: 128000;
882
+ readonly inputTokenCost: 10;
883
+ readonly cachedInputTokenCost: 1;
884
+ readonly outputTokenCost: 50;
885
+ readonly outputTokensPerSecond: 69;
886
+ readonly longContext: {
887
+ readonly inputTokenCost: 20;
888
+ readonly cachedInputTokenCost: 2;
889
+ readonly outputTokenCost: 75;
890
+ readonly thresholdTokens: 272000;
891
+ };
892
+ readonly reasoning: {
893
+ readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
894
+ readonly defaultLevel: "medium";
895
+ readonly canDisable: false;
896
+ readonly outputsThinking: false;
897
+ readonly outputsSignatures: false;
898
+ };
899
+ readonly modalities: {
900
+ readonly input: readonly ["text", "image", "pdf"];
901
+ readonly output: readonly ["text"];
902
+ };
903
+ readonly knowledge: "2026-04-30";
904
+ readonly releaseDate: "2026-09-04";
905
+ readonly lastUpdated: "2026-09-04";
906
+ readonly family: "gpt";
907
+ readonly openWeights: false;
908
+ readonly structuredOutput: true;
909
+ readonly temperatureSupported: false;
910
+ readonly provider: "openai";
869
911
  }, {
870
912
  readonly type: "text";
871
913
  readonly modelName: "gpt-5.6-sol";
@@ -875,12 +917,12 @@ export declare const textModels: readonly [{
875
917
  readonly inputTokenCost: 4;
876
918
  readonly cachedInputTokenCost: 0.4;
877
919
  readonly outputTokenCost: 20;
878
- readonly outputTokensPerSecond: 71;
920
+ readonly outputTokensPerSecond: 77;
879
921
  readonly longContext: {
880
922
  readonly inputTokenCost: 8;
881
923
  readonly cachedInputTokenCost: 0.8;
882
924
  readonly outputTokenCost: 30;
883
- readonly thresholdTokens: 200000;
925
+ readonly thresholdTokens: 272000;
884
926
  };
885
927
  readonly reasoning: {
886
928
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -910,12 +952,12 @@ export declare const textModels: readonly [{
910
952
  readonly inputTokenCost: 2;
911
953
  readonly cachedInputTokenCost: 0.2;
912
954
  readonly outputTokenCost: 12;
913
- readonly outputTokensPerSecond: 98;
955
+ readonly outputTokensPerSecond: 106;
914
956
  readonly longContext: {
915
957
  readonly inputTokenCost: 4;
916
958
  readonly cachedInputTokenCost: 0.4;
917
959
  readonly outputTokenCost: 18;
918
- readonly thresholdTokens: 200000;
960
+ readonly thresholdTokens: 272000;
919
961
  };
920
962
  readonly reasoning: {
921
963
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -945,12 +987,12 @@ export declare const textModels: readonly [{
945
987
  readonly inputTokenCost: 0.2;
946
988
  readonly cachedInputTokenCost: 0.02;
947
989
  readonly outputTokenCost: 1.2;
948
- readonly outputTokensPerSecond: 116;
990
+ readonly outputTokensPerSecond: 165;
949
991
  readonly longContext: {
950
992
  readonly inputTokenCost: 0.4;
951
993
  readonly cachedInputTokenCost: 0.04;
952
994
  readonly outputTokenCost: 1.8;
953
- readonly thresholdTokens: 200000;
995
+ readonly thresholdTokens: 272000;
954
996
  };
955
997
  readonly reasoning: {
956
998
  readonly levels: readonly ["none", "low", "medium", "high", "xhigh", "max"];
@@ -980,7 +1022,7 @@ export declare const textModels: readonly [{
980
1022
  readonly inputTokenCost: 2;
981
1023
  readonly cachedInputTokenCost: 0.2;
982
1024
  readonly outputTokenCost: 12;
983
- readonly outputTokensPerSecond: 133;
1025
+ readonly outputTokensPerSecond: 124;
984
1026
  readonly longContext: {
985
1027
  readonly inputTokenCost: 4;
986
1028
  readonly cachedInputTokenCost: 0.4;
@@ -1043,7 +1085,7 @@ export declare const textModels: readonly [{
1043
1085
  readonly inputTokenCost: 0.75;
1044
1086
  readonly cachedInputTokenCost: 0.075;
1045
1087
  readonly outputTokenCost: 3.75;
1046
- readonly outputTokensPerSecond: 312;
1088
+ readonly outputTokensPerSecond: 329;
1047
1089
  readonly inputAudioTokenCost: 1.5;
1048
1090
  readonly reasoning: {
1049
1091
  readonly levels: readonly ["low", "medium", "high"];
@@ -1455,6 +1497,7 @@ export declare const textModels: readonly [{
1455
1497
  readonly cachedInputTokenCost: 0.25;
1456
1498
  readonly cacheCreationInputTokenCost: 12.5;
1457
1499
  readonly outputTokenCost: 50;
1500
+ readonly outputTokensPerSecond: 69;
1458
1501
  readonly reasoning: {
1459
1502
  readonly thinkingStyle: "adaptive";
1460
1503
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
@@ -1484,6 +1527,7 @@ export declare const textModels: readonly [{
1484
1527
  readonly cachedInputTokenCost: 0.5;
1485
1528
  readonly cacheCreationInputTokenCost: 6.25;
1486
1529
  readonly outputTokenCost: 25;
1530
+ readonly outputTokensPerSecond: 59;
1487
1531
  readonly reasoning: {
1488
1532
  readonly thinkingStyle: "adaptive";
1489
1533
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
@@ -1625,7 +1669,7 @@ export declare const textModels: readonly [{
1625
1669
  readonly cachedInputTokenCost: 0.2;
1626
1670
  readonly cacheCreationInputTokenCost: 2.5;
1627
1671
  readonly outputTokenCost: 10;
1628
- readonly outputTokensPerSecond: 71;
1672
+ readonly outputTokensPerSecond: 81;
1629
1673
  readonly reasoning: {
1630
1674
  readonly thinkingStyle: "adaptive";
1631
1675
  readonly levels: readonly ["low", "medium", "high", "xhigh", "max"];
package/dist/models.js CHANGED
@@ -92,6 +92,14 @@ export const textToSpeechModels = [
92
92
  },
93
93
  // Gemini TTS is token-billed (text input + audio output). No maxInputChars:
94
94
  // Gemini documents a 32k-token context, and characters are not a sound proxy.
95
+ {
96
+ type: "text-to-speech",
97
+ modelName: "gemini-3.1-flash-tts-preview",
98
+ provider: "google",
99
+ inputTokenCost: 1.0, // $/1M text-input tokens, verified 2026-09-21
100
+ outputAudioTokenCost: 20.0, // $/1M audio-output tokens
101
+ formats: ["pcm", "wav"],
102
+ },
95
103
  {
96
104
  type: "text-to-speech",
97
105
  modelName: "gemini-2.5-flash-preview-tts",
@@ -626,7 +634,7 @@ export const textModels = [
626
634
  inputTokenCost: 5,
627
635
  cachedInputTokenCost: 0.5,
628
636
  outputTokenCost: 22.5,
629
- thresholdTokens: 200000,
637
+ thresholdTokens: 272000,
630
638
  },
631
639
  reasoning: {
632
640
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -719,7 +727,7 @@ export const textModels = [
719
727
  longContext: {
720
728
  inputTokenCost: 60,
721
729
  outputTokenCost: 270,
722
- thresholdTokens: 200000,
730
+ thresholdTokens: 272000,
723
731
  },
724
732
  reasoning: {
725
733
  levels: ["medium", "high", "xhigh"],
@@ -755,7 +763,7 @@ export const textModels = [
755
763
  inputTokenCost: 10,
756
764
  cachedInputTokenCost: 1,
757
765
  outputTokenCost: 45,
758
- thresholdTokens: 200000,
766
+ thresholdTokens: 272000,
759
767
  },
760
768
  reasoning: {
761
769
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -788,7 +796,7 @@ export const textModels = [
788
796
  longContext: {
789
797
  inputTokenCost: 60,
790
798
  outputTokenCost: 270,
791
- thresholdTokens: 200000,
799
+ thresholdTokens: 272000,
792
800
  },
793
801
  reasoning: {
794
802
  levels: ["none", "low", "medium", "high", "xhigh"],
@@ -810,6 +818,42 @@ export const textModels = [
810
818
  temperatureSupported: false,
811
819
  provider: "openai-responses",
812
820
  },
821
+ {
822
+ type: "text",
823
+ modelName: "gpt-6-astra",
824
+ description: "GPT-6 Astra is OpenAI's most capable model, built for the hardest end-to-end work: complex reasoning, coding, computer use, research, and document creation. 1M context window. Standard pricing for ≤272K input tokens; prompts above that are billed at 2x input/cache and 1.5x output for the whole request. Knowledge cutoff: April 2026.",
825
+ maxInputTokens: 1050000,
826
+ maxOutputTokens: 128000,
827
+ inputTokenCost: 10,
828
+ cachedInputTokenCost: 1,
829
+ outputTokenCost: 50,
830
+ outputTokensPerSecond: 69,
831
+ longContext: {
832
+ inputTokenCost: 20,
833
+ cachedInputTokenCost: 2,
834
+ outputTokenCost: 75,
835
+ thresholdTokens: 272000,
836
+ },
837
+ reasoning: {
838
+ levels: ["low", "medium", "high", "xhigh", "max"],
839
+ defaultLevel: "medium",
840
+ canDisable: false,
841
+ outputsThinking: false,
842
+ outputsSignatures: false,
843
+ },
844
+ modalities: {
845
+ input: ["text", "image", "pdf"],
846
+ output: ["text"],
847
+ },
848
+ knowledge: "2026-04-30",
849
+ releaseDate: "2026-09-04",
850
+ lastUpdated: "2026-09-04",
851
+ family: "gpt",
852
+ openWeights: false,
853
+ structuredOutput: true,
854
+ temperatureSupported: false,
855
+ provider: "openai",
856
+ },
813
857
  {
814
858
  type: "text",
815
859
  modelName: "gpt-5.6-sol",
@@ -819,12 +863,12 @@ export const textModels = [
819
863
  inputTokenCost: 4,
820
864
  cachedInputTokenCost: 0.4,
821
865
  outputTokenCost: 20,
822
- outputTokensPerSecond: 71,
866
+ outputTokensPerSecond: 77,
823
867
  longContext: {
824
868
  inputTokenCost: 8,
825
869
  cachedInputTokenCost: 0.8,
826
870
  outputTokenCost: 30,
827
- thresholdTokens: 200000,
871
+ thresholdTokens: 272000,
828
872
  },
829
873
  reasoning: {
830
874
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -855,12 +899,12 @@ export const textModels = [
855
899
  inputTokenCost: 2,
856
900
  cachedInputTokenCost: 0.2,
857
901
  outputTokenCost: 12,
858
- outputTokensPerSecond: 98,
902
+ outputTokensPerSecond: 106,
859
903
  longContext: {
860
904
  inputTokenCost: 4,
861
905
  cachedInputTokenCost: 0.4,
862
906
  outputTokenCost: 18,
863
- thresholdTokens: 200000,
907
+ thresholdTokens: 272000,
864
908
  },
865
909
  reasoning: {
866
910
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -891,12 +935,12 @@ export const textModels = [
891
935
  inputTokenCost: 0.2,
892
936
  cachedInputTokenCost: 0.02,
893
937
  outputTokenCost: 1.2,
894
- outputTokensPerSecond: 116,
938
+ outputTokensPerSecond: 165,
895
939
  longContext: {
896
940
  inputTokenCost: 0.4,
897
941
  cachedInputTokenCost: 0.04,
898
942
  outputTokenCost: 1.8,
899
- thresholdTokens: 200000,
943
+ thresholdTokens: 272000,
900
944
  },
901
945
  reasoning: {
902
946
  levels: ["none", "low", "medium", "high", "xhigh", "max"],
@@ -927,7 +971,7 @@ export const textModels = [
927
971
  inputTokenCost: 2,
928
972
  cachedInputTokenCost: 0.2,
929
973
  outputTokenCost: 12,
930
- outputTokensPerSecond: 133,
974
+ outputTokensPerSecond: 124,
931
975
  longContext: {
932
976
  inputTokenCost: 4,
933
977
  cachedInputTokenCost: 0.4,
@@ -992,7 +1036,7 @@ export const textModels = [
992
1036
  inputTokenCost: 0.75,
993
1037
  cachedInputTokenCost: 0.075,
994
1038
  outputTokenCost: 3.75,
995
- outputTokensPerSecond: 312,
1039
+ outputTokensPerSecond: 329,
996
1040
  inputAudioTokenCost: 1.5,
997
1041
  reasoning: {
998
1042
  levels: ["low", "medium", "high"],
@@ -1424,6 +1468,7 @@ export const textModels = [
1424
1468
  cachedInputTokenCost: 0.25,
1425
1469
  cacheCreationInputTokenCost: 12.5,
1426
1470
  outputTokenCost: 50,
1471
+ outputTokensPerSecond: 69,
1427
1472
  reasoning: {
1428
1473
  thinkingStyle: "adaptive",
1429
1474
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1454,6 +1499,7 @@ export const textModels = [
1454
1499
  cachedInputTokenCost: 0.5,
1455
1500
  cacheCreationInputTokenCost: 6.25,
1456
1501
  outputTokenCost: 25,
1502
+ outputTokensPerSecond: 59,
1457
1503
  reasoning: {
1458
1504
  thinkingStyle: "adaptive",
1459
1505
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1600,7 +1646,7 @@ export const textModels = [
1600
1646
  cachedInputTokenCost: 0.2,
1601
1647
  cacheCreationInputTokenCost: 2.5,
1602
1648
  outputTokenCost: 10,
1603
- outputTokensPerSecond: 71,
1649
+ outputTokensPerSecond: 81,
1604
1650
  reasoning: {
1605
1651
  thinkingStyle: "adaptive",
1606
1652
  levels: ["low", "medium", "high", "xhigh", "max"],
@@ -1939,7 +1985,7 @@ export const hostedTools = [
1939
1985
  category: "code_execution",
1940
1986
  description: "Run code in a sandboxed container.",
1941
1987
  providerToolId: "code_execution",
1942
- pricing: { unit: "per_hour", amount: 0.05, freeAllowance: "50 container-hours/day", note: "Free when used with web_search or web_fetch." },
1988
+ pricing: { unit: "per_hour", amount: 0.05, freeAllowance: "1,550 container-hours/month", note: "Free when used with web_search or web_fetch. 5-minute minimum execution time per container." },
1943
1989
  },
1944
1990
  {
1945
1991
  name: "web_search",
@@ -2014,7 +2060,12 @@ export const hostedTools = [
2014
2060
  description: "Grounding with Google Maps (Gemini 3 only).",
2015
2061
  providerToolId: "google_maps",
2016
2062
  models: ["gemini-3-pro-preview", "gemini-3.1-pro-preview", "gemini-3-flash-preview", "gemini-3.5-flash", "gemini-3.6-flash", "gemini-3.7-flash", "gemini-3.8-flash", "gemini-3.1-flash-lite", "gemini-3.5-flash-lite"],
2017
- pricing: { unit: "per_call", note: "Gemini 3 family only; see Google pricing." },
2063
+ pricing: {
2064
+ unit: "per_call",
2065
+ amount: 0.014,
2066
+ freeAllowance: "5,000 grounded prompts/month (Gemini 3)",
2067
+ note: "$14 per 1,000 search queries on the Gemini 3 family.",
2068
+ },
2018
2069
  },
2019
2070
  {
2020
2071
  name: "web_search",
@@ -11,6 +11,11 @@ export type SpeechClientConfig = {
11
11
  /** Base-URL map (for OpenAI-compatible providers); read via resolveBaseUrl. */
12
12
  baseUrl?: SmolConfig["baseUrl"];
13
13
  voice: string;
14
+ /** How the speech should sound, in plain words: "Alarmed and urgent."
15
+ * Passed through as-is by the OpenAI-shaped providers (openai, groq,
16
+ * openai-compat, mlx); whether the model reads it is up to the model
17
+ * (OpenAI's tts-1 and tts-1-hd do not). The google provider ignores it. */
18
+ instructions?: string;
14
19
  modelData?: ModelDataBlob;
15
20
  /** Output format; provider-specific vocabulary (OpenAI: mp3/opus/aac/flac/wav/pcm). */
16
21
  format?: string;
@@ -0,0 +1,23 @@
1
+ import OpenAI from "openai";
2
+ import { Result } from "../types/result.js";
3
+ import type { SpeakFormat } from "../util/audioMime.js";
4
+ import { OpenAISpeechClient } from "./openai.js";
5
+ import type { SpeechResult } from "../speech.js";
6
+ /**
7
+ * Speech from an MLX server on localhost, usually `agency local serve
8
+ * --speech <model>`. The same request the OpenAI client makes, with the
9
+ * things the chat and embedding mlx clients also fix: the base URL has a
10
+ * default, the key is a placeholder the server ignores, and the cost is
11
+ * zero. It also never retries. The server runs one generation at a time,
12
+ * so a retry would wait behind the request that just timed out and then
13
+ * generate the same text again.
14
+ */
15
+ export declare class MlxSpeechClient extends OpenAISpeechClient {
16
+ protected makeClient(): OpenAI;
17
+ protected requiresKey(): boolean;
18
+ protected defaultFormat(): SpeakFormat;
19
+ /** Local speech is free. Set after the base class's cost pass, so model
20
+ * data that prices this model cannot override it (matching the chat mlx
21
+ * client, whose provider cost of 0 wins over registry pricing). */
22
+ speak(text: string): Promise<Result<SpeechResult>>;
23
+ }
@@ -0,0 +1,49 @@
1
+ import OpenAI from "openai";
2
+ import { success } from "../types/result.js";
3
+ import { resolveBaseUrl } from "../util/provider.js";
4
+ import { OpenAISpeechClient } from "./openai.js";
5
+ /** How long one request may take. Equal to the SDK default; stated here so
6
+ * the ceiling is visible. A local model generating a long piece of text is
7
+ * slow, and the caller keeps its requests short. */
8
+ const MLX_SPEECH_TIMEOUT_MS = 600_000;
9
+ /**
10
+ * Speech from an MLX server on localhost, usually `agency local serve
11
+ * --speech <model>`. The same request the OpenAI client makes, with the
12
+ * things the chat and embedding mlx clients also fix: the base URL has a
13
+ * default, the key is a placeholder the server ignores, and the cost is
14
+ * zero. It also never retries. The server runs one generation at a time,
15
+ * so a retry would wait behind the request that just timed out and then
16
+ * generate the same text again.
17
+ */
18
+ export class MlxSpeechClient extends OpenAISpeechClient {
19
+ makeClient() {
20
+ // resolveBaseUrl always returns a value for "mlx" (it has a default).
21
+ const baseURL = resolveBaseUrl("mlx", { baseUrl: this.config.baseUrl });
22
+ // The OpenAI SDK refuses an empty key. The local server never reads it.
23
+ return new OpenAI({
24
+ apiKey: "mlx-local",
25
+ baseURL,
26
+ maxRetries: 0,
27
+ timeout: MLX_SPEECH_TIMEOUT_MS,
28
+ });
29
+ }
30
+ requiresKey() {
31
+ return false;
32
+ }
33
+ defaultFormat() {
34
+ return "wav";
35
+ }
36
+ /** Local speech is free. Set after the base class's cost pass, so model
37
+ * data that prices this model cannot override it (matching the chat mlx
38
+ * client, whose provider cost of 0 wins over registry pricing). */
39
+ async speak(text) {
40
+ const result = await super.speak(text);
41
+ if (!result.success) {
42
+ return result;
43
+ }
44
+ return success({
45
+ ...result.value,
46
+ cost: { inputCost: 0, outputCost: 0, totalCost: 0, currency: "USD" },
47
+ });
48
+ }
49
+ }
@@ -10,5 +10,8 @@ export declare class OpenAISpeechClient extends BaseSpeechClient {
10
10
  protected defaultFormat(): SpeakFormat;
11
11
  /** Provider-specific diagnostic when no API key is resolved. Subclasses override. */
12
12
  protected noKeyMessage(): string;
13
+ /** Whether a request needs an API key at all. A local server ignores the
14
+ * key, and its client says so by returning false. */
15
+ protected requiresKey(): boolean;
13
16
  protected _speak(text: string): Promise<Result<SpeechResult>>;
14
17
  }
@@ -15,10 +15,15 @@ export class OpenAISpeechClient extends BaseSpeechClient {
15
15
  noKeyMessage() {
16
16
  return "No OpenAI API key provided. Set apiKey.openAi or OPENAI_API_KEY.";
17
17
  }
18
+ /** Whether a request needs an API key at all. A local server ignores the
19
+ * key, and its client says so by returning false. */
20
+ requiresKey() {
21
+ return true;
22
+ }
18
23
  // No try/catch here: BaseSpeechClient.speak() is the single
19
24
  // redacting/logging exception boundary.
20
25
  async _speak(text) {
21
- if (!this.config.apiKey) {
26
+ if (this.requiresKey() && !this.config.apiKey) {
22
27
  return failure(this.noKeyMessage());
23
28
  }
24
29
  // The shared contract carries format as a plain string; narrow to OpenAI's
@@ -40,6 +45,9 @@ export class OpenAISpeechClient extends BaseSpeechClient {
40
45
  if (this.config.speed !== undefined) {
41
46
  params.speed = this.config.speed;
42
47
  }
48
+ if (this.config.instructions !== undefined && this.config.instructions !== "") {
49
+ params.instructions = this.config.instructions;
50
+ }
43
51
  const res = await client.audio.speech.create(params, { signal: this.config.abortSignal });
44
52
  const audio = new Uint8Array(await res.arrayBuffer());
45
53
  const result = { audio, mimeType };
package/dist/speech.d.ts CHANGED
@@ -7,6 +7,11 @@ import { BaseSpeechClient, SpeechClientConfig } from "./speech/baseSpeechClient.
7
7
  export type SpeakOptions = {
8
8
  model: string;
9
9
  voice: string;
10
+ /** How the speech should sound, in plain words: "Alarmed and urgent."
11
+ * Passed through as-is by the OpenAI-shaped providers (openai, groq,
12
+ * openai-compat, mlx); whether the model reads it is up to the model
13
+ * (OpenAI's tts-1 and tts-1-hd do not). The google provider ignores it. */
14
+ instructions?: string;
10
15
  provider?: string;
11
16
  modelData?: ModelDataBlob;
12
17
  apiKey?: SmolConfig["apiKey"];
package/dist/speech.js CHANGED
@@ -6,12 +6,14 @@ import { OpenAISpeechClient } from "./speech/openai.js";
6
6
  import { GroqSpeechClient } from "./speech/groq.js";
7
7
  import { GoogleSpeechClient } from "./speech/google.js";
8
8
  import { OpenAiCompatSpeechClient } from "./speech/openaiCompat.js";
9
+ import { MlxSpeechClient } from "./speech/mlx.js";
9
10
  // Checked before the user registry so a registered "openai" can't hijack the built-in.
10
11
  const builtinClients = Object.create(null);
11
12
  builtinClients["openai"] = OpenAISpeechClient;
12
13
  builtinClients["groq"] = GroqSpeechClient;
13
14
  builtinClients["google"] = GoogleSpeechClient;
14
15
  builtinClients["openai-compat"] = OpenAiCompatSpeechClient;
16
+ builtinClients["mlx"] = MlxSpeechClient;
15
17
  // Null-prototype so provider names like "toString"/"__proto__" can't collide
16
18
  // with Object.prototype or pollute the registry.
17
19
  const registered = Object.create(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.14.0",
3
+ "version": "0.14.2",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [