smoltalk 0.4.0 → 0.4.2

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.
@@ -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);
@@ -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, {
@@ -12,6 +12,13 @@ export declare class SmolAnthropic extends BaseClient implements SmolClient {
12
12
  getModel(): ModelName;
13
13
  private calculateUsageAndCost;
14
14
  private buildRequest;
15
+ /**
16
+ * Translate `thinking` / `reasoningEffort` into the correct Anthropic
17
+ * parameters for this model. Callers shouldn't have to know whether a model
18
+ * speaks the legacy `budget_tokens` API or the newer adaptive + effort API —
19
+ * that's exactly the vendor difference this package smooths over.
20
+ */
21
+ private resolveThinking;
15
22
  private rethrowAsSmolError;
16
23
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
17
24
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
@@ -4,10 +4,25 @@ import { SystemMessage, DeveloperMessage } from "../classes/message/index.js";
4
4
  import { getLogger } from "../util/logger.js";
5
5
  import { success, } from "../types.js";
6
6
  import { zodToAnthropicTool } from "../util/tool.js";
7
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
7
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
8
+ import { extractHttpErrorFields } from "../util/httpError.js";
8
9
  import { BaseClient } from "./baseClient.js";
10
+ import { getModel, isTextModel } from "../models.js";
9
11
  import { Model } from "../model.js";
10
12
  const DEFAULT_MAX_TOKENS = 4096;
13
+ /**
14
+ * Which thinking API a model speaks. New flagship Anthropic models (Opus 4.7+)
15
+ * reject the legacy `{type: "enabled", budget_tokens}` form with a 400, so we
16
+ * default unknown/unregistered models to "adaptive" (the forward-looking shape)
17
+ * rather than the form that's being phased out.
18
+ */
19
+ function thinkingStyleFor(modelName) {
20
+ const model = getModel(modelName);
21
+ if (model && isTextModel(model) && model.reasoning?.thinkingStyle) {
22
+ return model.reasoning.thinkingStyle;
23
+ }
24
+ return "adaptive";
25
+ }
11
26
  /**
12
27
  * Attach ephemeral cache_control breakpoints to (up to) three places:
13
28
  * 1. the last tool definition
@@ -152,49 +167,72 @@ export class SmolAnthropic extends BaseClient {
152
167
  description: tool.description,
153
168
  }))
154
169
  : undefined;
155
- const reasoningBudgetMap = {
156
- low: 2048,
157
- medium: 5000,
158
- high: 10000,
159
- };
160
- const thinking = config.thinking?.enabled
161
- ? {
162
- type: "enabled",
163
- budget_tokens: config.thinking.budgetTokens ?? 5000,
164
- }
165
- : config.reasoningEffort
166
- ? {
167
- type: "enabled",
168
- budget_tokens: reasoningBudgetMap[config.reasoningEffort],
169
- }
170
- : undefined;
170
+ // Normalize the user's provider-agnostic thinking/effort config into the
171
+ // shape this specific model accepts. Both `thinking.enabled` and
172
+ // `reasoningEffort` are treated as a request to think.
173
+ const { thinking, outputConfig } = this.resolveThinking(config);
171
174
  const cachingEnabled = config.caching?.enabled !== false;
172
175
  const baseRequest = { system, messages: anthropicMessages, tools };
173
176
  const finalRequest = cachingEnabled
174
177
  ? applyCacheBreakpoints(baseRequest)
175
178
  : baseRequest;
176
- return { ...finalRequest, thinking };
179
+ return { ...finalRequest, thinking, outputConfig };
180
+ }
181
+ /**
182
+ * Translate `thinking` / `reasoningEffort` into the correct Anthropic
183
+ * parameters for this model. Callers shouldn't have to know whether a model
184
+ * speaks the legacy `budget_tokens` API or the newer adaptive + effort API —
185
+ * that's exactly the vendor difference this package smooths over.
186
+ */
187
+ resolveThinking(config) {
188
+ const wantsThinking = config.thinking?.enabled === true;
189
+ const effort = config.reasoningEffort;
190
+ if (!wantsThinking && !effort) {
191
+ return { thinking: undefined, outputConfig: undefined };
192
+ }
193
+ if (thinkingStyleFor(this.getModel()) === "adaptive") {
194
+ // `output_config.effort` controls depth; the budget is irrelevant here.
195
+ return {
196
+ thinking: { type: "adaptive" },
197
+ outputConfig: effort ? { effort } : undefined,
198
+ };
199
+ }
200
+ // Legacy budget-based thinking (Haiku 4.5 and older models).
201
+ const reasoningBudgetMap = {
202
+ low: 2048,
203
+ medium: 5000,
204
+ high: 10000,
205
+ };
206
+ const budget_tokens = wantsThinking
207
+ ? (config.thinking?.budgetTokens ?? 5000)
208
+ : reasoningBudgetMap[effort];
209
+ return {
210
+ thinking: { type: "enabled", budget_tokens },
211
+ outputConfig: undefined,
212
+ };
177
213
  }
178
214
  rethrowAsSmolError(error) {
179
215
  if (error instanceof Anthropic.APIError) {
216
+ const http = { ...extractHttpErrorFields(error), cause: error };
180
217
  const msg = error.message.toLowerCase();
181
218
  if (msg.includes("prompt is too long") ||
182
219
  msg.includes("context length") ||
183
220
  msg.includes("context window") ||
184
221
  msg.includes("too many tokens")) {
185
- throw new SmolContextWindowExceededError(error.message);
222
+ throw new SmolContextWindowExceededError(error.message, http);
186
223
  }
187
224
  if (msg.includes("content policy") ||
188
225
  msg.includes("usage policies") ||
189
226
  msg.includes("content filtering") ||
190
227
  msg.includes("violates our")) {
191
- throw new SmolContentPolicyError(error.message);
228
+ throw new SmolContentPolicyError(error.message, http);
192
229
  }
230
+ throw smolErrorForStatus(error.message, http);
193
231
  }
194
232
  throw error;
195
233
  }
196
234
  async _textSync(config) {
197
- const { system, messages, tools, thinking } = this.buildRequest(config);
235
+ const { system, messages, tools, thinking, outputConfig } = this.buildRequest(config);
198
236
  let debugData = {
199
237
  model: this.getModel(),
200
238
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -202,6 +240,7 @@ export class SmolAnthropic extends BaseClient {
202
240
  system,
203
241
  tools,
204
242
  thinking,
243
+ output_config: outputConfig,
205
244
  };
206
245
  this.logger.debug("Sending request to Anthropic:", debugData);
207
246
  this.statelogClient?.promptRequest(debugData);
@@ -215,6 +254,7 @@ export class SmolAnthropic extends BaseClient {
215
254
  ...(system && { system }),
216
255
  ...(tools && { tools }),
217
256
  ...(thinking && { thinking }),
257
+ ...(outputConfig && { output_config: outputConfig }),
218
258
  ...(config.temperature !== undefined && {
219
259
  temperature: config.temperature,
220
260
  }),
@@ -253,7 +293,7 @@ export class SmolAnthropic extends BaseClient {
253
293
  });
254
294
  }
255
295
  async *_textStream(config) {
256
- const { system, messages, tools, thinking } = this.buildRequest(config);
296
+ const { system, messages, tools, thinking, outputConfig } = this.buildRequest(config);
257
297
  const streamDebugData = {
258
298
  model: this.model,
259
299
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
@@ -261,6 +301,7 @@ export class SmolAnthropic extends BaseClient {
261
301
  system,
262
302
  tools,
263
303
  thinking,
304
+ output_config: outputConfig,
264
305
  };
265
306
  this.logger.debug("Sending streaming request to Anthropic:", streamDebugData);
266
307
  this.statelogClient?.promptRequest(streamDebugData);
@@ -274,6 +315,7 @@ export class SmolAnthropic extends BaseClient {
274
315
  ...(system && { system }),
275
316
  ...(tools && { tools }),
276
317
  ...(thinking && { thinking }),
318
+ ...(outputConfig && { output_config: outputConfig }),
277
319
  ...(config.temperature !== undefined && {
278
320
  temperature: config.temperature,
279
321
  }),
@@ -17,6 +17,7 @@ export declare class SmolGoogle extends BaseClient implements SmolClient {
17
17
  getModel(): ModelName;
18
18
  private calculateUsageAndCost;
19
19
  private buildRequest;
20
+ private rethrowAsSmolError;
20
21
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
21
22
  __textSync(request: GeneratedRequest): Promise<Result<PromptResult>>;
22
23
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
@@ -3,7 +3,8 @@ import { ToolCall } from "../classes/ToolCall.js";
3
3
  import { getLogger } from "../util/logger.js";
4
4
  import { addCosts, addTokenUsage, success, } from "../types.js";
5
5
  import { zodToGoogleTool } from "../util/tool.js";
6
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
6
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
7
+ import { extractHttpErrorFields } from "../util/httpError.js";
7
8
  import { sanitizeAttributes } from "../util/util.js";
8
9
  import { BaseClient } from "./baseClient.js";
9
10
  import { Model } from "../model.js";
@@ -88,6 +89,18 @@ export class SmolGoogle extends BaseClient {
88
89
  ...sanitizeAttributes(config.rawAttributes),
89
90
  };
90
91
  }
92
+ rethrowAsSmolError(error) {
93
+ const http = { ...extractHttpErrorFields(error), cause: error };
94
+ const msg = (error.message || "").toLowerCase();
95
+ if (msg.includes("token") &&
96
+ (msg.includes("exceed") || msg.includes("too long") || msg.includes("limit"))) {
97
+ throw new SmolContextWindowExceededError(error.message, http);
98
+ }
99
+ if (http.status !== undefined) {
100
+ throw smolErrorForStatus(error.message, http);
101
+ }
102
+ throw error;
103
+ }
91
104
  async _textSync(config) {
92
105
  const signal = this.getAbortSignal(config);
93
106
  const request = {
@@ -180,14 +193,7 @@ export class SmolGoogle extends BaseClient {
180
193
  result = await this.client.models.generateContent(request);
181
194
  }
182
195
  catch (error) {
183
- const msg = (error.message || "").toLowerCase();
184
- if (msg.includes("token") &&
185
- (msg.includes("exceed") ||
186
- msg.includes("too long") ||
187
- msg.includes("limit"))) {
188
- throw new SmolContextWindowExceededError(error.message);
189
- }
190
- throw error;
196
+ this.rethrowAsSmolError(error);
191
197
  }
192
198
  this.logger.debug("Response from Google Gemini:", JSON.stringify(result, null, 2));
193
199
  this.statelogClient?.promptResponse(result);
@@ -257,14 +263,7 @@ export class SmolGoogle extends BaseClient {
257
263
  stream = await this.client.models.generateContentStream(request);
258
264
  }
259
265
  catch (error) {
260
- const msg = (error.message || "").toLowerCase();
261
- if (msg.includes("token") &&
262
- (msg.includes("exceed") ||
263
- msg.includes("too long") ||
264
- msg.includes("limit"))) {
265
- throw new SmolContextWindowExceededError(error.message);
266
- }
267
- throw error;
266
+ this.rethrowAsSmolError(error);
268
267
  }
269
268
  let content = "";
270
269
  const toolCallsMap = new Map();
@@ -12,6 +12,7 @@ export declare class SmolOllama extends BaseClient implements SmolClient {
12
12
  getClient(): Ollama;
13
13
  getModel(): ModelName;
14
14
  private calculateUsageAndCost;
15
+ private rethrowAsSmolError;
15
16
  _textSync(config: SmolConfig): Promise<Result<PromptResult>>;
16
17
  _textStream(config: SmolConfig): AsyncGenerator<StreamChunk>;
17
18
  }
@@ -5,7 +5,8 @@ import { success, } from "../types.js";
5
5
  import { zodToGoogleTool } from "../util/tool.js";
6
6
  import { sanitizeAttributes } from "../util/util.js";
7
7
  import { BaseClient } from "./baseClient.js";
8
- import { SmolContextWindowExceededError } from "../smolError.js";
8
+ import { SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
9
+ import { extractHttpErrorFields } from "../util/httpError.js";
9
10
  import { Model } from "../model.js";
10
11
  export const DEFAULT_OLLAMA_HOST = "http://localhost:11434";
11
12
  export class SmolOllama extends BaseClient {
@@ -51,6 +52,17 @@ export class SmolOllama extends BaseClient {
51
52
  }
52
53
  return { usage, cost };
53
54
  }
55
+ rethrowAsSmolError(error) {
56
+ const http = { ...extractHttpErrorFields(error), cause: error };
57
+ const msg = (error.message || "").toLowerCase();
58
+ if (msg.includes("context length") || msg.includes("context window")) {
59
+ throw new SmolContextWindowExceededError(error.message, http);
60
+ }
61
+ if (http.status !== undefined) {
62
+ throw smolErrorForStatus(error.message, http);
63
+ }
64
+ throw error;
65
+ }
54
66
  async _textSync(config) {
55
67
  const messages = config.messages.map((msg) => msg.toOpenAIMessage());
56
68
  const tools = (config.tools || []).map((tool) => {
@@ -82,11 +94,7 @@ export class SmolOllama extends BaseClient {
82
94
  result = await this.client.chat(request);
83
95
  }
84
96
  catch (error) {
85
- const msg = (error.message || "").toLowerCase();
86
- if (msg.includes("context length") || msg.includes("context window")) {
87
- throw new SmolContextWindowExceededError(error.message);
88
- }
89
- throw error;
97
+ this.rethrowAsSmolError(error);
90
98
  }
91
99
  finally {
92
100
  if (signal && abortHandler) {
@@ -201,6 +209,9 @@ export class SmolOllama extends BaseClient {
201
209
  },
202
210
  };
203
211
  }
212
+ catch (error) {
213
+ this.rethrowAsSmolError(error);
214
+ }
204
215
  finally {
205
216
  if (signal && abortHandler) {
206
217
  signal.removeEventListener("abort", abortHandler);
@@ -4,7 +4,8 @@ import { ToolCall } from "../classes/ToolCall.js";
4
4
  import { isFunctionToolCall, sanitizeAttributes } from "../util/util.js";
5
5
  import { getLogger } from "../util/logger.js";
6
6
  import { BaseClient } from "./baseClient.js";
7
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
7
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
8
+ import { extractHttpErrorFields } from "../util/httpError.js";
8
9
  import { zodToOpenAITool } from "../util/tool.js";
9
10
  import { Model } from "../model.js";
10
11
  export class SmolOpenAi extends BaseClient {
@@ -74,12 +75,14 @@ export class SmolOpenAi extends BaseClient {
74
75
  }
75
76
  rethrowAsSmolError(error) {
76
77
  if (error instanceof OpenAI.APIError) {
78
+ const http = { ...extractHttpErrorFields(error), cause: error };
77
79
  if (error.code === "context_length_exceeded") {
78
- throw new SmolContextWindowExceededError(error.message);
80
+ throw new SmolContextWindowExceededError(error.message, http);
79
81
  }
80
82
  if (error.code === "content_policy_violation") {
81
- throw new SmolContentPolicyError(error.message);
83
+ throw new SmolContentPolicyError(error.message, http);
82
84
  }
85
+ throw smolErrorForStatus(error.message, http);
83
86
  }
84
87
  throw error;
85
88
  }
@@ -6,7 +6,8 @@ import { BaseClient } from "./baseClient.js";
6
6
  import { zodToOpenAIResponsesTool } from "../util/tool.js";
7
7
  import { sanitizeAttributes } from "../util/util.js";
8
8
  import { Model } from "../model.js";
9
- import { SmolContentPolicyError, SmolContextWindowExceededError, } from "../smolError.js";
9
+ import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
10
+ import { extractHttpErrorFields } from "../util/httpError.js";
10
11
  export class SmolOpenAiResponses extends BaseClient {
11
12
  client;
12
13
  logger;
@@ -107,12 +108,14 @@ export class SmolOpenAiResponses extends BaseClient {
107
108
  }
108
109
  rethrowAsSmolError(error) {
109
110
  if (error instanceof OpenAI.APIError) {
111
+ const http = { ...extractHttpErrorFields(error), cause: error };
110
112
  if (error.code === "context_length_exceeded") {
111
- throw new SmolContextWindowExceededError(error.message);
113
+ throw new SmolContextWindowExceededError(error.message, http);
112
114
  }
113
115
  if (error.code === "content_policy_violation") {
114
- throw new SmolContentPolicyError(error.message);
116
+ throw new SmolContentPolicyError(error.message, http);
115
117
  }
118
+ throw smolErrorForStatus(error.message, http);
116
119
  }
117
120
  throw error;
118
121
  }