smoltalk 0.4.2 → 0.5.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 +139 -0
- package/dist/classes/message/AssistantMessage.d.ts +1 -0
- package/dist/client.js +5 -3
- package/dist/clients/anthropic.d.ts +3 -1
- package/dist/clients/anthropic.js +65 -10
- package/dist/clients/baseClient.js +11 -1
- package/dist/clients/google.d.ts +3 -0
- package/dist/clients/google.js +76 -7
- package/dist/clients/ollama.js +1 -1
- package/dist/clients/openai.js +1 -1
- package/dist/clients/openaiResponses.d.ts +3 -0
- package/dist/clients/openaiResponses.js +54 -4
- package/dist/embed/openai.js +3 -3
- package/dist/embed.d.ts +5 -2
- package/dist/embed.js +14 -3
- package/dist/image/google.js +3 -3
- package/dist/image/openai.js +6 -6
- package/dist/image.d.ts +5 -2
- package/dist/image.js +14 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/model.d.ts +4 -2
- package/dist/model.js +7 -5
- package/dist/modelData.d.ts +40 -0
- package/dist/modelData.js +222 -0
- package/dist/models.d.ts +658 -760
- package/dist/models.js +836 -60
- package/dist/types/costEstimate.d.ts +2 -0
- package/dist/types/costEstimate.js +1 -0
- package/dist/types.d.ts +30 -1
- package/dist/types.js +2 -1
- package/dist/util/hostedTools.d.ts +19 -0
- package/dist/util/hostedTools.js +104 -0
- package/dist/util/provider.d.ts +2 -1
- package/dist/util/provider.js +2 -2
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -194,6 +194,145 @@ Detects when the model is stuck in a repetitive tool-call loop.
|
|
|
194
194
|
| `intervention` | `string` | Action to take: `"remove-tool"`, `"remove-all-tools"`, `"throw-error"`, or `"halt-execution"`. |
|
|
195
195
|
| `excludeTools` | `string[]` | Tool names to ignore when counting calls. |
|
|
196
196
|
|
|
197
|
+
## Refreshing model data
|
|
198
|
+
|
|
199
|
+
Smoltalk ships a baked-in model registry (pricing, context limits, capabilities).
|
|
200
|
+
Because that data goes stale between releases, you can pull a fresh copy at
|
|
201
|
+
runtime and layer it over the built-ins. **You decide where to store it** —
|
|
202
|
+
smoltalk never writes to disk.
|
|
203
|
+
|
|
204
|
+
```ts
|
|
205
|
+
import { refreshModels, registerModelData } from "smoltalk";
|
|
206
|
+
|
|
207
|
+
// Fetch the latest data (from a URL smoltalk controls by default).
|
|
208
|
+
const result = await refreshModels();
|
|
209
|
+
if (result.success) {
|
|
210
|
+
// Persist result.value however you like (file, KV store, etc.),
|
|
211
|
+
// then register it once at startup:
|
|
212
|
+
registerModelData(result.value);
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
Precedence is **per-call `config.modelData` > `registerModelData` (global) >
|
|
217
|
+
baked-in baseline**, merged field-by-field (a refreshed field wins; missing
|
|
218
|
+
fields never erase built-in values). Per-call override:
|
|
219
|
+
|
|
220
|
+
```ts
|
|
221
|
+
import { textSync, type Message, type ModelDataBlob } from "smoltalk";
|
|
222
|
+
|
|
223
|
+
declare const messages: Message[];
|
|
224
|
+
declare const modelData: ModelDataBlob;
|
|
225
|
+
|
|
226
|
+
await textSync({ model: "claude-opus-4-8", messages, modelData });
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
Override the source URL with the `SMOLTALK_MODEL_DATA_URL` env var or
|
|
230
|
+
`refreshModels({ url })`. The URL may be remote (`https://`, e.g. your own
|
|
231
|
+
self-hosted catalog) or local (`file://…/model-data.json`). The blob also carries
|
|
232
|
+
a `hostedTools` catalog (`getHostedTools()`); the published file is kept current
|
|
233
|
+
by a daily CI job that translates [models.dev](https://models.dev) into
|
|
234
|
+
smoltalk's shape.
|
|
235
|
+
|
|
236
|
+
## Hosted tools catalog
|
|
237
|
+
|
|
238
|
+
Each cloud provider offers server-side "hosted" tools (web search, code
|
|
239
|
+
execution, file search, image generation). Smoltalk ships a catalog of what's
|
|
240
|
+
available and what it costs — query it with `getHostedTools()`:
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
import { getHostedTools, hostedToolPricingFor } from "smoltalk";
|
|
244
|
+
|
|
245
|
+
// Hosted tools usable with a given model (respects provider + model allowlists):
|
|
246
|
+
console.log(getHostedTools({ model: "claude-opus-4-8" }));
|
|
247
|
+
|
|
248
|
+
// All web-search tools across providers:
|
|
249
|
+
const search = getHostedTools({ category: "web_search" });
|
|
250
|
+
|
|
251
|
+
// Effective pricing for a tool on a specific model (applies per-model overrides):
|
|
252
|
+
const first = search[0];
|
|
253
|
+
if (first) {
|
|
254
|
+
console.log(hostedToolPricingFor(first, "gemini-2.5-pro"));
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
The catalog rides in the same refresh blob as model data, so `refreshModels()`
|
|
259
|
+
keeps it current. Local models (Ollama) have none.
|
|
260
|
+
|
|
261
|
+
### Using a hosted tool (web search)
|
|
262
|
+
|
|
263
|
+
Enable a provider's hosted web search on a call with `hostedTools` (a list of
|
|
264
|
+
capability names). It's separate from `tools` because hosted tools run
|
|
265
|
+
server-side — you can't intercept or gate them like your own functions.
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
import { textSync, type Message } from "smoltalk";
|
|
269
|
+
|
|
270
|
+
declare const messages: Message[];
|
|
271
|
+
|
|
272
|
+
const result = await textSync({
|
|
273
|
+
model: "claude-opus-4-8",
|
|
274
|
+
messages,
|
|
275
|
+
hostedTools: ["web_search"],
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Normalized across providers, regardless of who ran the search:
|
|
279
|
+
if (result.success) {
|
|
280
|
+
console.log(result.value.hostedToolResults);
|
|
281
|
+
// [{ tool: "web_search", provider: "anthropic", queries: [...], sources: [...],
|
|
282
|
+
// citations: [...], callCount: 1, estimatedCost: 0.01 }]
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
Supported on Anthropic, Google, and OpenAI **Responses-API** models. Note that
|
|
287
|
+
smoltalk routes base GPT-5 / GPT-4o to Chat Completions, so on OpenAI hosted web
|
|
288
|
+
search is available only on the `openai-responses` models (the `*-pro` variants,
|
|
289
|
+
e.g. `gpt-5-pro`). Chat-only OpenAI models (`gpt-4o`, `gpt-5`) and local models
|
|
290
|
+
return a clear error — use a search *function* (e.g. the Brave/Tavily-backed
|
|
291
|
+
stdlib tools) as a regular `tool` instead.
|
|
292
|
+
|
|
293
|
+
`estimatedCost` is an upper-bound estimate (providers report usage counts, not
|
|
294
|
+
charges; free-tier allowances are ignored). Results are populated on `textSync`;
|
|
295
|
+
streaming text is unaffected but the streamed result does not include them yet.
|
|
296
|
+
On Google, web search can't be combined with structured output in one call.
|
|
297
|
+
|
|
298
|
+
## Registering custom providers
|
|
299
|
+
|
|
300
|
+
Smoltalk has three registration entry points — one per capability:
|
|
301
|
+
|
|
302
|
+
```ts
|
|
303
|
+
// example: skip-typecheck
|
|
304
|
+
import {
|
|
305
|
+
success, // Result helper
|
|
306
|
+
registerProvider, // text generation (a class extending BaseClient)
|
|
307
|
+
registerEmbeddingProvider, // embeddings (a function)
|
|
308
|
+
registerImageProvider, // images (a function)
|
|
309
|
+
} from "smoltalk";
|
|
310
|
+
|
|
311
|
+
// Text: a class extending BaseClient (implements _textSync / _textStream)
|
|
312
|
+
registerProvider("my-llm", MyTextClient);
|
|
313
|
+
|
|
314
|
+
// Embeddings: a function
|
|
315
|
+
registerEmbeddingProvider("my-embed", async (inputs, config) => {
|
|
316
|
+
// read credentials from config (e.g. config.metadata), call your service
|
|
317
|
+
return success({ embeddings: [...], model: config.model });
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
// Images: a function
|
|
321
|
+
registerImageProvider("my-image", async (input, config) => {
|
|
322
|
+
return success({ images: [...], model: config.model });
|
|
323
|
+
});
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
Select a custom provider by passing `provider` in the call config
|
|
327
|
+
(`embed(input, { provider: "my-embed", model })`,
|
|
328
|
+
`image(input, { provider: "my-image", model })`). Built-in providers always take
|
|
329
|
+
precedence; a registered name that collides with a built-in is ignored. Custom
|
|
330
|
+
providers receive the full `config` and read their own credentials from it
|
|
331
|
+
(e.g. `config.metadata`).
|
|
332
|
+
|
|
333
|
+
Text is a class (it needs retries, tool-loop detection, streaming); embeddings
|
|
334
|
+
and images are one-shot functions.
|
|
335
|
+
|
|
197
336
|
## Limitations
|
|
198
337
|
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:
|
|
199
338
|
|
|
@@ -37,6 +37,7 @@ export declare const AssistantMessageJSONSchema: z.ZodObject<{
|
|
|
37
37
|
outputCost: z.ZodNumber;
|
|
38
38
|
cachedInputCost: z.ZodOptional<z.ZodNumber>;
|
|
39
39
|
cacheCreationInputCost: z.ZodOptional<z.ZodNumber>;
|
|
40
|
+
hostedToolsCost: z.ZodOptional<z.ZodNumber>;
|
|
40
41
|
totalCost: z.ZodNumber;
|
|
41
42
|
currency: z.ZodString;
|
|
42
43
|
}, z.core.$strip>>;
|
package/dist/client.js
CHANGED
|
@@ -12,7 +12,9 @@ import { SmolOpenAiResponses } from "./clients/openaiResponses.js";
|
|
|
12
12
|
import { getModel, isTextModel } from "./models.js";
|
|
13
13
|
import { SmolError } from "./smolError.js";
|
|
14
14
|
import { resolveApiKey, resolveProvider } from "./util/provider.js";
|
|
15
|
-
|
|
15
|
+
// Null-prototype so provider names like "toString"/"__proto__" can't collide
|
|
16
|
+
// with Object.prototype or pollute the registry.
|
|
17
|
+
const registeredProviders = Object.create(null);
|
|
16
18
|
export function registerProvider(providerName, clientClass) {
|
|
17
19
|
registeredProviders[providerName] = clientClass;
|
|
18
20
|
}
|
|
@@ -25,11 +27,11 @@ export function unregisterProvider(providerName) {
|
|
|
25
27
|
}
|
|
26
28
|
export function getClient(config) {
|
|
27
29
|
const modelName = config.model;
|
|
28
|
-
const provider = resolveProvider(modelName, config.provider);
|
|
30
|
+
const provider = resolveProvider(modelName, config.provider, config.modelData);
|
|
29
31
|
// For getClient, validate that the model is a text model when no explicit
|
|
30
32
|
// provider is given (since this factory only returns text-generation clients).
|
|
31
33
|
if (!config.provider) {
|
|
32
|
-
const model = getModel(modelName);
|
|
34
|
+
const model = getModel(modelName, config.modelData);
|
|
33
35
|
if (model && !isTextModel(model)) {
|
|
34
36
|
throw new SmolError(`Only text models are supported currently. ${modelName} is a ${model?.type} model.`);
|
|
35
37
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
|
|
1
|
+
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
|
|
2
2
|
import { BaseClient } from "./baseClient.js";
|
|
3
3
|
import { ModelName } from "../models.js";
|
|
4
|
+
export declare function anthropicWebSearchEntries(hostedTools?: string[]): any[];
|
|
5
|
+
export declare function parseAnthropicHostedTools(response: any, provider: string): HostedToolResult[];
|
|
4
6
|
export type SmolAnthropicConfig = SmolConfig & {
|
|
5
7
|
anthropicApiKey: string;
|
|
6
8
|
};
|
|
@@ -3,6 +3,7 @@ import { ToolCall } from "../classes/ToolCall.js";
|
|
|
3
3
|
import { SystemMessage, DeveloperMessage } from "../classes/message/index.js";
|
|
4
4
|
import { getLogger } from "../util/logger.js";
|
|
5
5
|
import { success, } from "../types.js";
|
|
6
|
+
import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
|
|
6
7
|
import { zodToAnthropicTool } from "../util/tool.js";
|
|
7
8
|
import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
8
9
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
@@ -10,14 +11,57 @@ import { BaseClient } from "./baseClient.js";
|
|
|
10
11
|
import { getModel, isTextModel } from "../models.js";
|
|
11
12
|
import { Model } from "../model.js";
|
|
12
13
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
14
|
+
export function anthropicWebSearchEntries(hostedTools) {
|
|
15
|
+
if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
|
|
16
|
+
return [{ type: "web_search_20250305", name: "web_search" }];
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
export function parseAnthropicHostedTools(response, provider) {
|
|
21
|
+
const queries = [];
|
|
22
|
+
const sources = [];
|
|
23
|
+
const citations = [];
|
|
24
|
+
const raw = [];
|
|
25
|
+
for (const block of response.content || []) {
|
|
26
|
+
if (block.type === "server_tool_use" && block.name === "web_search") {
|
|
27
|
+
raw.push(block);
|
|
28
|
+
// input.query is always the search string; guard only against a malformed block.
|
|
29
|
+
if (block.input?.query) {
|
|
30
|
+
queries.push(block.input.query);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (block.type === "web_search_tool_result") {
|
|
34
|
+
raw.push(block);
|
|
35
|
+
// content items are web_search_result entries; an error variant has no url.
|
|
36
|
+
for (const r of block.content || []) {
|
|
37
|
+
if (r && typeof r.url === "string") {
|
|
38
|
+
sources.push({ url: r.url, title: r.title });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
else if (block.type === "text" && Array.isArray(block.citations)) {
|
|
43
|
+
for (const c of block.citations) {
|
|
44
|
+
if (c && typeof c.url === "string") {
|
|
45
|
+
citations.push({ url: c.url, title: c.title });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const callCount = response.usage?.server_tool_use?.web_search_requests;
|
|
51
|
+
const used = queries.length > 0 || sources.length > 0 || (callCount ?? 0) > 0;
|
|
52
|
+
if (!used) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
return [webSearchResult(provider, { queries, sources, citations, callCount, raw })];
|
|
56
|
+
}
|
|
13
57
|
/**
|
|
14
58
|
* Which thinking API a model speaks. New flagship Anthropic models (Opus 4.7+)
|
|
15
59
|
* reject the legacy `{type: "enabled", budget_tokens}` form with a 400, so we
|
|
16
60
|
* default unknown/unregistered models to "adaptive" (the forward-looking shape)
|
|
17
61
|
* rather than the form that's being phased out.
|
|
18
62
|
*/
|
|
19
|
-
function thinkingStyleFor(modelName) {
|
|
20
|
-
const model = getModel(modelName);
|
|
63
|
+
function thinkingStyleFor(modelName, modelData) {
|
|
64
|
+
const model = getModel(modelName, modelData);
|
|
21
65
|
if (model && isTextModel(model) && model.reasoning?.thinkingStyle) {
|
|
22
66
|
return model.reasoning.thinkingStyle;
|
|
23
67
|
}
|
|
@@ -107,7 +151,7 @@ export class SmolAnthropic extends BaseClient {
|
|
|
107
151
|
super(config);
|
|
108
152
|
this.client = new Anthropic({ apiKey: config.anthropicApiKey });
|
|
109
153
|
this.logger = getLogger();
|
|
110
|
-
this.model = new Model(config.model);
|
|
154
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
111
155
|
}
|
|
112
156
|
getModel() {
|
|
113
157
|
return this.model.getModel();
|
|
@@ -162,11 +206,14 @@ export class SmolAnthropic extends BaseClient {
|
|
|
162
206
|
}
|
|
163
207
|
anthropicMessages.push(converted);
|
|
164
208
|
}
|
|
165
|
-
const
|
|
209
|
+
const functionTools = config.tools && config.tools.length > 0
|
|
166
210
|
? config.tools.map((tool) => zodToAnthropicTool(tool.name, tool.schema, {
|
|
167
211
|
description: tool.description,
|
|
168
212
|
}))
|
|
169
|
-
:
|
|
213
|
+
: [];
|
|
214
|
+
const hostedEntries = anthropicWebSearchEntries(config.hostedTools);
|
|
215
|
+
const allTools = [...functionTools, ...hostedEntries];
|
|
216
|
+
const tools = allTools.length > 0 ? allTools : undefined;
|
|
170
217
|
// Normalize the user's provider-agnostic thinking/effort config into the
|
|
171
218
|
// shape this specific model accepts. Both `thinking.enabled` and
|
|
172
219
|
// `reasoningEffort` are treated as a request to think.
|
|
@@ -190,7 +237,7 @@ export class SmolAnthropic extends BaseClient {
|
|
|
190
237
|
if (!wantsThinking && !effort) {
|
|
191
238
|
return { thinking: undefined, outputConfig: undefined };
|
|
192
239
|
}
|
|
193
|
-
if (thinkingStyleFor(this.getModel()) === "adaptive") {
|
|
240
|
+
if (thinkingStyleFor(this.getModel(), this.config.modelData) === "adaptive") {
|
|
194
241
|
// `output_config.effort` controls depth; the budget is irrelevant here.
|
|
195
242
|
return {
|
|
196
243
|
thinking: { type: "adaptive" },
|
|
@@ -283,14 +330,22 @@ export class SmolAnthropic extends BaseClient {
|
|
|
283
330
|
}
|
|
284
331
|
}
|
|
285
332
|
const { usage, cost } = this.calculateUsageAndCost(response.usage);
|
|
286
|
-
|
|
333
|
+
const parsed = parseAnthropicHostedTools(response, "anthropic");
|
|
334
|
+
const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, this.getModel(), this.config.modelData);
|
|
335
|
+
const result = {
|
|
287
336
|
output,
|
|
288
337
|
toolCalls,
|
|
289
|
-
...(thinkingBlocks.length > 0 && { thinkingBlocks }),
|
|
290
338
|
usage,
|
|
291
|
-
cost,
|
|
339
|
+
cost: finalCost,
|
|
292
340
|
model: this.getModel(),
|
|
293
|
-
}
|
|
341
|
+
};
|
|
342
|
+
if (thinkingBlocks.length > 0) {
|
|
343
|
+
result.thinkingBlocks = thinkingBlocks;
|
|
344
|
+
}
|
|
345
|
+
if (hostedToolResults.length > 0) {
|
|
346
|
+
result.hostedToolResults = hostedToolResults;
|
|
347
|
+
}
|
|
348
|
+
return success(result);
|
|
294
349
|
}
|
|
295
350
|
async *_textStream(config) {
|
|
296
351
|
const { system, messages, tools, thinking, outputConfig } = this.buildRequest(config);
|
|
@@ -3,7 +3,8 @@ import { getLogger } from "../util/logger.js";
|
|
|
3
3
|
import { SmolStructuredOutputError } from "../smolError.js";
|
|
4
4
|
import { getStatelogClient } from "../statelogClient.js";
|
|
5
5
|
import { stripCodeFence } from "../util/util.js";
|
|
6
|
-
import { success, } from "../types.js";
|
|
6
|
+
import { success, failure, } from "../types.js";
|
|
7
|
+
import { validateHostedTools } from "../util/hostedTools.js";
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
const DEFAULT_NUM_RETRIES = 2;
|
|
9
10
|
export class BaseClient {
|
|
@@ -55,6 +56,10 @@ export class BaseClient {
|
|
|
55
56
|
const messageLimitResult = this.checkMessageLimit(promptConfig);
|
|
56
57
|
if (messageLimitResult)
|
|
57
58
|
return messageLimitResult;
|
|
59
|
+
const hostedError = validateHostedTools(promptConfig.hostedTools, promptConfig.model, promptConfig.modelData);
|
|
60
|
+
if (hostedError) {
|
|
61
|
+
return failure(hostedError);
|
|
62
|
+
}
|
|
58
63
|
const { continue: shouldContinue, newSmolConfig } = this.checkForToolLoops(promptConfig);
|
|
59
64
|
if (!shouldContinue) {
|
|
60
65
|
return {
|
|
@@ -264,6 +269,11 @@ export class BaseClient {
|
|
|
264
269
|
};
|
|
265
270
|
return;
|
|
266
271
|
}
|
|
272
|
+
const hostedError = validateHostedTools(config.hostedTools, config.model, config.modelData);
|
|
273
|
+
if (hostedError) {
|
|
274
|
+
yield { type: "error", error: hostedError };
|
|
275
|
+
return;
|
|
276
|
+
}
|
|
267
277
|
const { continue: shouldContinue, newSmolConfig } = this.checkForToolLoops(config);
|
|
268
278
|
if (!shouldContinue) {
|
|
269
279
|
yield {
|
package/dist/clients/google.d.ts
CHANGED
|
@@ -2,7 +2,10 @@ import { Content, GenerateContentConfig, GoogleGenAI } from "@google/genai";
|
|
|
2
2
|
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
|
|
3
3
|
import { BaseClient } from "./baseClient.js";
|
|
4
4
|
import { ModelName } from "../models.js";
|
|
5
|
+
import { HostedToolResult } from "../types.js";
|
|
5
6
|
export type SmolGoogleConfig = SmolConfig;
|
|
7
|
+
export declare function googleWebSearchEntries(hostedTools?: string[]): any[];
|
|
8
|
+
export declare function parseGoogleHostedTools(result: any, provider: string, model: string): HostedToolResult[];
|
|
6
9
|
type GeneratedRequest = {
|
|
7
10
|
contents: Content[];
|
|
8
11
|
model: ModelName;
|
package/dist/clients/google.js
CHANGED
|
@@ -7,8 +7,61 @@ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForSta
|
|
|
7
7
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
8
8
|
import { sanitizeAttributes } from "../util/util.js";
|
|
9
9
|
import { BaseClient } from "./baseClient.js";
|
|
10
|
+
import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
|
|
10
11
|
import { Model } from "../model.js";
|
|
11
12
|
import { userMessage } from "../classes/message/index.js";
|
|
13
|
+
export function googleWebSearchEntries(hostedTools) {
|
|
14
|
+
if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
|
|
15
|
+
return [{ googleSearch: {} }];
|
|
16
|
+
}
|
|
17
|
+
return [];
|
|
18
|
+
}
|
|
19
|
+
export function parseGoogleHostedTools(result, provider, model) {
|
|
20
|
+
const queries = [];
|
|
21
|
+
const sources = [];
|
|
22
|
+
const citations = [];
|
|
23
|
+
const raw = [];
|
|
24
|
+
for (const candidate of result.candidates || []) {
|
|
25
|
+
const gm = candidate.groundingMetadata;
|
|
26
|
+
// A candidate without groundingMetadata simply didn't ground — nothing to
|
|
27
|
+
// extract. If no candidate grounded, this returns [] overall.
|
|
28
|
+
if (!gm) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
raw.push(gm);
|
|
32
|
+
for (const q of gm.webSearchQueries || []) {
|
|
33
|
+
queries.push(q);
|
|
34
|
+
}
|
|
35
|
+
const chunks = gm.groundingChunks || [];
|
|
36
|
+
for (const c of chunks) {
|
|
37
|
+
if (c.web && typeof c.web.uri === "string") {
|
|
38
|
+
sources.push({ url: c.web.uri, title: c.web.title });
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
for (const s of gm.groundingSupports || []) {
|
|
42
|
+
for (const idx of s.groundingChunkIndices || []) {
|
|
43
|
+
const chunk = chunks[idx];
|
|
44
|
+
if (chunk && chunk.web && typeof chunk.web.uri === "string") {
|
|
45
|
+
citations.push({
|
|
46
|
+
url: chunk.web.uri,
|
|
47
|
+
title: chunk.web.title,
|
|
48
|
+
startIndex: s.segment?.startIndex,
|
|
49
|
+
endIndex: s.segment?.endIndex,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (queries.length === 0 && sources.length === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
// Gemini 2.5 bills per prompt (1), Gemini 3+ per query.
|
|
59
|
+
let callCount = queries.length;
|
|
60
|
+
if (model.startsWith("gemini-2.5")) {
|
|
61
|
+
callCount = 1;
|
|
62
|
+
}
|
|
63
|
+
return [webSearchResult(provider, { queries, sources, citations, callCount, raw })];
|
|
64
|
+
}
|
|
12
65
|
export class SmolGoogle extends BaseClient {
|
|
13
66
|
client;
|
|
14
67
|
logger;
|
|
@@ -20,7 +73,7 @@ export class SmolGoogle extends BaseClient {
|
|
|
20
73
|
}
|
|
21
74
|
this.client = new GoogleGenAI({ apiKey: config.googleApiKey });
|
|
22
75
|
this.logger = getLogger();
|
|
23
|
-
this.model = new Model(config.model);
|
|
76
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
24
77
|
}
|
|
25
78
|
getClient() {
|
|
26
79
|
return this.client;
|
|
@@ -69,8 +122,16 @@ export class SmolGoogle extends BaseClient {
|
|
|
69
122
|
if (systemParts.length > 0) {
|
|
70
123
|
genConfig.systemInstruction = systemParts.join("\n");
|
|
71
124
|
}
|
|
72
|
-
|
|
73
|
-
|
|
125
|
+
const hostedEntries = googleWebSearchEntries(config.hostedTools);
|
|
126
|
+
if (tools.length > 0 || hostedEntries.length > 0) {
|
|
127
|
+
const toolGroups = [];
|
|
128
|
+
if (tools.length > 0) {
|
|
129
|
+
toolGroups.push({ functionDeclarations: tools });
|
|
130
|
+
}
|
|
131
|
+
for (const entry of hostedEntries) {
|
|
132
|
+
toolGroups.push(entry);
|
|
133
|
+
}
|
|
134
|
+
genConfig.tools = toolGroups;
|
|
74
135
|
}
|
|
75
136
|
if (config.responseFormat) {
|
|
76
137
|
genConfig.responseMimeType = "application/json";
|
|
@@ -232,15 +293,23 @@ export class SmolGoogle extends BaseClient {
|
|
|
232
293
|
const output = textContent || null;
|
|
233
294
|
// Extract usage and calculate cost
|
|
234
295
|
const { usage, cost } = this.calculateUsageAndCost(result.usageMetadata);
|
|
296
|
+
const parsed = parseGoogleHostedTools(result, "google", request.model);
|
|
297
|
+
const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, request.model, this.config.modelData);
|
|
235
298
|
// Return the response, updating the chat history
|
|
236
|
-
|
|
299
|
+
const promptResult = {
|
|
237
300
|
output,
|
|
238
301
|
toolCalls,
|
|
239
|
-
...(thinkingBlocks.length > 0 && { thinkingBlocks }),
|
|
240
302
|
usage,
|
|
241
|
-
cost,
|
|
303
|
+
cost: finalCost,
|
|
242
304
|
model: request.model,
|
|
243
|
-
}
|
|
305
|
+
};
|
|
306
|
+
if (thinkingBlocks.length > 0) {
|
|
307
|
+
promptResult.thinkingBlocks = thinkingBlocks;
|
|
308
|
+
}
|
|
309
|
+
if (hostedToolResults.length > 0) {
|
|
310
|
+
promptResult.hostedToolResults = hostedToolResults;
|
|
311
|
+
}
|
|
312
|
+
return success(promptResult);
|
|
244
313
|
}
|
|
245
314
|
async *_textStream(config) {
|
|
246
315
|
const signal = this.getAbortSignal(config);
|
package/dist/clients/ollama.js
CHANGED
|
@@ -16,7 +16,7 @@ export class SmolOllama extends BaseClient {
|
|
|
16
16
|
constructor(config) {
|
|
17
17
|
super(config);
|
|
18
18
|
this.logger = getLogger();
|
|
19
|
-
this.model = new Model(config.model);
|
|
19
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
20
20
|
if (config.ollamaApiKey) {
|
|
21
21
|
this.client = new Ollama({
|
|
22
22
|
host: "https://cloud.ollama.com",
|
package/dist/clients/openai.js
CHANGED
|
@@ -19,7 +19,7 @@ export class SmolOpenAi extends BaseClient {
|
|
|
19
19
|
}
|
|
20
20
|
this.client = new OpenAI({ apiKey: config.openAiApiKey });
|
|
21
21
|
this.logger = getLogger();
|
|
22
|
-
this.model = new Model(config.model);
|
|
22
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
23
23
|
}
|
|
24
24
|
getClient() {
|
|
25
25
|
return this.client;
|
|
@@ -2,7 +2,10 @@ import OpenAI from "openai";
|
|
|
2
2
|
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk } from "../types.js";
|
|
3
3
|
import { BaseClient } from "./baseClient.js";
|
|
4
4
|
import { ModelName } from "../models.js";
|
|
5
|
+
import { HostedToolResult } from "../types.js";
|
|
5
6
|
export type SmolOpenAiResponsesConfig = SmolConfig;
|
|
7
|
+
export declare function openaiResponsesWebSearchEntries(hostedTools?: string[]): any[];
|
|
8
|
+
export declare function parseOpenAIResponsesHostedTools(response: any, provider: string): HostedToolResult[];
|
|
6
9
|
export declare class SmolOpenAiResponses extends BaseClient implements SmolClient {
|
|
7
10
|
private client;
|
|
8
11
|
private logger;
|
|
@@ -5,9 +5,48 @@ import { getLogger } from "../util/logger.js";
|
|
|
5
5
|
import { BaseClient } from "./baseClient.js";
|
|
6
6
|
import { zodToOpenAIResponsesTool } from "../util/tool.js";
|
|
7
7
|
import { sanitizeAttributes } from "../util/util.js";
|
|
8
|
+
import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
|
|
8
9
|
import { Model } from "../model.js";
|
|
9
10
|
import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
10
11
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
12
|
+
export function openaiResponsesWebSearchEntries(hostedTools) {
|
|
13
|
+
if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
|
|
14
|
+
return [{ type: "web_search" }];
|
|
15
|
+
}
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
export function parseOpenAIResponsesHostedTools(response, provider) {
|
|
19
|
+
const queries = [];
|
|
20
|
+
const sources = [];
|
|
21
|
+
const citations = [];
|
|
22
|
+
const raw = [];
|
|
23
|
+
let callCount = 0;
|
|
24
|
+
for (const item of response.output || []) {
|
|
25
|
+
if (item.type === "web_search_call") {
|
|
26
|
+
raw.push(item);
|
|
27
|
+
callCount += 1;
|
|
28
|
+
// action.query is present for `search` actions but not always (e.g. open_page).
|
|
29
|
+
const query = item.action?.query;
|
|
30
|
+
if (typeof query === "string") {
|
|
31
|
+
queries.push(query);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
else if (item.type === "message") {
|
|
35
|
+
for (const part of item.content || []) {
|
|
36
|
+
for (const ann of part.annotations || []) {
|
|
37
|
+
if (ann.type === "url_citation" && typeof ann.url === "string") {
|
|
38
|
+
citations.push({ url: ann.url, title: ann.title, startIndex: ann.start_index, endIndex: ann.end_index });
|
|
39
|
+
sources.push({ url: ann.url, title: ann.title });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (callCount === 0 && citations.length === 0) {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return [webSearchResult(provider, { queries, sources, citations, callCount, raw })];
|
|
49
|
+
}
|
|
11
50
|
export class SmolOpenAiResponses extends BaseClient {
|
|
12
51
|
client;
|
|
13
52
|
logger;
|
|
@@ -19,7 +58,7 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
19
58
|
}
|
|
20
59
|
this.client = new OpenAI({ apiKey: config.openAiApiKey });
|
|
21
60
|
this.logger = getLogger();
|
|
22
|
-
this.model = new Model(config.model);
|
|
61
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
23
62
|
}
|
|
24
63
|
getClient() {
|
|
25
64
|
return this.client;
|
|
@@ -62,6 +101,11 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
62
101
|
description: tool.description,
|
|
63
102
|
}));
|
|
64
103
|
}
|
|
104
|
+
const hostedEntries = openaiResponsesWebSearchEntries(config.hostedTools);
|
|
105
|
+
if (hostedEntries.length > 0) {
|
|
106
|
+
const existing = Array.isArray(request.tools) ? request.tools : [];
|
|
107
|
+
request.tools = [...existing, ...hostedEntries];
|
|
108
|
+
}
|
|
65
109
|
if (config.temperature !== undefined) {
|
|
66
110
|
request.temperature = config.temperature;
|
|
67
111
|
}
|
|
@@ -144,13 +188,19 @@ export class SmolOpenAiResponses extends BaseClient {
|
|
|
144
188
|
}
|
|
145
189
|
}
|
|
146
190
|
const { usage, cost } = this.calculateUsageAndCost(response.usage);
|
|
147
|
-
|
|
191
|
+
const parsed = parseOpenAIResponsesHostedTools(response, "openai-responses");
|
|
192
|
+
const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, this.getModel(), this.config.modelData);
|
|
193
|
+
const result = {
|
|
148
194
|
output,
|
|
149
195
|
toolCalls,
|
|
150
196
|
usage,
|
|
151
|
-
cost,
|
|
197
|
+
cost: finalCost,
|
|
152
198
|
model: this.getModel(),
|
|
153
|
-
}
|
|
199
|
+
};
|
|
200
|
+
if (hostedToolResults.length > 0) {
|
|
201
|
+
result.hostedToolResults = hostedToolResults;
|
|
202
|
+
}
|
|
203
|
+
return success(result);
|
|
154
204
|
}
|
|
155
205
|
async *_textStream(config) {
|
|
156
206
|
const request = this.buildRequest(config);
|
package/dist/embed/openai.js
CHANGED
|
@@ -16,7 +16,7 @@ export async function openaiEmbed(inputs, config, apiKey) {
|
|
|
16
16
|
.sort((a, b) => a.index - b.index)
|
|
17
17
|
.map((d) => d.embedding);
|
|
18
18
|
const inputTokens = response.usage.prompt_tokens;
|
|
19
|
-
const costEstimate = calculateEmbeddingCost(config.model, inputTokens);
|
|
19
|
+
const costEstimate = calculateEmbeddingCost(config.model, inputTokens, config.modelData);
|
|
20
20
|
return success({
|
|
21
21
|
embeddings,
|
|
22
22
|
model: response.model,
|
|
@@ -28,8 +28,8 @@ export async function openaiEmbed(inputs, config, apiKey) {
|
|
|
28
28
|
return failure(err instanceof Error ? err.message : "OpenAI embedding request failed");
|
|
29
29
|
}
|
|
30
30
|
}
|
|
31
|
-
function calculateEmbeddingCost(modelName, inputTokens) {
|
|
32
|
-
const model = getModel(modelName);
|
|
31
|
+
function calculateEmbeddingCost(modelName, inputTokens, modelData) {
|
|
32
|
+
const model = getModel(modelName, modelData);
|
|
33
33
|
if (!model || !isEmbeddingsModel(model) || !model.tokenCost)
|
|
34
34
|
return undefined;
|
|
35
35
|
const inputCost = round((inputTokens * model.tokenCost) / 1_000_000, 6);
|
package/dist/embed.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ModelDataBlob } from "./modelData.js";
|
|
2
2
|
import { Result } from "./types/result.js";
|
|
3
3
|
import { TokenUsage } from "./types/tokenUsage.js";
|
|
4
4
|
import { CostEstimate } from "./types/costEstimate.js";
|
|
5
5
|
export type EmbedConfig = {
|
|
6
6
|
model: string;
|
|
7
|
-
provider?:
|
|
7
|
+
provider?: string;
|
|
8
8
|
dimensions?: number;
|
|
9
9
|
openAiApiKey?: string;
|
|
10
10
|
googleApiKey?: string;
|
|
11
11
|
ollamaApiKey?: string;
|
|
12
12
|
ollamaHost?: string;
|
|
13
13
|
metadata?: Record<string, unknown>;
|
|
14
|
+
modelData?: ModelDataBlob;
|
|
14
15
|
};
|
|
15
16
|
export type EmbedResult = {
|
|
16
17
|
embeddings: number[][];
|
|
@@ -18,4 +19,6 @@ export type EmbedResult = {
|
|
|
18
19
|
tokenUsage?: TokenUsage;
|
|
19
20
|
costEstimate?: CostEstimate;
|
|
20
21
|
};
|
|
22
|
+
export type EmbedProvider = (inputs: string[], config: EmbedConfig) => Promise<Result<EmbedResult>>;
|
|
23
|
+
export declare function registerEmbeddingProvider(name: string, fn: EmbedProvider): void;
|
|
21
24
|
export declare function embed(input: string | string[], config: EmbedConfig): Promise<Result<EmbedResult>>;
|