smoltalk 0.13.0 → 0.13.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.
- package/dist/clients/anthropic.d.ts +11 -0
- package/dist/clients/anthropic.js +25 -1
- package/dist/clients/google.js +4 -3
- package/dist/smolError.d.ts +3 -1
- package/dist/smolError.js +3 -1
- package/dist/util/jsonSchema.d.ts +14 -0
- package/dist/util/jsonSchema.js +83 -0
- package/package.json +1 -1
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import Anthropic from "@anthropic-ai/sdk";
|
|
1
2
|
import type { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages.js";
|
|
2
3
|
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
|
|
3
4
|
import { BaseClient } from "./baseClient.js";
|
|
@@ -13,6 +14,16 @@ export declare function mergeConsecutiveMessages(messages: MessageParam[]): Mess
|
|
|
13
14
|
* supported (forward-looking).
|
|
14
15
|
*/
|
|
15
16
|
export declare function anthropicSupportsStructuredOutput(model: string): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* A 400 whose message is only the generic "Invalid request data", with no
|
|
19
|
+
* field named, is not a malformed request. Anthropic returns it seconds into
|
|
20
|
+
* generation, intermittently, for a request that succeeds unchanged on the
|
|
21
|
+
* next try (seen with `output_config.format` plus tools on Opus 4.8). A real
|
|
22
|
+
* validation failure always names what it rejected, e.g.
|
|
23
|
+
* `messages.1.content: ...`. Treat the bare form as transient so it is retried
|
|
24
|
+
* like a 5xx.
|
|
25
|
+
*/
|
|
26
|
+
export declare function isBareInvalidRequest(error: InstanceType<typeof Anthropic.APIError>): boolean;
|
|
16
27
|
export declare function anthropicWebSearchEntries(hostedTools?: string[]): any[];
|
|
17
28
|
export declare function parseAnthropicHostedTools(response: any, provider: string): HostedToolResult[];
|
|
18
29
|
type EphemeralCacheControl = {
|
|
@@ -8,7 +8,7 @@ import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hosted
|
|
|
8
8
|
import { zodToAnthropicTool } from "../util/tool.js";
|
|
9
9
|
import { responseFormatToJsonSchema } from "../util/jsonSchema.js";
|
|
10
10
|
import { normalizeAnthropicStopReason } from "../util/stopReason.js";
|
|
11
|
-
import { SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
11
|
+
import { SmolContentPolicyError, SmolContextWindowExceededError, SmolOverloadedError, smolErrorForStatus, } from "../smolError.js";
|
|
12
12
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
13
13
|
import { BaseClient } from "./baseClient.js";
|
|
14
14
|
import { getModel, isTextModel } from "../models.js";
|
|
@@ -60,6 +60,27 @@ export function mergeConsecutiveMessages(messages) {
|
|
|
60
60
|
export function anthropicSupportsStructuredOutput(model) {
|
|
61
61
|
return !/^claude-(?:3(?:[-.]|$)|2(?:[-.]|$)|instant(?:[-.]|$))/i.test(model);
|
|
62
62
|
}
|
|
63
|
+
/**
|
|
64
|
+
* A 400 whose message is only the generic "Invalid request data", with no
|
|
65
|
+
* field named, is not a malformed request. Anthropic returns it seconds into
|
|
66
|
+
* generation, intermittently, for a request that succeeds unchanged on the
|
|
67
|
+
* next try (seen with `output_config.format` plus tools on Opus 4.8). A real
|
|
68
|
+
* validation failure always names what it rejected, e.g.
|
|
69
|
+
* `messages.1.content: ...`. Treat the bare form as transient so it is retried
|
|
70
|
+
* like a 5xx.
|
|
71
|
+
*/
|
|
72
|
+
export function isBareInvalidRequest(error) {
|
|
73
|
+
if (error.status !== 400) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
const body = error.error;
|
|
77
|
+
const bodyMessage = body?.error?.message;
|
|
78
|
+
if (typeof bodyMessage === "string") {
|
|
79
|
+
return bodyMessage.trim() === BARE_INVALID_REQUEST;
|
|
80
|
+
}
|
|
81
|
+
return error.message.trim() === BARE_INVALID_REQUEST;
|
|
82
|
+
}
|
|
83
|
+
const BARE_INVALID_REQUEST = "Invalid request data";
|
|
63
84
|
export function anthropicWebSearchEntries(hostedTools) {
|
|
64
85
|
if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
|
|
65
86
|
return [{ type: "web_search_20250305", name: "web_search" }];
|
|
@@ -333,6 +354,9 @@ export class SmolAnthropic extends BaseClient {
|
|
|
333
354
|
msg.includes("violates our")) {
|
|
334
355
|
throw new SmolContentPolicyError(error.message, http);
|
|
335
356
|
}
|
|
357
|
+
if (isBareInvalidRequest(error)) {
|
|
358
|
+
throw new SmolOverloadedError(error.message, http);
|
|
359
|
+
}
|
|
336
360
|
throw smolErrorForStatus(error.message, http);
|
|
337
361
|
}
|
|
338
362
|
throw error;
|
package/dist/clients/google.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getLogger } from "../util/logger.js";
|
|
|
4
4
|
import { redactAttachments } from "../util/redact.js";
|
|
5
5
|
import { addCosts, addTokenUsage, success, } from "../types.js";
|
|
6
6
|
import { zodToGoogleTool } from "../util/tool.js";
|
|
7
|
-
import { responseFormatToJsonSchema } from "../util/jsonSchema.js";
|
|
7
|
+
import { responseFormatToJsonSchema, constToEnum } from "../util/jsonSchema.js";
|
|
8
8
|
import { normalizeGoogleStopReason } from "../util/stopReason.js";
|
|
9
9
|
import { SmolError, SmolContentPolicyError, SmolContextWindowExceededError, smolErrorForStatus, } from "../smolError.js";
|
|
10
10
|
import { extractHttpErrorFields } from "../util/httpError.js";
|
|
@@ -297,7 +297,8 @@ export class SmolGoogle extends BaseClient {
|
|
|
297
297
|
}
|
|
298
298
|
if (config.responseFormat) {
|
|
299
299
|
genConfig.responseMimeType = "application/json";
|
|
300
|
-
|
|
300
|
+
// Gemini ignores `const` but honours `enum`; see constToEnum.
|
|
301
|
+
genConfig.responseJsonSchema = constToEnum(responseFormatToJsonSchema(config.responseFormat));
|
|
301
302
|
}
|
|
302
303
|
if (config.thinking?.enabled) {
|
|
303
304
|
// Gemini only returns thought-summary parts (parts with `thought: true`,
|
|
@@ -520,7 +521,7 @@ export class SmolGoogle extends BaseClient {
|
|
|
520
521
|
const hasTools = config.tools && config.tools.length > 0;
|
|
521
522
|
const hasStructuredResponse = !!config.responseFormat;
|
|
522
523
|
if (hasTools && hasStructuredResponse) {
|
|
523
|
-
this.logger.
|
|
524
|
+
this.logger.warn("Gemini does not support streaming responses with both tool calls and structured response formats. Response format will be ignored.");
|
|
524
525
|
this.statelogClient?.debug("Google Gemini: streaming with tools + structured response not supported, ignoring response format", {});
|
|
525
526
|
request.config.responseMimeType = undefined;
|
|
526
527
|
request.config.responseJsonSchema = undefined;
|
package/dist/smolError.d.ts
CHANGED
|
@@ -72,7 +72,9 @@ export declare class SmolContextWindowExceededError extends SmolError {
|
|
|
72
72
|
export declare class SmolRateLimitError extends SmolError {
|
|
73
73
|
constructor(message: string, options?: SmolErrorOptions);
|
|
74
74
|
}
|
|
75
|
-
/** Provider temporarily overloaded/unavailable (HTTP 503, or Anthropic's 529).
|
|
75
|
+
/** Provider temporarily overloaded/unavailable (HTTP 503, or Anthropic's 529).
|
|
76
|
+
* Also raised for Anthropic's bare "Invalid request data" 400, which is a
|
|
77
|
+
* transient generation failure rather than a malformed request. */
|
|
76
78
|
export declare class SmolOverloadedError extends SmolError {
|
|
77
79
|
constructor(message: string, options?: SmolErrorOptions);
|
|
78
80
|
}
|
package/dist/smolError.js
CHANGED
|
@@ -121,7 +121,9 @@ export class SmolRateLimitError extends SmolError {
|
|
|
121
121
|
this.name = "SmolRateLimitError";
|
|
122
122
|
}
|
|
123
123
|
}
|
|
124
|
-
/** Provider temporarily overloaded/unavailable (HTTP 503, or Anthropic's 529).
|
|
124
|
+
/** Provider temporarily overloaded/unavailable (HTTP 503, or Anthropic's 529).
|
|
125
|
+
* Also raised for Anthropic's bare "Invalid request data" 400, which is a
|
|
126
|
+
* transient generation failure rather than a malformed request. */
|
|
125
127
|
export class SmolOverloadedError extends SmolError {
|
|
126
128
|
constructor(message, options = {}) {
|
|
127
129
|
super(message, options);
|
|
@@ -36,3 +36,17 @@ export declare function responseFormatToJsonSchema(schema: {
|
|
|
36
36
|
* there are left as-is and only an object subschema is sanitized.
|
|
37
37
|
*/
|
|
38
38
|
export declare function sanitizeJsonSchema(node: unknown): unknown;
|
|
39
|
+
/**
|
|
40
|
+
* Rewrite `const` to a one-value `enum`, recursing through the same
|
|
41
|
+
* subschema positions as `sanitizeJsonSchema` (value slots and the
|
|
42
|
+
* anyOf/oneOf/allOf/prefixItems arrays; assertion positions such as `not` and
|
|
43
|
+
* `contains` are left untouched), and collapse an `anyOf` whose branches are
|
|
44
|
+
* all single-type enums of the same type into one `{type, enum}` node.
|
|
45
|
+
*
|
|
46
|
+
* Gemini's `responseJsonSchema` silently ignores `const` (verified live: a Zod
|
|
47
|
+
* union of string literals — emitted as `anyOf: [{type:"string", const:"a"}, …]`
|
|
48
|
+
* — came back as free prose), but it enforces `enum`. Other providers accept
|
|
49
|
+
* `const`, so only the Google client applies this. Annotations on each node
|
|
50
|
+
* are preserved; a mixed-type anyOf stays an anyOf with each const rewritten.
|
|
51
|
+
*/
|
|
52
|
+
export declare function constToEnum(node: unknown): unknown;
|
package/dist/util/jsonSchema.js
CHANGED
|
@@ -131,3 +131,86 @@ function sanitizeSubschema(value) {
|
|
|
131
131
|
return value;
|
|
132
132
|
return sanitizeJsonSchema(value);
|
|
133
133
|
}
|
|
134
|
+
/**
|
|
135
|
+
* Rewrite `const` to a one-value `enum`, recursing through the same
|
|
136
|
+
* subschema positions as `sanitizeJsonSchema` (value slots and the
|
|
137
|
+
* anyOf/oneOf/allOf/prefixItems arrays; assertion positions such as `not` and
|
|
138
|
+
* `contains` are left untouched), and collapse an `anyOf` whose branches are
|
|
139
|
+
* all single-type enums of the same type into one `{type, enum}` node.
|
|
140
|
+
*
|
|
141
|
+
* Gemini's `responseJsonSchema` silently ignores `const` (verified live: a Zod
|
|
142
|
+
* union of string literals — emitted as `anyOf: [{type:"string", const:"a"}, …]`
|
|
143
|
+
* — came back as free prose), but it enforces `enum`. Other providers accept
|
|
144
|
+
* `const`, so only the Google client applies this. Annotations on each node
|
|
145
|
+
* are preserved; a mixed-type anyOf stays an anyOf with each const rewritten.
|
|
146
|
+
*/
|
|
147
|
+
export function constToEnum(node) {
|
|
148
|
+
if (typeof node !== "object" || node === null || Array.isArray(node)) {
|
|
149
|
+
return node;
|
|
150
|
+
}
|
|
151
|
+
const out = { ...node };
|
|
152
|
+
if ("const" in out) {
|
|
153
|
+
out.enum = [out.const];
|
|
154
|
+
delete out.const;
|
|
155
|
+
}
|
|
156
|
+
for (const key of SCHEMA_KEYS) {
|
|
157
|
+
if (key in out) {
|
|
158
|
+
out[key] = constToEnumSubschema(out[key]);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
for (const key of SCHEMA_MAP_KEYS) {
|
|
162
|
+
const map = out[key];
|
|
163
|
+
if (map && typeof map === "object" && !Array.isArray(map)) {
|
|
164
|
+
const rewritten = Object.create(null);
|
|
165
|
+
for (const [name, sub] of Object.entries(map)) {
|
|
166
|
+
rewritten[name] = constToEnum(sub);
|
|
167
|
+
}
|
|
168
|
+
out[key] = rewritten;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
for (const key of SCHEMA_ARRAY_KEYS) {
|
|
172
|
+
const arr = out[key];
|
|
173
|
+
if (Array.isArray(arr)) {
|
|
174
|
+
out[key] = arr.map((sub) => constToEnum(sub));
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
if ("additionalProperties" in out) {
|
|
178
|
+
out.additionalProperties = constToEnumSubschema(out.additionalProperties);
|
|
179
|
+
}
|
|
180
|
+
return collapseEnumAnyOf(out);
|
|
181
|
+
}
|
|
182
|
+
function constToEnumSubschema(value) {
|
|
183
|
+
if (typeof value === "boolean") {
|
|
184
|
+
return value;
|
|
185
|
+
}
|
|
186
|
+
return constToEnum(value);
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* `{anyOf: [{type:"string", enum:["a"]}, {type:"string", enum:["b"]}]}` →
|
|
190
|
+
* `{type:"string", enum:["a","b"]}`. Only collapses when every branch is
|
|
191
|
+
* exactly `{type, enum}` with one shared `type`, so no constraint is lost.
|
|
192
|
+
*/
|
|
193
|
+
function collapseEnumAnyOf(node) {
|
|
194
|
+
const branches = node.anyOf;
|
|
195
|
+
if (!Array.isArray(branches) || branches.length === 0) {
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
const first = branches[0];
|
|
199
|
+
const sharedType = typeof first === "object" && first !== null
|
|
200
|
+
? first.type
|
|
201
|
+
: undefined;
|
|
202
|
+
if (typeof sharedType !== "string") {
|
|
203
|
+
return node;
|
|
204
|
+
}
|
|
205
|
+
const isPlainEnum = (branch) => typeof branch === "object" &&
|
|
206
|
+
branch !== null &&
|
|
207
|
+
Object.keys(branch).every((key) => key === "type" || key === "enum") &&
|
|
208
|
+
branch.type === sharedType &&
|
|
209
|
+
Array.isArray(branch.enum);
|
|
210
|
+
if (!branches.every(isPlainEnum)) {
|
|
211
|
+
return node;
|
|
212
|
+
}
|
|
213
|
+
const { anyOf, ...rest } = node;
|
|
214
|
+
const values = branches.flatMap((branch) => branch.enum);
|
|
215
|
+
return { ...rest, type: sharedType, enum: values };
|
|
216
|
+
}
|