smoltalk 0.14.0 → 0.14.1
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 +10 -2
- package/dist/clients/google.js +3 -0
- package/dist/clients/ollama.js +6 -0
- package/dist/clients/openai.d.ts +9 -2
- package/dist/clients/openai.js +15 -2
- package/dist/clients/openaiCompat.d.ts +1 -0
- package/dist/clients/openaiCompat.js +8 -0
- package/dist/clients/openrouter.d.ts +2 -3
- package/dist/clients/openrouter.js +3 -5
- package/dist/speech/baseSpeechClient.d.ts +5 -0
- package/dist/speech/mlx.d.ts +23 -0
- package/dist/speech/mlx.js +49 -0
- package/dist/speech/openai.d.ts +3 -0
- package/dist/speech/openai.js +9 -1
- package/dist/speech.d.ts +5 -0
- package/dist/speech.js +2 -0
- package/package.json +1 -1
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`
|
|
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 `
|
|
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 —
|
package/dist/clients/google.js
CHANGED
|
@@ -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
|
}
|
package/dist/clients/ollama.js
CHANGED
|
@@ -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);
|
package/dist/clients/openai.d.ts
CHANGED
|
@@ -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
|
|
36
|
-
*
|
|
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.
|
package/dist/clients/openai.js
CHANGED
|
@@ -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
|
|
55
|
-
*
|
|
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
|
};
|
|
@@ -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
|
|
9
|
-
*
|
|
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
|
|
10
|
-
*
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
+
}
|
package/dist/speech/openai.d.ts
CHANGED
|
@@ -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
|
}
|
package/dist/speech/openai.js
CHANGED
|
@@ -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);
|