smoltalk 0.5.0 → 0.6.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 +67 -10
- package/dist/client.d.ts +4 -0
- package/dist/client.js +37 -16
- package/dist/clients/anthropic.d.ts +1 -3
- package/dist/clients/anthropic.js +5 -1
- package/dist/clients/baseClient.js +2 -2
- package/dist/clients/deepinfra.d.ts +17 -0
- package/dist/clients/deepinfra.js +27 -0
- package/dist/clients/google.js +3 -2
- package/dist/clients/litellm.d.ts +19 -0
- package/dist/clients/litellm.js +32 -0
- package/dist/clients/ollama.js +5 -3
- package/dist/clients/openai.d.ts +41 -5
- package/dist/clients/openai.js +75 -12
- package/dist/clients/openaiCompat.d.ts +22 -0
- package/dist/clients/openaiCompat.js +33 -0
- package/dist/clients/openaiResponses.js +3 -2
- package/dist/clients/openrouter.d.ts +23 -0
- package/dist/clients/openrouter.js +76 -0
- package/dist/embed/openai.d.ts +7 -1
- package/dist/embed/openai.js +8 -2
- package/dist/embed.d.ts +18 -4
- package/dist/embed.js +32 -4
- package/dist/image/openaiCompat.d.ts +9 -0
- package/dist/image/openaiCompat.js +60 -0
- package/dist/image.d.ts +13 -2
- package/dist/image.js +28 -3
- package/dist/models.d.ts +5 -1
- package/dist/models.js +25 -2
- package/dist/types.d.ts +22 -10
- package/dist/util/hostedTools.d.ts +1 -1
- package/dist/util/hostedTools.js +19 -5
- package/dist/util/provider.d.ts +32 -6
- package/dist/util/provider.js +38 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,6 +8,15 @@ 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
|
+
|
|
11
20
|
## Hello world example
|
|
12
21
|
|
|
13
22
|
```typescript
|
|
@@ -68,8 +77,10 @@ const messages = [
|
|
|
68
77
|
const resp = await text({
|
|
69
78
|
messages,
|
|
70
79
|
model: "gemini-2.0-flash-lite",
|
|
71
|
-
|
|
72
|
-
|
|
80
|
+
apiKey: {
|
|
81
|
+
openAi: process.env.OPENAI_API_KEY || "",
|
|
82
|
+
google: process.env.GEMINI_API_KEY || "",
|
|
83
|
+
},
|
|
73
84
|
logLevel: "debug",
|
|
74
85
|
});
|
|
75
86
|
```
|
|
@@ -80,8 +91,10 @@ If you want to construct a client once and reuse it across many calls, use `getC
|
|
|
80
91
|
import { getClient, userMessage } from "smoltalk";
|
|
81
92
|
|
|
82
93
|
const client = getClient({
|
|
83
|
-
|
|
84
|
-
|
|
94
|
+
apiKey: {
|
|
95
|
+
openAi: process.env.OPENAI_API_KEY || "",
|
|
96
|
+
google: process.env.GEMINI_API_KEY || "",
|
|
97
|
+
},
|
|
85
98
|
model: "gemini-2.0-flash-lite",
|
|
86
99
|
});
|
|
87
100
|
|
|
@@ -147,12 +160,9 @@ A couple of design decisions to note:
|
|
|
147
160
|
|--------|------|-------------|
|
|
148
161
|
| `model` | `ModelName` | **Required.** The model to use (e.g. `"gpt-4o"`, `"gemini-2.0-flash-lite"`). |
|
|
149
162
|
| `messages` | `Message[]` | **Required.** The conversation messages to send. |
|
|
150
|
-
| `
|
|
151
|
-
| `
|
|
152
|
-
| `
|
|
153
|
-
| `ollamaApiKey` | `string` | Ollama API key (only needed for cloud Ollama). |
|
|
154
|
-
| `ollamaHost` | `string` | Ollama host URL (for self-hosted or cloud Ollama). |
|
|
155
|
-
| `provider` | `Provider` | Override provider detection. One of `"openai"`, `"openai-responses"`, `"google"`, `"ollama"`, `"anthropic"`, or any provider registered via `registerProvider()`. |
|
|
163
|
+
| `apiKey` | `{ openAi?, google?, anthropic?, ollama?, openRouter?, deepInfra?, liteLlm?, openAiCompat? }` | API keys, nested by provider. Each falls back to its conventional env var (`OPENAI_API_KEY`, `GEMINI_API_KEY`, `ANTHROPIC_API_KEY`, `OPENROUTER_API_KEY`, `DEEPINFRA_API_KEY`, `LITELLM_API_KEY`, `OPENAI_COMPAT_API_KEY`). Ollama has no env-var fallback for the key. |
|
|
164
|
+
| `baseUrl` | `{ ollama?, openRouter?, deepInfra?, liteLlm?, openAiCompat? }` | Custom base URLs. `ollama` defaults to `$OLLAMA_HOST` then `http://localhost:11434`; `openRouter`/`deepInfra` defaults are baked in; `liteLlm`/`openAiCompat` require an explicit URL (or `LITELLM_BASE_URL` / `OPENAI_COMPAT_BASE_URL` env). |
|
|
165
|
+
| `provider` | `Provider` | Override provider detection. One of `"openai"`, `"openai-responses"`, `"google"`, `"ollama"`, `"anthropic"`, `"openrouter"`, `"deepinfra"`, `"litellm"`, `"openai-compat"`, or any provider registered via `registerProvider()`. |
|
|
156
166
|
| `logLevel` | `LogLevel` | Logging verbosity: `"debug"`, `"info"`, `"warn"`, `"error"`. |
|
|
157
167
|
| `tools` | `{ name, description?, schema }[]` | Tool definitions. `schema` is a Zod object schema. |
|
|
158
168
|
| `responseFormat` | `ZodType` | Zod schema for structured output. The response is parsed and validated against this schema. |
|
|
@@ -194,6 +204,53 @@ Detects when the model is stuck in a repetitive tool-call loop.
|
|
|
194
204
|
| `intervention` | `string` | Action to take: `"remove-tool"`, `"remove-all-tools"`, `"throw-error"`, or `"halt-execution"`. |
|
|
195
205
|
| `excludeTools` | `string[]` | Tool names to ignore when counting calls. |
|
|
196
206
|
|
|
207
|
+
## Hosted OpenAI-compatible providers
|
|
208
|
+
|
|
209
|
+
Smoltalk ships four built-in providers for hosted open-source models that all
|
|
210
|
+
speak the OpenAI chat-completions shape. Use these when you want to run a
|
|
211
|
+
Llama, GLM, Qwen, etc. via someone else's hosted infrastructure without
|
|
212
|
+
adding a new dependency. You must pass `provider:` explicitly because these
|
|
213
|
+
model ids aren't in the smoltalk registry.
|
|
214
|
+
|
|
215
|
+
| `provider:` | What it is | Required config | Cost source |
|
|
216
|
+
|-------------|------------|-----------------|-------------|
|
|
217
|
+
| `"openrouter"` | OpenRouter.ai aggregator | `apiKey.openRouter` (or `OPENROUTER_API_KEY`) | `usage.cost` (auto-enabled by injecting `usage:{include:true}`) |
|
|
218
|
+
| `"deepinfra"` | DeepInfra hosted models | `apiKey.deepInfra` (or `DEEPINFRA_API_KEY`) | `usage.estimated_cost` |
|
|
219
|
+
| `"litellm"` | Your own LiteLLM proxy | `apiKey.liteLlm` + `baseUrl.liteLlm` (or `LITELLM_API_KEY` / `LITELLM_BASE_URL`) | `x-litellm-response-cost` header (non-stream only) |
|
|
220
|
+
| `"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 |
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
import { textSync, userMessage } from "smoltalk";
|
|
224
|
+
|
|
225
|
+
const r = await textSync({
|
|
226
|
+
model: "z-ai/glm-5.2",
|
|
227
|
+
provider: "openrouter",
|
|
228
|
+
apiKey: { openRouter: process.env.OPENROUTER_API_KEY! },
|
|
229
|
+
messages: [userMessage("hi")],
|
|
230
|
+
});
|
|
231
|
+
// r.value.cost.totalCost is a real OpenRouter-reported USD cost.
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
**Capability matrix:**
|
|
235
|
+
|
|
236
|
+
| | chat | embeddings | image generation | `web_search` hosted tool |
|
|
237
|
+
|-----------------|------|------------|------------------|--------------------------|
|
|
238
|
+
| `openrouter` | ✅ | ❌ | ❌ | ✅ (via `:online` / web plugin) |
|
|
239
|
+
| `deepinfra` | ✅ | ✅ | ❌ (uses per-model endpoints, not OpenAI shape) | ❌ |
|
|
240
|
+
| `litellm` | ✅ | ✅ | ✅ (if the upstream model supports it) | ✅ (if upstream supports it) |
|
|
241
|
+
| `openai-compat` | ✅ | ✅ | ✅ (backend-dependent) | depends on backend |
|
|
242
|
+
|
|
243
|
+
Smoltalk surfaces a clear `failure(...)` from `embed()`/`image()` for the
|
|
244
|
+
unsupported combinations rather than silently dropping the call.
|
|
245
|
+
|
|
246
|
+
**Running a local LiteLLM proxy:**
|
|
247
|
+
|
|
248
|
+
```bash
|
|
249
|
+
pip install 'litellm[proxy]'
|
|
250
|
+
litellm --model openai/gpt-4o
|
|
251
|
+
# In your code: baseUrl: { liteLlm: "http://localhost:4000" }
|
|
252
|
+
```
|
|
253
|
+
|
|
197
254
|
## Refreshing model data
|
|
198
255
|
|
|
199
256
|
Smoltalk ships a baked-in model registry (pricing, context limits, capabilities).
|
package/dist/client.d.ts
CHANGED
|
@@ -4,6 +4,10 @@ export * from "./clients/openai.js";
|
|
|
4
4
|
export * from "./clients/openaiResponses.js";
|
|
5
5
|
export * from "./clients/baseClient.js";
|
|
6
6
|
export * from "./clients/ollama.js";
|
|
7
|
+
export * from "./clients/openaiCompat.js";
|
|
8
|
+
export * from "./clients/openrouter.js";
|
|
9
|
+
export * from "./clients/deepinfra.js";
|
|
10
|
+
export * from "./clients/litellm.js";
|
|
7
11
|
import { BaseClient } from "./clients/baseClient.js";
|
|
8
12
|
import { SmolClientConfig } from "./types.js";
|
|
9
13
|
export declare function registerProvider(providerName: string, clientClass: typeof BaseClient): void;
|
package/dist/client.js
CHANGED
|
@@ -4,11 +4,19 @@ export * from "./clients/openai.js";
|
|
|
4
4
|
export * from "./clients/openaiResponses.js";
|
|
5
5
|
export * from "./clients/baseClient.js";
|
|
6
6
|
export * from "./clients/ollama.js";
|
|
7
|
+
export * from "./clients/openaiCompat.js";
|
|
8
|
+
export * from "./clients/openrouter.js";
|
|
9
|
+
export * from "./clients/deepinfra.js";
|
|
10
|
+
export * from "./clients/litellm.js";
|
|
7
11
|
import { SmolAnthropic } from "./clients/anthropic.js";
|
|
8
12
|
import { SmolGoogle } from "./clients/google.js";
|
|
9
13
|
import { SmolOllama } from "./clients/ollama.js";
|
|
10
14
|
import { SmolOpenAi } from "./clients/openai.js";
|
|
11
15
|
import { SmolOpenAiResponses } from "./clients/openaiResponses.js";
|
|
16
|
+
import { SmolOpenAiCompat } from "./clients/openaiCompat.js";
|
|
17
|
+
import { SmolOpenRouter } from "./clients/openrouter.js";
|
|
18
|
+
import { SmolDeepInfra } from "./clients/deepinfra.js";
|
|
19
|
+
import { SmolLiteLlm } from "./clients/litellm.js";
|
|
12
20
|
import { getModel, isTextModel } from "./models.js";
|
|
13
21
|
import { SmolError } from "./smolError.js";
|
|
14
22
|
import { resolveApiKey, resolveProvider } from "./util/provider.js";
|
|
@@ -36,43 +44,56 @@ export function getClient(config) {
|
|
|
36
44
|
throw new SmolError(`Only text models are supported currently. ${modelName} is a ${model?.type} model.`);
|
|
37
45
|
}
|
|
38
46
|
}
|
|
47
|
+
// Resolve API keys from config or env vars and inject them back into the
|
|
48
|
+
// nested apiKey map. The client constructors also read env vars directly,
|
|
49
|
+
// but this lets getClient surface a clear error before construction.
|
|
39
50
|
const resolvedKeys = {
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
openAi: resolveApiKey("openai", config),
|
|
52
|
+
google: resolveApiKey("google", config),
|
|
53
|
+
anthropic: resolveApiKey("anthropic", config),
|
|
54
|
+
ollama: resolveApiKey("ollama", config),
|
|
55
|
+
openRouter: resolveApiKey("openrouter", config),
|
|
56
|
+
deepInfra: resolveApiKey("deepinfra", config),
|
|
57
|
+
liteLlm: resolveApiKey("litellm", config),
|
|
58
|
+
openAiCompat: resolveApiKey("openai-compat", config),
|
|
43
59
|
};
|
|
44
60
|
const clientConfig = {
|
|
45
61
|
messages: [],
|
|
46
62
|
...config,
|
|
47
|
-
...resolvedKeys,
|
|
63
|
+
apiKey: { ...resolvedKeys, ...config.apiKey },
|
|
48
64
|
model: modelName,
|
|
49
65
|
};
|
|
50
66
|
switch (provider) {
|
|
51
67
|
case "anthropic":
|
|
52
|
-
if (!resolvedKeys.
|
|
53
|
-
throw new SmolError("No Anthropic API key provided. Please provide an Anthropic API key in the config using
|
|
68
|
+
if (!resolvedKeys.anthropic) {
|
|
69
|
+
throw new SmolError("No Anthropic API key provided. Please provide an Anthropic API key in the config using apiKey.anthropic, or set the ANTHROPIC_API_KEY environment variable.");
|
|
54
70
|
}
|
|
55
|
-
return new SmolAnthropic(
|
|
56
|
-
...clientConfig,
|
|
57
|
-
anthropicApiKey: resolvedKeys.anthropicApiKey,
|
|
58
|
-
});
|
|
71
|
+
return new SmolAnthropic(clientConfig);
|
|
59
72
|
case "openai":
|
|
60
|
-
if (!resolvedKeys.
|
|
61
|
-
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using
|
|
73
|
+
if (!resolvedKeys.openAi) {
|
|
74
|
+
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using apiKey.openAi, or set the OPENAI_API_KEY environment variable.");
|
|
62
75
|
}
|
|
63
76
|
return new SmolOpenAi(clientConfig);
|
|
64
77
|
case "openai-responses":
|
|
65
|
-
if (!resolvedKeys.
|
|
66
|
-
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using
|
|
78
|
+
if (!resolvedKeys.openAi) {
|
|
79
|
+
throw new SmolError("No OpenAI API key provided. Please provide an OpenAI API key in the config using apiKey.openAi, or set the OPENAI_API_KEY environment variable.");
|
|
67
80
|
}
|
|
68
81
|
return new SmolOpenAiResponses(clientConfig);
|
|
69
82
|
case "google":
|
|
70
|
-
if (!resolvedKeys.
|
|
71
|
-
throw new SmolError("No Google API key provided. Please provide a Google API key in the config using
|
|
83
|
+
if (!resolvedKeys.google) {
|
|
84
|
+
throw new SmolError("No Google API key provided. Please provide a Google API key in the config using apiKey.google, or set the GEMINI_API_KEY environment variable.");
|
|
72
85
|
}
|
|
73
86
|
return new SmolGoogle(clientConfig);
|
|
74
87
|
case "ollama":
|
|
75
88
|
return new SmolOllama(clientConfig);
|
|
89
|
+
case "openrouter":
|
|
90
|
+
return new SmolOpenRouter(clientConfig);
|
|
91
|
+
case "deepinfra":
|
|
92
|
+
return new SmolDeepInfra(clientConfig);
|
|
93
|
+
case "litellm":
|
|
94
|
+
return new SmolLiteLlm(clientConfig);
|
|
95
|
+
case "openai-compat":
|
|
96
|
+
return new SmolOpenAiCompat(clientConfig);
|
|
76
97
|
default:
|
|
77
98
|
if (provider in registeredProviders) {
|
|
78
99
|
const ClientClass = registeredProviders[provider];
|
|
@@ -3,9 +3,7 @@ import { BaseClient } from "./baseClient.js";
|
|
|
3
3
|
import { ModelName } from "../models.js";
|
|
4
4
|
export declare function anthropicWebSearchEntries(hostedTools?: string[]): any[];
|
|
5
5
|
export declare function parseAnthropicHostedTools(response: any, provider: string): HostedToolResult[];
|
|
6
|
-
export type SmolAnthropicConfig = SmolConfig
|
|
7
|
-
anthropicApiKey: string;
|
|
8
|
-
};
|
|
6
|
+
export type SmolAnthropicConfig = SmolConfig;
|
|
9
7
|
export declare class SmolAnthropic extends BaseClient implements SmolClient {
|
|
10
8
|
private client;
|
|
11
9
|
private logger;
|
|
@@ -149,7 +149,11 @@ export class SmolAnthropic extends BaseClient {
|
|
|
149
149
|
model;
|
|
150
150
|
constructor(config) {
|
|
151
151
|
super(config);
|
|
152
|
-
|
|
152
|
+
const apiKey = config.apiKey?.anthropic || process.env.ANTHROPIC_API_KEY;
|
|
153
|
+
if (!apiKey) {
|
|
154
|
+
throw new Error("Anthropic API key is required for SmolAnthropic client.");
|
|
155
|
+
}
|
|
156
|
+
this.client = new Anthropic({ apiKey });
|
|
153
157
|
this.logger = getLogger();
|
|
154
158
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
155
159
|
}
|
|
@@ -56,7 +56,7 @@ export class BaseClient {
|
|
|
56
56
|
const messageLimitResult = this.checkMessageLimit(promptConfig);
|
|
57
57
|
if (messageLimitResult)
|
|
58
58
|
return messageLimitResult;
|
|
59
|
-
const hostedError = validateHostedTools(promptConfig.hostedTools, promptConfig.model, promptConfig.modelData);
|
|
59
|
+
const hostedError = validateHostedTools(promptConfig.hostedTools, promptConfig.model, promptConfig.provider, promptConfig.modelData);
|
|
60
60
|
if (hostedError) {
|
|
61
61
|
return failure(hostedError);
|
|
62
62
|
}
|
|
@@ -269,7 +269,7 @@ export class BaseClient {
|
|
|
269
269
|
};
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
|
-
const hostedError = validateHostedTools(config.hostedTools, config.model, config.modelData);
|
|
272
|
+
const hostedError = validateHostedTools(config.hostedTools, config.model, config.provider, config.modelData);
|
|
273
273
|
if (hostedError) {
|
|
274
274
|
yield { type: "error", error: hostedError };
|
|
275
275
|
return;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import type { SmolConfig } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* DeepInfra client (https://deepinfra.com). OpenAI-compatible chat completions
|
|
5
|
+
* at the `/v1/openai` base URL.
|
|
6
|
+
*
|
|
7
|
+
* - Baked base URL `https://api.deepinfra.com/v1/openai` (override via config.baseUrl.deepInfra).
|
|
8
|
+
* - Key: config.apiKey.deepInfra or env DEEPINFRA_API_KEY.
|
|
9
|
+
* - Cost: reads `usage.estimated_cost` (USD).
|
|
10
|
+
*/
|
|
11
|
+
export declare class SmolDeepInfra extends SmolOpenAiCompat {
|
|
12
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
13
|
+
apiKey: string;
|
|
14
|
+
baseURL: string;
|
|
15
|
+
};
|
|
16
|
+
protected resolveCostUsd(usage: any): number | undefined;
|
|
17
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* DeepInfra client (https://deepinfra.com). OpenAI-compatible chat completions
|
|
5
|
+
* at the `/v1/openai` base URL.
|
|
6
|
+
*
|
|
7
|
+
* - Baked base URL `https://api.deepinfra.com/v1/openai` (override via config.baseUrl.deepInfra).
|
|
8
|
+
* - Key: config.apiKey.deepInfra or env DEEPINFRA_API_KEY.
|
|
9
|
+
* - Cost: reads `usage.estimated_cost` (USD).
|
|
10
|
+
*/
|
|
11
|
+
export class SmolDeepInfra extends SmolOpenAiCompat {
|
|
12
|
+
resolveClientOptions(config) {
|
|
13
|
+
const apiKey = resolveApiKey("deepinfra", config);
|
|
14
|
+
// resolveBaseUrl bakes in the deepinfra default, so this is always defined.
|
|
15
|
+
const baseURL = resolveBaseUrl("deepinfra", config);
|
|
16
|
+
if (!apiKey) {
|
|
17
|
+
throw new Error("deepinfra: API key required (config.apiKey.deepInfra or DEEPINFRA_API_KEY).");
|
|
18
|
+
}
|
|
19
|
+
return { apiKey, baseURL };
|
|
20
|
+
}
|
|
21
|
+
resolveCostUsd(usage) {
|
|
22
|
+
// DeepInfra returns usage.estimated_cost (USD) on chat completions.
|
|
23
|
+
return typeof usage?.estimated_cost === "number"
|
|
24
|
+
? usage.estimated_cost
|
|
25
|
+
: undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
package/dist/clients/google.js
CHANGED
|
@@ -68,10 +68,11 @@ export class SmolGoogle extends BaseClient {
|
|
|
68
68
|
model;
|
|
69
69
|
constructor(config) {
|
|
70
70
|
super(config);
|
|
71
|
-
|
|
71
|
+
const apiKey = config.apiKey?.google || process.env.GEMINI_API_KEY;
|
|
72
|
+
if (!apiKey) {
|
|
72
73
|
throw new Error("Google API key is required for SmolGoogle client.");
|
|
73
74
|
}
|
|
74
|
-
this.client = new GoogleGenAI({ apiKey
|
|
75
|
+
this.client = new GoogleGenAI({ apiKey });
|
|
75
76
|
this.logger = getLogger();
|
|
76
77
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
77
78
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import type { SmolConfig } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* LiteLLM proxy client (https://docs.litellm.ai/docs/simple_proxy). OpenAI-
|
|
5
|
+
* compatible chat completions through a user-run LiteLLM proxy.
|
|
6
|
+
*
|
|
7
|
+
* - Base URL is REQUIRED from config.baseUrl.liteLlm or env LITELLM_BASE_URL.
|
|
8
|
+
* - Key: config.apiKey.liteLlm or env LITELLM_API_KEY.
|
|
9
|
+
* - Cost: reads the `x-litellm-response-cost` header (USD). Only available
|
|
10
|
+
* for non-streaming responses; streaming requests fall back to the
|
|
11
|
+
* smoltalk model registry.
|
|
12
|
+
*/
|
|
13
|
+
export declare class SmolLiteLlm extends SmolOpenAiCompat {
|
|
14
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseURL: string;
|
|
17
|
+
};
|
|
18
|
+
protected resolveCostUsd(_usage: any, rawResponse?: Response): number | undefined;
|
|
19
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { SmolOpenAiCompat } from "./openaiCompat.js";
|
|
2
|
+
import { resolveApiKey, resolveBaseUrl } from "../util/provider.js";
|
|
3
|
+
/**
|
|
4
|
+
* LiteLLM proxy client (https://docs.litellm.ai/docs/simple_proxy). OpenAI-
|
|
5
|
+
* compatible chat completions through a user-run LiteLLM proxy.
|
|
6
|
+
*
|
|
7
|
+
* - Base URL is REQUIRED from config.baseUrl.liteLlm or env LITELLM_BASE_URL.
|
|
8
|
+
* - Key: config.apiKey.liteLlm or env LITELLM_API_KEY.
|
|
9
|
+
* - Cost: reads the `x-litellm-response-cost` header (USD). Only available
|
|
10
|
+
* for non-streaming responses; streaming requests fall back to the
|
|
11
|
+
* smoltalk model registry.
|
|
12
|
+
*/
|
|
13
|
+
export class SmolLiteLlm extends SmolOpenAiCompat {
|
|
14
|
+
resolveClientOptions(config) {
|
|
15
|
+
const apiKey = resolveApiKey("litellm", config);
|
|
16
|
+
const baseURL = resolveBaseUrl("litellm", config);
|
|
17
|
+
if (!apiKey) {
|
|
18
|
+
throw new Error("litellm: API key required (config.apiKey.liteLlm or LITELLM_API_KEY).");
|
|
19
|
+
}
|
|
20
|
+
if (!baseURL) {
|
|
21
|
+
throw new Error("litellm: base URL required (config.baseUrl.liteLlm or LITELLM_BASE_URL).");
|
|
22
|
+
}
|
|
23
|
+
return { apiKey, baseURL };
|
|
24
|
+
}
|
|
25
|
+
resolveCostUsd(_usage, rawResponse) {
|
|
26
|
+
const header = rawResponse?.headers?.get?.("x-litellm-response-cost");
|
|
27
|
+
if (!header)
|
|
28
|
+
return undefined;
|
|
29
|
+
const n = Number(header);
|
|
30
|
+
return Number.isFinite(n) ? n : undefined;
|
|
31
|
+
}
|
|
32
|
+
}
|
package/dist/clients/ollama.js
CHANGED
|
@@ -4,6 +4,7 @@ import { getLogger } from "../util/logger.js";
|
|
|
4
4
|
import { success, } from "../types.js";
|
|
5
5
|
import { zodToGoogleTool } from "../util/tool.js";
|
|
6
6
|
import { sanitizeAttributes } from "../util/util.js";
|
|
7
|
+
import { resolveBaseUrl } from "../util/provider.js";
|
|
7
8
|
import { BaseClient } from "./baseClient.js";
|
|
8
9
|
import { SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
9
10
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
@@ -17,14 +18,15 @@ export class SmolOllama extends BaseClient {
|
|
|
17
18
|
super(config);
|
|
18
19
|
this.logger = getLogger();
|
|
19
20
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
20
|
-
|
|
21
|
+
const apiKey = config.apiKey?.ollama;
|
|
22
|
+
if (apiKey) {
|
|
21
23
|
this.client = new Ollama({
|
|
22
24
|
host: "https://cloud.ollama.com",
|
|
23
|
-
headers: { Authorization: "Bearer " +
|
|
25
|
+
headers: { Authorization: "Bearer " + apiKey },
|
|
24
26
|
});
|
|
25
27
|
}
|
|
26
28
|
else {
|
|
27
|
-
const host = config
|
|
29
|
+
const host = resolveBaseUrl("ollama", config) || DEFAULT_OLLAMA_HOST;
|
|
28
30
|
this.client = new Ollama({ host });
|
|
29
31
|
}
|
|
30
32
|
}
|
package/dist/clients/openai.d.ts
CHANGED
|
@@ -1,16 +1,52 @@
|
|
|
1
1
|
import OpenAI from "openai";
|
|
2
|
-
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
|
|
2
|
+
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
|
|
3
|
+
import { EgonLog } from "../util/logger.js";
|
|
3
4
|
import { BaseClient } from "./baseClient.js";
|
|
4
5
|
import { ModelName } from "../models.js";
|
|
6
|
+
import { Model } from "../model.js";
|
|
7
|
+
import { CostEstimate, TokenUsage } from "../types.js";
|
|
5
8
|
export type SmolOpenAiConfig = SmolConfig;
|
|
6
9
|
export declare class SmolOpenAi extends BaseClient implements SmolClient {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
+
protected client: OpenAI;
|
|
11
|
+
protected logger: EgonLog;
|
|
12
|
+
protected model: Model;
|
|
10
13
|
constructor(config: SmolOpenAiConfig);
|
|
14
|
+
/**
|
|
15
|
+
* Build the `new OpenAI({...})` options. Subclasses override to inject a
|
|
16
|
+
* different baseURL or to pull the key from a different config field.
|
|
17
|
+
*
|
|
18
|
+
* Default behavior: read OPENAI_API_KEY (or config.apiKey.openAi). Throws
|
|
19
|
+
* if the key is missing — subclasses with required keys do their own check.
|
|
20
|
+
*/
|
|
21
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
22
|
+
apiKey?: string;
|
|
23
|
+
baseURL?: string;
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Inspect the provider-returned usage (and optionally the raw HTTP response)
|
|
27
|
+
* for a USD cost figure. Returns `undefined` if the provider didn't surface
|
|
28
|
+
* one, in which case `calculateUsageAndCost` falls back to the smoltalk
|
|
29
|
+
* model registry. Subclasses (OpenRouter, DeepInfra, LiteLLM) override this.
|
|
30
|
+
*/
|
|
31
|
+
protected resolveCostUsd(_usage: any, _rawResponse?: Response): number | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Extra request body fields injected on every call. Subclasses override to
|
|
34
|
+
* add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
|
|
35
|
+
* or its `plugins: [{ id: "web" }]` for web search). Merged into the request
|
|
36
|
+
* after the standard params so it can override them.
|
|
37
|
+
*/
|
|
38
|
+
protected buildRequestExtras(_config: SmolConfig): Record<string, unknown>;
|
|
39
|
+
/**
|
|
40
|
+
* Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
|
|
41
|
+
* annotations) from a completion. Subclasses override; default returns none.
|
|
42
|
+
*/
|
|
43
|
+
protected parseHostedToolResults(_completion: any, _config: SmolConfig): HostedToolResult[];
|
|
11
44
|
getClient(): OpenAI;
|
|
12
45
|
getModel(): ModelName;
|
|
13
|
-
|
|
46
|
+
protected calculateUsageAndCost(usageData: any, rawResponse?: Response): {
|
|
47
|
+
usage?: TokenUsage;
|
|
48
|
+
cost?: CostEstimate;
|
|
49
|
+
};
|
|
14
50
|
private buildRequest;
|
|
15
51
|
private rethrowAsSmolError;
|
|
16
52
|
_textSync(config: SmolConfig): Promise<Result<PromptResult>>;
|
package/dist/clients/openai.js
CHANGED
|
@@ -14,20 +14,57 @@ export class SmolOpenAi extends BaseClient {
|
|
|
14
14
|
model;
|
|
15
15
|
constructor(config) {
|
|
16
16
|
super(config);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
20
|
-
this.client = new OpenAI({ apiKey: config.openAiApiKey });
|
|
17
|
+
const options = this.resolveClientOptions(config);
|
|
18
|
+
this.client = new OpenAI(options);
|
|
21
19
|
this.logger = getLogger();
|
|
22
20
|
this.model = new Model(config.model, undefined, config.modelData);
|
|
23
21
|
}
|
|
22
|
+
/**
|
|
23
|
+
* Build the `new OpenAI({...})` options. Subclasses override to inject a
|
|
24
|
+
* different baseURL or to pull the key from a different config field.
|
|
25
|
+
*
|
|
26
|
+
* Default behavior: read OPENAI_API_KEY (or config.apiKey.openAi). Throws
|
|
27
|
+
* if the key is missing — subclasses with required keys do their own check.
|
|
28
|
+
*/
|
|
29
|
+
resolveClientOptions(config) {
|
|
30
|
+
const apiKey = config.apiKey?.openAi || process.env.OPENAI_API_KEY;
|
|
31
|
+
if (!apiKey) {
|
|
32
|
+
throw new Error("OpenAI API key is required for SmolOpenAi client.");
|
|
33
|
+
}
|
|
34
|
+
return { apiKey };
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Inspect the provider-returned usage (and optionally the raw HTTP response)
|
|
38
|
+
* for a USD cost figure. Returns `undefined` if the provider didn't surface
|
|
39
|
+
* one, in which case `calculateUsageAndCost` falls back to the smoltalk
|
|
40
|
+
* model registry. Subclasses (OpenRouter, DeepInfra, LiteLLM) override this.
|
|
41
|
+
*/
|
|
42
|
+
resolveCostUsd(_usage, _rawResponse) {
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Extra request body fields injected on every call. Subclasses override to
|
|
47
|
+
* add provider-specific request shapes (e.g. OpenRouter's `usage: { include: true }`
|
|
48
|
+
* or its `plugins: [{ id: "web" }]` for web search). Merged into the request
|
|
49
|
+
* after the standard params so it can override them.
|
|
50
|
+
*/
|
|
51
|
+
buildRequestExtras(_config) {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Extract provider-specific hosted-tool results (e.g. OpenRouter web_search
|
|
56
|
+
* annotations) from a completion. Subclasses override; default returns none.
|
|
57
|
+
*/
|
|
58
|
+
parseHostedToolResults(_completion, _config) {
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
24
61
|
getClient() {
|
|
25
62
|
return this.client;
|
|
26
63
|
}
|
|
27
64
|
getModel() {
|
|
28
65
|
return this.model.getModel();
|
|
29
66
|
}
|
|
30
|
-
calculateUsageAndCost(usageData) {
|
|
67
|
+
calculateUsageAndCost(usageData, rawResponse) {
|
|
31
68
|
let usage;
|
|
32
69
|
let cost;
|
|
33
70
|
if (usageData) {
|
|
@@ -40,9 +77,23 @@ export class SmolOpenAi extends BaseClient {
|
|
|
40
77
|
if (cached > 0) {
|
|
41
78
|
usage.cachedInputTokens = cached;
|
|
42
79
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
80
|
+
// Prefer provider-supplied cost when available (e.g. OpenRouter
|
|
81
|
+
// usage.cost, DeepInfra usage.estimated_cost, LiteLLM header).
|
|
82
|
+
// Fall back to the smoltalk model-registry calculation.
|
|
83
|
+
const providerCost = this.resolveCostUsd(usageData, rawResponse);
|
|
84
|
+
if (typeof providerCost === "number") {
|
|
85
|
+
cost = {
|
|
86
|
+
inputCost: 0,
|
|
87
|
+
outputCost: 0,
|
|
88
|
+
totalCost: providerCost,
|
|
89
|
+
currency: "USD",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
const calculatedCost = this.model.calculateCost(usage);
|
|
94
|
+
if (calculatedCost) {
|
|
95
|
+
cost = calculatedCost;
|
|
96
|
+
}
|
|
46
97
|
}
|
|
47
98
|
}
|
|
48
99
|
return { usage, cost };
|
|
@@ -61,6 +112,7 @@ export class SmolOpenAi extends BaseClient {
|
|
|
61
112
|
reasoning_effort: config.reasoningEffort,
|
|
62
113
|
}),
|
|
63
114
|
...sanitizeAttributes(config.rawAttributes),
|
|
115
|
+
...this.buildRequestExtras(config),
|
|
64
116
|
};
|
|
65
117
|
if (config.responseFormat) {
|
|
66
118
|
request.response_format = {
|
|
@@ -92,11 +144,16 @@ export class SmolOpenAi extends BaseClient {
|
|
|
92
144
|
this.statelogClient?.promptRequest(request);
|
|
93
145
|
const signal = this.getAbortSignal(config);
|
|
94
146
|
let completion;
|
|
147
|
+
let rawResponse;
|
|
95
148
|
try {
|
|
96
|
-
|
|
149
|
+
const result = await this.client.chat.completions
|
|
150
|
+
.create({
|
|
97
151
|
...request,
|
|
98
152
|
stream: false,
|
|
99
|
-
}, { ...(signal && { signal }) })
|
|
153
|
+
}, { ...(signal && { signal }) })
|
|
154
|
+
.withResponse();
|
|
155
|
+
completion = result.data;
|
|
156
|
+
rawResponse = result.response;
|
|
100
157
|
}
|
|
101
158
|
catch (error) {
|
|
102
159
|
this.rethrowAsSmolError(error);
|
|
@@ -123,14 +180,17 @@ export class SmolOpenAi extends BaseClient {
|
|
|
123
180
|
}
|
|
124
181
|
}
|
|
125
182
|
}
|
|
126
|
-
// Extract usage and calculate cost
|
|
127
|
-
|
|
183
|
+
// Extract usage and calculate cost. rawResponse lets subclasses read
|
|
184
|
+
// response headers (e.g. LiteLLM's x-litellm-response-cost).
|
|
185
|
+
const { usage, cost } = this.calculateUsageAndCost(completion.usage, rawResponse);
|
|
186
|
+
const hostedToolResults = this.parseHostedToolResults(completion, config);
|
|
128
187
|
return success({
|
|
129
188
|
output,
|
|
130
189
|
toolCalls,
|
|
131
190
|
usage,
|
|
132
191
|
cost,
|
|
133
192
|
model: this.getModel(),
|
|
193
|
+
...(hostedToolResults.length > 0 ? { hostedToolResults } : {}),
|
|
134
194
|
});
|
|
135
195
|
}
|
|
136
196
|
async *_textStream(config) {
|
|
@@ -156,6 +216,9 @@ export class SmolOpenAi extends BaseClient {
|
|
|
156
216
|
for await (const chunk of completion) {
|
|
157
217
|
// Extract usage from the final chunk
|
|
158
218
|
if (chunk.usage) {
|
|
219
|
+
// Header-based cost (LiteLLM) is unsupported while streaming.
|
|
220
|
+
// Body-based cost (OpenRouter/DeepInfra) still works because it
|
|
221
|
+
// comes through chunk.usage.
|
|
159
222
|
const usageAndCost = this.calculateUsageAndCost(chunk.usage);
|
|
160
223
|
usage = usageAndCost.usage;
|
|
161
224
|
cost = usageAndCost.cost;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { SmolOpenAi } from "./openai.js";
|
|
2
|
+
import type { SmolConfig } from "../types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Generic OpenAI-compatible client. Use when pointing smoltalk at any
|
|
5
|
+
* arbitrary OpenAI-shaped endpoint (vLLM, TGI, LM Studio, OpenLLM, etc.).
|
|
6
|
+
*
|
|
7
|
+
* Both `config.apiKey.openAiCompat` and `config.baseUrl.openAiCompat` are
|
|
8
|
+
* required (or their env-var fallbacks: `OPENAI_COMPAT_API_KEY`,
|
|
9
|
+
* `OPENAI_COMPAT_BASE_URL`).
|
|
10
|
+
*
|
|
11
|
+
* Cost: opportunistically reads `usage.cost`, `usage.estimated_cost`, or
|
|
12
|
+
* `usage.cost_usd` if the backend includes one of them. Otherwise falls back
|
|
13
|
+
* to the smoltalk model registry (which won't have these custom models, so
|
|
14
|
+
* cost stays undefined — that's expected for arbitrary backends).
|
|
15
|
+
*/
|
|
16
|
+
export declare class SmolOpenAiCompat extends SmolOpenAi {
|
|
17
|
+
protected resolveClientOptions(config: SmolConfig): {
|
|
18
|
+
apiKey: string;
|
|
19
|
+
baseURL: string;
|
|
20
|
+
};
|
|
21
|
+
protected resolveCostUsd(usage: any): number | undefined;
|
|
22
|
+
}
|