smoltalk 0.4.1 → 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/ToolCall.js +4 -3
- package/dist/classes/message/AssistantMessage.d.ts +1 -0
- package/dist/classes/message/AssistantMessage.js +5 -3
- package/dist/classes/message/DeveloperMessage.js +5 -3
- package/dist/classes/message/SystemMessage.js +5 -3
- package/dist/classes/message/ToolMessage.js +4 -3
- package/dist/classes/message/UserMessage.js +5 -3
- package/dist/client.js +5 -3
- package/dist/clients/anthropic.d.ts +3 -1
- package/dist/clients/anthropic.js +71 -13
- package/dist/clients/baseClient.js +11 -1
- package/dist/clients/google.d.ts +4 -0
- package/dist/clients/google.js +92 -24
- package/dist/clients/ollama.d.ts +1 -0
- package/dist/clients/ollama.js +18 -7
- package/dist/clients/openai.js +7 -4
- package/dist/clients/openaiResponses.d.ts +3 -0
- package/dist/clients/openaiResponses.js +60 -7
- 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/smolError.d.ts +78 -5
- package/dist/smolError.js +134 -9
- 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/httpError.d.ts +23 -0
- package/dist/util/httpError.js +237 -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
|
|
package/dist/classes/ToolCall.js
CHANGED
|
@@ -47,9 +47,10 @@ export class ToolCall {
|
|
|
47
47
|
static fromJSON(json) {
|
|
48
48
|
const result = ToolCallJSONSchema.safeParse(json);
|
|
49
49
|
if (!result.success) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
50
|
+
const logger = getLogger();
|
|
51
|
+
logger.error("Failed to parse ToolCall:", z.prettifyError(result.error));
|
|
52
|
+
// Raw payload can contain tool-call arguments (often secrets) — only at debug level.
|
|
53
|
+
logger.debug("ToolCall payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
53
54
|
throw new Error("Failed to parse ToolCall");
|
|
54
55
|
}
|
|
55
56
|
return new ToolCall(result.data.id, result.data.name, result.data.arguments);
|
|
@@ -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>>;
|
|
@@ -2,6 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { BaseMessage } from "./BaseMessage.js";
|
|
3
3
|
import { CostEstimateSchema, TextPartSchema, ThinkingBlockSchema, TokenUsageSchema, } from "../../types.js";
|
|
4
4
|
import { ToolCall, ToolCallJSONSchema } from "../ToolCall.js";
|
|
5
|
+
import { getLogger } from "../../util/logger.js";
|
|
5
6
|
export const AssistantMessageJSONSchema = z.object({
|
|
6
7
|
role: z.literal("assistant"),
|
|
7
8
|
content: z.union([z.string(), z.array(TextPartSchema), z.null()]),
|
|
@@ -86,9 +87,10 @@ export class AssistantMessage extends BaseMessage {
|
|
|
86
87
|
static fromJSON(json) {
|
|
87
88
|
const result = AssistantMessageJSONSchema.safeParse(json);
|
|
88
89
|
if (!result.success) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
90
|
+
const logger = getLogger();
|
|
91
|
+
logger.error("Failed to parse AssistantMessage:", z.prettifyError(result.error));
|
|
92
|
+
// Raw payload can contain assistant content / tool args — only at debug level.
|
|
93
|
+
logger.debug("AssistantMessage payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
92
94
|
throw new Error("Failed to parse AssistantMessage");
|
|
93
95
|
}
|
|
94
96
|
return new AssistantMessage(result.data.content, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BaseMessage } from "./BaseMessage.js";
|
|
3
3
|
import { TextPartSchema } from "../../types.js";
|
|
4
|
+
import { getLogger } from "../../util/logger.js";
|
|
4
5
|
export const DeveloperMessageJSONSchema = z.object({
|
|
5
6
|
role: z.literal("developer"),
|
|
6
7
|
content: z.union([z.string(), z.array(TextPartSchema)]),
|
|
@@ -43,9 +44,10 @@ export class DeveloperMessage extends BaseMessage {
|
|
|
43
44
|
static fromJSON(json) {
|
|
44
45
|
const result = DeveloperMessageJSONSchema.safeParse(json);
|
|
45
46
|
if (!result.success) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const logger = getLogger();
|
|
48
|
+
logger.error("Failed to parse DeveloperMessage:", z.prettifyError(result.error));
|
|
49
|
+
// Raw payload can contain developer-message content — only at debug level.
|
|
50
|
+
logger.debug("DeveloperMessage payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
49
51
|
throw new Error("Failed to parse DeveloperMessage");
|
|
50
52
|
}
|
|
51
53
|
return new DeveloperMessage(result.data.content, {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BaseMessage } from "./BaseMessage.js";
|
|
3
3
|
import { TextPartSchema } from "../../types.js";
|
|
4
|
+
import { getLogger } from "../../util/logger.js";
|
|
4
5
|
export const SystemMessageJSONSchema = z.object({
|
|
5
6
|
role: z.literal("system"),
|
|
6
7
|
content: z.union([z.string(), z.array(TextPartSchema)]),
|
|
@@ -43,9 +44,10 @@ export class SystemMessage extends BaseMessage {
|
|
|
43
44
|
static fromJSON(json) {
|
|
44
45
|
const result = SystemMessageJSONSchema.safeParse(json);
|
|
45
46
|
if (!result.success) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
47
|
+
const logger = getLogger();
|
|
48
|
+
logger.error("Failed to parse SystemMessage:", z.prettifyError(result.error));
|
|
49
|
+
// Raw payload can contain system prompt content — only at debug level.
|
|
50
|
+
logger.debug("SystemMessage payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
49
51
|
throw new Error("Failed to parse SystemMessage");
|
|
50
52
|
}
|
|
51
53
|
return new SystemMessage(result.data.content, {
|
|
@@ -52,9 +52,10 @@ export class ToolMessage extends BaseMessage {
|
|
|
52
52
|
static fromJSON(json) {
|
|
53
53
|
const result = ToolMessageJSONSchema.safeParse(json);
|
|
54
54
|
if (!result.success) {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
55
|
+
const logger = getLogger();
|
|
56
|
+
logger.error("Failed to parse ToolMessage:", z.prettifyError(result.error));
|
|
57
|
+
// Raw payload can contain tool results (file paths, secrets) — only at debug level.
|
|
58
|
+
logger.debug("ToolMessage payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
58
59
|
throw new Error("Failed to parse ToolMessage");
|
|
59
60
|
}
|
|
60
61
|
const TextPartArraySchema = z.array(TextPartSchema);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { BaseMessage } from "./BaseMessage.js";
|
|
3
|
+
import { getLogger } from "../../util/logger.js";
|
|
3
4
|
export const UserMessageJSONSchema = z.object({
|
|
4
5
|
role: z.literal("user"),
|
|
5
6
|
content: z.string(),
|
|
@@ -42,9 +43,10 @@ export class UserMessage extends BaseMessage {
|
|
|
42
43
|
static fromJSON(json) {
|
|
43
44
|
const result = UserMessageJSONSchema.safeParse(json);
|
|
44
45
|
if (!result.success) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const logger = getLogger();
|
|
47
|
+
logger.error("Failed to parse UserMessage:", z.prettifyError(result.error));
|
|
48
|
+
// Raw payload can contain user prompts / PII — only at debug level.
|
|
49
|
+
logger.debug("UserMessage payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
48
50
|
throw new Error("Failed to parse UserMessage");
|
|
49
51
|
}
|
|
50
52
|
return new UserMessage(result.data.content, {
|
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,20 +3,65 @@ 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
|
-
import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
|
|
8
|
+
import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
9
|
+
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
8
10
|
import { BaseClient } from "./baseClient.js";
|
|
9
11
|
import { getModel, isTextModel } from "../models.js";
|
|
10
12
|
import { Model } from "../model.js";
|
|
11
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
|
+
}
|
|
12
57
|
/**
|
|
13
58
|
* Which thinking API a model speaks. New flagship Anthropic models (Opus 4.7+)
|
|
14
59
|
* reject the legacy `{type: "enabled", budget_tokens}` form with a 400, so we
|
|
15
60
|
* default unknown/unregistered models to "adaptive" (the forward-looking shape)
|
|
16
61
|
* rather than the form that's being phased out.
|
|
17
62
|
*/
|
|
18
|
-
function thinkingStyleFor(modelName) {
|
|
19
|
-
const model = getModel(modelName);
|
|
63
|
+
function thinkingStyleFor(modelName, modelData) {
|
|
64
|
+
const model = getModel(modelName, modelData);
|
|
20
65
|
if (model && isTextModel(model) && model.reasoning?.thinkingStyle) {
|
|
21
66
|
return model.reasoning.thinkingStyle;
|
|
22
67
|
}
|
|
@@ -106,7 +151,7 @@ export class SmolAnthropic extends BaseClient {
|
|
|
106
151
|
super(config);
|
|
107
152
|
this.client = new Anthropic({ apiKey: config.anthropicApiKey });
|
|
108
153
|
this.logger = getLogger();
|
|
109
|
-
this.model = new Model(config.model);
|
|
154
|
+
this.model = new Model(config.model, undefined, config.modelData);
|
|
110
155
|
}
|
|
111
156
|
getModel() {
|
|
112
157
|
return this.model.getModel();
|
|
@@ -161,11 +206,14 @@ export class SmolAnthropic extends BaseClient {
|
|
|
161
206
|
}
|
|
162
207
|
anthropicMessages.push(converted);
|
|
163
208
|
}
|
|
164
|
-
const
|
|
209
|
+
const functionTools = config.tools && config.tools.length > 0
|
|
165
210
|
? config.tools.map((tool) => zodToAnthropicTool(tool.name, tool.schema, {
|
|
166
211
|
description: tool.description,
|
|
167
212
|
}))
|
|
168
|
-
:
|
|
213
|
+
: [];
|
|
214
|
+
const hostedEntries = anthropicWebSearchEntries(config.hostedTools);
|
|
215
|
+
const allTools = [...functionTools, ...hostedEntries];
|
|
216
|
+
const tools = allTools.length > 0 ? allTools : undefined;
|
|
169
217
|
// Normalize the user's provider-agnostic thinking/effort config into the
|
|
170
218
|
// shape this specific model accepts. Both `thinking.enabled` and
|
|
171
219
|
// `reasoningEffort` are treated as a request to think.
|
|
@@ -189,7 +237,7 @@ export class SmolAnthropic extends BaseClient {
|
|
|
189
237
|
if (!wantsThinking && !effort) {
|
|
190
238
|
return { thinking: undefined, outputConfig: undefined };
|
|
191
239
|
}
|
|
192
|
-
if (thinkingStyleFor(this.getModel()) === "adaptive") {
|
|
240
|
+
if (thinkingStyleFor(this.getModel(), this.config.modelData) === "adaptive") {
|
|
193
241
|
// `output_config.effort` controls depth; the budget is irrelevant here.
|
|
194
242
|
return {
|
|
195
243
|
thinking: { type: "adaptive" },
|
|
@@ -212,19 +260,21 @@ export class SmolAnthropic extends BaseClient {
|
|
|
212
260
|
}
|
|
213
261
|
rethrowAsSmolError(error) {
|
|
214
262
|
if (error instanceof Anthropic.APIError) {
|
|
263
|
+
const http = { ...extractHttpErrorFields(error), cause: error };
|
|
215
264
|
const msg = error.message.toLowerCase();
|
|
216
265
|
if (msg.includes("prompt is too long") ||
|
|
217
266
|
msg.includes("context length") ||
|
|
218
267
|
msg.includes("context window") ||
|
|
219
268
|
msg.includes("too many tokens")) {
|
|
220
|
-
throw new SmolContextWindowExceededError(error.message);
|
|
269
|
+
throw new SmolContextWindowExceededError(error.message, http);
|
|
221
270
|
}
|
|
222
271
|
if (msg.includes("content policy") ||
|
|
223
272
|
msg.includes("usage policies") ||
|
|
224
273
|
msg.includes("content filtering") ||
|
|
225
274
|
msg.includes("violates our")) {
|
|
226
|
-
throw new SmolContentPolicyError(error.message);
|
|
275
|
+
throw new SmolContentPolicyError(error.message, http);
|
|
227
276
|
}
|
|
277
|
+
throw smolErrorForStatus(error.message, http);
|
|
228
278
|
}
|
|
229
279
|
throw error;
|
|
230
280
|
}
|
|
@@ -280,14 +330,22 @@ export class SmolAnthropic extends BaseClient {
|
|
|
280
330
|
}
|
|
281
331
|
}
|
|
282
332
|
const { usage, cost } = this.calculateUsageAndCost(response.usage);
|
|
283
|
-
|
|
333
|
+
const parsed = parseAnthropicHostedTools(response, "anthropic");
|
|
334
|
+
const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, this.getModel(), this.config.modelData);
|
|
335
|
+
const result = {
|
|
284
336
|
output,
|
|
285
337
|
toolCalls,
|
|
286
|
-
...(thinkingBlocks.length > 0 && { thinkingBlocks }),
|
|
287
338
|
usage,
|
|
288
|
-
cost,
|
|
339
|
+
cost: finalCost,
|
|
289
340
|
model: this.getModel(),
|
|
290
|
-
}
|
|
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);
|
|
291
349
|
}
|
|
292
350
|
async *_textStream(config) {
|
|
293
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;
|
|
@@ -17,6 +20,7 @@ export declare class SmolGoogle extends BaseClient implements SmolClient {
|
|
|
17
20
|
getModel(): ModelName;
|
|
18
21
|
private calculateUsageAndCost;
|
|
19
22
|
private buildRequest;
|
|
23
|
+
private rethrowAsSmolError;
|
|
20
24
|
_textSync(config: SmolConfig): Promise<Result<PromptResult>>;
|
|
21
25
|
__textSync(request: GeneratedRequest): Promise<Result<PromptResult>>;
|
|
22
26
|
_textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
|