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.
Files changed (47) hide show
  1. package/README.md +139 -0
  2. package/dist/classes/ToolCall.js +4 -3
  3. package/dist/classes/message/AssistantMessage.d.ts +1 -0
  4. package/dist/classes/message/AssistantMessage.js +5 -3
  5. package/dist/classes/message/DeveloperMessage.js +5 -3
  6. package/dist/classes/message/SystemMessage.js +5 -3
  7. package/dist/classes/message/ToolMessage.js +4 -3
  8. package/dist/classes/message/UserMessage.js +5 -3
  9. package/dist/client.js +5 -3
  10. package/dist/clients/anthropic.d.ts +3 -1
  11. package/dist/clients/anthropic.js +71 -13
  12. package/dist/clients/baseClient.js +11 -1
  13. package/dist/clients/google.d.ts +4 -0
  14. package/dist/clients/google.js +92 -24
  15. package/dist/clients/ollama.d.ts +1 -0
  16. package/dist/clients/ollama.js +18 -7
  17. package/dist/clients/openai.js +7 -4
  18. package/dist/clients/openaiResponses.d.ts +3 -0
  19. package/dist/clients/openaiResponses.js +60 -7
  20. package/dist/embed/openai.js +3 -3
  21. package/dist/embed.d.ts +5 -2
  22. package/dist/embed.js +14 -3
  23. package/dist/image/google.js +3 -3
  24. package/dist/image/openai.js +6 -6
  25. package/dist/image.d.ts +5 -2
  26. package/dist/image.js +14 -3
  27. package/dist/index.d.ts +1 -0
  28. package/dist/index.js +1 -0
  29. package/dist/model.d.ts +4 -2
  30. package/dist/model.js +7 -5
  31. package/dist/modelData.d.ts +40 -0
  32. package/dist/modelData.js +222 -0
  33. package/dist/models.d.ts +658 -760
  34. package/dist/models.js +836 -60
  35. package/dist/smolError.d.ts +78 -5
  36. package/dist/smolError.js +134 -9
  37. package/dist/types/costEstimate.d.ts +2 -0
  38. package/dist/types/costEstimate.js +1 -0
  39. package/dist/types.d.ts +30 -1
  40. package/dist/types.js +2 -1
  41. package/dist/util/hostedTools.d.ts +19 -0
  42. package/dist/util/hostedTools.js +104 -0
  43. package/dist/util/httpError.d.ts +23 -0
  44. package/dist/util/httpError.js +237 -0
  45. package/dist/util/provider.d.ts +2 -1
  46. package/dist/util/provider.js +2 -2
  47. 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
 
@@ -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
- console.error("Failed to parse ToolCall");
51
- console.error(JSON.stringify(json, null, 2));
52
- console.error(z.prettifyError(result.error));
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
- console.error("Failed to parse AssistantMessage");
90
- console.error(JSON.stringify(json, null, 2));
91
- console.error(z.prettifyError(result.error));
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
- console.error("Failed to parse DeveloperMessage");
47
- console.error(JSON.stringify(json, null, 2));
48
- console.error(z.prettifyError(result.error));
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
- console.error("Failed to parse SystemMessage");
47
- console.error(JSON.stringify(json, null, 2));
48
- console.error(z.prettifyError(result.error));
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
- console.error("Failed to parse ToolMessage");
56
- console.error(JSON.stringify(json, null, 2));
57
- console.error(z.prettifyError(result.error));
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
- console.error("Failed to parse UserMessage");
46
- console.error(JSON.stringify(json, null, 2));
47
- console.error(z.prettifyError(result.error));
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
- const registeredProviders = {};
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 tools = config.tools && config.tools.length > 0
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
- : undefined;
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
- return success({
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 {
@@ -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>;