smoltalk 0.7.1 → 0.8.1
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.
|
@@ -5,23 +5,29 @@ export declare const ToolCallJSONSchema: z.ZodObject<{
|
|
|
5
5
|
id: z.ZodDefault<z.ZodString>;
|
|
6
6
|
name: z.ZodString;
|
|
7
7
|
arguments: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
8
|
+
thoughtSignature: z.ZodOptional<z.ZodString>;
|
|
8
9
|
}, z.core.$strip>;
|
|
9
10
|
export type ToolCallJSON = z.infer<typeof ToolCallJSONSchema>;
|
|
10
|
-
export type ToolCallOptions = {
|
|
11
|
+
export type ToolCallOptions = {
|
|
12
|
+
thoughtSignature?: string;
|
|
13
|
+
};
|
|
11
14
|
export declare class ToolCall {
|
|
12
15
|
private _id;
|
|
13
16
|
private _name;
|
|
14
17
|
private _arguments;
|
|
18
|
+
private _thoughtSignature?;
|
|
15
19
|
private logger;
|
|
16
20
|
constructor(id: string, name: string, args: Record<string, any> | string, options?: ToolCallOptions);
|
|
17
21
|
get id(): string;
|
|
18
22
|
get name(): string;
|
|
19
23
|
get arguments(): Record<string, any>;
|
|
24
|
+
get thoughtSignature(): string | undefined;
|
|
20
25
|
toJSON(): ToolCallJSON;
|
|
21
26
|
static fromJSON(json: unknown): ToolCall;
|
|
22
27
|
toOpenAI(): any;
|
|
23
28
|
toGoogle(): {
|
|
24
29
|
functionCall: FunctionCall;
|
|
30
|
+
thoughtSignature?: string;
|
|
25
31
|
};
|
|
26
32
|
toOpenAIResponseInputItem(): ResponseInputItem;
|
|
27
33
|
}
|
package/dist/classes/ToolCall.js
CHANGED
|
@@ -4,15 +4,20 @@ export const ToolCallJSONSchema = z.object({
|
|
|
4
4
|
id: z.string().default(""),
|
|
5
5
|
name: z.string(),
|
|
6
6
|
arguments: z.record(z.string(), z.any()).default({}),
|
|
7
|
+
// Google Gemini 3 attaches an encrypted thought signature to the same part as
|
|
8
|
+
// the function call; it must be echoed back verbatim during tool use.
|
|
9
|
+
thoughtSignature: z.string().optional(),
|
|
7
10
|
});
|
|
8
11
|
export class ToolCall {
|
|
9
12
|
_id;
|
|
10
13
|
_name;
|
|
11
14
|
_arguments;
|
|
15
|
+
_thoughtSignature;
|
|
12
16
|
logger;
|
|
13
17
|
constructor(id, name, args, options = {}) {
|
|
14
18
|
this._id = id;
|
|
15
19
|
this._name = name;
|
|
20
|
+
this._thoughtSignature = options.thoughtSignature;
|
|
16
21
|
this.logger = getLogger();
|
|
17
22
|
if (typeof args === "string") {
|
|
18
23
|
try {
|
|
@@ -37,11 +42,17 @@ export class ToolCall {
|
|
|
37
42
|
get arguments() {
|
|
38
43
|
return this._arguments;
|
|
39
44
|
}
|
|
45
|
+
get thoughtSignature() {
|
|
46
|
+
return this._thoughtSignature;
|
|
47
|
+
}
|
|
40
48
|
toJSON() {
|
|
41
49
|
return {
|
|
42
50
|
id: this._id,
|
|
43
51
|
name: this._name,
|
|
44
52
|
arguments: this._arguments,
|
|
53
|
+
...(this._thoughtSignature !== undefined && {
|
|
54
|
+
thoughtSignature: this._thoughtSignature,
|
|
55
|
+
}),
|
|
45
56
|
};
|
|
46
57
|
}
|
|
47
58
|
static fromJSON(json) {
|
|
@@ -53,7 +64,9 @@ export class ToolCall {
|
|
|
53
64
|
logger.debug("ToolCall payload that failed to parse:", JSON.stringify(json, null, 2));
|
|
54
65
|
throw new Error("Failed to parse ToolCall");
|
|
55
66
|
}
|
|
56
|
-
return new ToolCall(result.data.id, result.data.name, result.data.arguments
|
|
67
|
+
return new ToolCall(result.data.id, result.data.name, result.data.arguments, {
|
|
68
|
+
thoughtSignature: result.data.thoughtSignature,
|
|
69
|
+
});
|
|
57
70
|
}
|
|
58
71
|
toOpenAI() {
|
|
59
72
|
return {
|
|
@@ -71,6 +84,11 @@ export class ToolCall {
|
|
|
71
84
|
name: this.name,
|
|
72
85
|
args: this.arguments,
|
|
73
86
|
},
|
|
87
|
+
// Gemini 3 requires the original thought signature echoed back on the
|
|
88
|
+
// function-call part; omitting it fails validation during tool use.
|
|
89
|
+
...(this._thoughtSignature !== undefined && {
|
|
90
|
+
thoughtSignature: this._thoughtSignature,
|
|
91
|
+
}),
|
|
74
92
|
};
|
|
75
93
|
}
|
|
76
94
|
toOpenAIResponseInputItem() {
|
|
@@ -19,6 +19,7 @@ export declare const AssistantMessageJSONSchema: z.ZodObject<{
|
|
|
19
19
|
id: z.ZodDefault<z.ZodString>;
|
|
20
20
|
name: z.ZodString;
|
|
21
21
|
arguments: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodAny>>;
|
|
22
|
+
thoughtSignature: z.ZodOptional<z.ZodString>;
|
|
22
23
|
}, z.core.$strip>>>;
|
|
23
24
|
thinkingBlocks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
24
25
|
text: z.ZodString;
|
|
@@ -2,6 +2,7 @@ import type { MessageParam, Tool } from "@anthropic-ai/sdk/resources/messages.js
|
|
|
2
2
|
import { PromptResult, Result, SmolClient, SmolConfig, StreamChunk, HostedToolResult } from "../types.js";
|
|
3
3
|
import { BaseClient } from "./baseClient.js";
|
|
4
4
|
import { ModelName } from "../models.js";
|
|
5
|
+
export declare function mergeConsecutiveMessages(messages: MessageParam[]): MessageParam[];
|
|
5
6
|
export declare function anthropicWebSearchEntries(hostedTools?: string[]): any[];
|
|
6
7
|
export declare function parseAnthropicHostedTools(response: any, provider: string): HostedToolResult[];
|
|
7
8
|
type EphemeralCacheControl = {
|
|
@@ -12,6 +12,40 @@ import { BaseClient } from "./baseClient.js";
|
|
|
12
12
|
import { getModel, isTextModel } from "../models.js";
|
|
13
13
|
import { Model } from "../model.js";
|
|
14
14
|
const DEFAULT_MAX_TOKENS = 4096;
|
|
15
|
+
// Normalize an Anthropic message's content to a block array for merging.
|
|
16
|
+
// Array content is passed through; a non-empty string becomes a single text
|
|
17
|
+
// block; an empty string (or any other unexpected value) becomes no blocks
|
|
18
|
+
// (Anthropic rejects empty text blocks). Guarding non-array values keeps the
|
|
19
|
+
// exported merge helper from throwing on a spread if a caller passes content
|
|
20
|
+
// that isn't the string | block[] the type promises.
|
|
21
|
+
function anthropicContentToBlocks(content) {
|
|
22
|
+
if (Array.isArray(content)) {
|
|
23
|
+
return content;
|
|
24
|
+
}
|
|
25
|
+
if (typeof content === "string" && content.length > 0) {
|
|
26
|
+
return [{ type: "text", text: content }];
|
|
27
|
+
}
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
// Anthropic requires strict user/assistant turn alternation. Collapse any run of
|
|
31
|
+
// consecutive messages that share a role into a single message by concatenating
|
|
32
|
+
// their content blocks. Does not mutate the input.
|
|
33
|
+
export function mergeConsecutiveMessages(messages) {
|
|
34
|
+
const merged = [];
|
|
35
|
+
for (const msg of messages) {
|
|
36
|
+
const last = merged[merged.length - 1];
|
|
37
|
+
if (last && last.role === msg.role) {
|
|
38
|
+
const content = [
|
|
39
|
+
...anthropicContentToBlocks(last.content),
|
|
40
|
+
...anthropicContentToBlocks(msg.content),
|
|
41
|
+
];
|
|
42
|
+
merged[merged.length - 1] = { ...last, content };
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
merged.push(msg);
|
|
46
|
+
}
|
|
47
|
+
return merged;
|
|
48
|
+
}
|
|
15
49
|
export function anthropicWebSearchEntries(hostedTools) {
|
|
16
50
|
if (hostedTools && hostedTools.includes(WEB_SEARCH)) {
|
|
17
51
|
return [{ type: "web_search_20250305", name: "web_search" }];
|
|
@@ -187,30 +221,21 @@ export class SmolAnthropic extends BaseClient {
|
|
|
187
221
|
.filter((m) => m instanceof SystemMessage || m instanceof DeveloperMessage)
|
|
188
222
|
.map((m) => m.content);
|
|
189
223
|
const system = systemParts.length > 0 ? systemParts.join("\n") : undefined;
|
|
190
|
-
// Convert remaining messages
|
|
191
|
-
const
|
|
224
|
+
// Convert remaining messages into Anthropic message params.
|
|
225
|
+
const converted = [];
|
|
192
226
|
for (const msg of config.messages) {
|
|
193
227
|
if (msg instanceof SystemMessage || msg instanceof DeveloperMessage) {
|
|
194
228
|
continue;
|
|
195
229
|
}
|
|
196
|
-
const
|
|
197
|
-
if (
|
|
230
|
+
const c = msg.toAnthropicMessage();
|
|
231
|
+
if (c === null)
|
|
198
232
|
continue;
|
|
199
|
-
|
|
200
|
-
if (converted.role === "user" &&
|
|
201
|
-
Array.isArray(converted.content) &&
|
|
202
|
-
converted.content.every((c) => c.type === "tool_result")) {
|
|
203
|
-
const last = anthropicMessages[anthropicMessages.length - 1];
|
|
204
|
-
if (last &&
|
|
205
|
-
last.role === "user" &&
|
|
206
|
-
Array.isArray(last.content) &&
|
|
207
|
-
last.content.every((c) => c.type === "tool_result")) {
|
|
208
|
-
last.content.push(...converted.content);
|
|
209
|
-
continue;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
anthropicMessages.push(converted);
|
|
233
|
+
converted.push(c);
|
|
213
234
|
}
|
|
235
|
+
// Anthropic requires strict user/assistant alternation — merge any run of
|
|
236
|
+
// consecutive same-role messages into one (e.g. two user turns, or a user
|
|
237
|
+
// turn followed by tool results, which also map to role "user").
|
|
238
|
+
const anthropicMessages = mergeConsecutiveMessages(converted);
|
|
214
239
|
const functionTools = config.tools && config.tools.length > 0
|
|
215
240
|
? config.tools.map((tool) => zodToAnthropicTool(tool.name, tool.schema, {
|
|
216
241
|
description: tool.description,
|
package/dist/clients/google.js
CHANGED
|
@@ -134,6 +134,14 @@ export class SmolGoogle extends BaseClient {
|
|
|
134
134
|
toolGroups.push(entry);
|
|
135
135
|
}
|
|
136
136
|
genConfig.tools = toolGroups;
|
|
137
|
+
// Gemini rejects mixing built-in (server-side) tools with function calling
|
|
138
|
+
// unless the caller opts in. Only required when both kinds coexist.
|
|
139
|
+
if (tools.length > 0 && hostedEntries.length > 0) {
|
|
140
|
+
genConfig.toolConfig = {
|
|
141
|
+
...genConfig.toolConfig,
|
|
142
|
+
includeServerSideToolInvocations: true,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
137
145
|
}
|
|
138
146
|
if (config.responseFormat) {
|
|
139
147
|
genConfig.responseMimeType = "application/json";
|
|
@@ -277,7 +285,11 @@ export class SmolGoogle extends BaseClient {
|
|
|
277
285
|
candidate.content.parts.forEach((part) => {
|
|
278
286
|
if (part.functionCall) {
|
|
279
287
|
const functionCall = part.functionCall;
|
|
280
|
-
|
|
288
|
+
// Gemini 3 rides the thought signature on the same part as the
|
|
289
|
+
// function call; capture it so it can be echoed back during tool use.
|
|
290
|
+
toolCalls.push(new ToolCall("", functionCall.name, functionCall.args, {
|
|
291
|
+
thoughtSignature: part.thoughtSignature,
|
|
292
|
+
}));
|
|
281
293
|
}
|
|
282
294
|
else if (part.thoughtSignature) {
|
|
283
295
|
// Capture thought parts (thought: true indicates a thinking part)
|
|
@@ -352,7 +364,38 @@ export class SmolGoogle extends BaseClient {
|
|
|
352
364
|
for (const candidate of chunk.candidates || []) {
|
|
353
365
|
for (const part of candidate?.content?.parts || []) {
|
|
354
366
|
const p = part;
|
|
355
|
-
|
|
367
|
+
// Check functionCall first: Gemini 3 attaches the thought signature to
|
|
368
|
+
// the same part as the function call, so a thoughtSignature-first check
|
|
369
|
+
// would misfile the tool call as a thinking block and drop it.
|
|
370
|
+
if (p.functionCall) {
|
|
371
|
+
const id = p.functionCall.id || p.functionCall.name || "";
|
|
372
|
+
const name = p.functionCall.name || "";
|
|
373
|
+
const existing = toolCallsMap.get(id);
|
|
374
|
+
if (!existing) {
|
|
375
|
+
toolCallsMap.set(id, {
|
|
376
|
+
id,
|
|
377
|
+
name,
|
|
378
|
+
arguments: p.functionCall.args,
|
|
379
|
+
thoughtSignature: p.thoughtSignature,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
// A later chunk can carry the thought signature (or fuller
|
|
384
|
+
// args/name) for a function call first seen without them.
|
|
385
|
+
// Backfill missing fields rather than dropping the update, so the
|
|
386
|
+
// signature isn't lost during tool-use round trips.
|
|
387
|
+
if (p.thoughtSignature && !existing.thoughtSignature) {
|
|
388
|
+
existing.thoughtSignature = p.thoughtSignature;
|
|
389
|
+
}
|
|
390
|
+
if (p.functionCall.args && !existing.arguments) {
|
|
391
|
+
existing.arguments = p.functionCall.args;
|
|
392
|
+
}
|
|
393
|
+
if (name && !existing.name) {
|
|
394
|
+
existing.name = name;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
else if (p.thoughtSignature) {
|
|
356
399
|
const block = {
|
|
357
400
|
text: p.text || "",
|
|
358
401
|
signature: p.thoughtSignature,
|
|
@@ -368,17 +411,6 @@ export class SmolGoogle extends BaseClient {
|
|
|
368
411
|
content += p.text;
|
|
369
412
|
yield { type: "text", text: p.text };
|
|
370
413
|
}
|
|
371
|
-
else if (p.functionCall) {
|
|
372
|
-
const id = p.functionCall.id || p.functionCall.name || "";
|
|
373
|
-
const name = p.functionCall.name || "";
|
|
374
|
-
if (!toolCallsMap.has(id)) {
|
|
375
|
-
toolCallsMap.set(id, {
|
|
376
|
-
id,
|
|
377
|
-
name,
|
|
378
|
-
arguments: p.functionCall.args,
|
|
379
|
-
});
|
|
380
|
-
}
|
|
381
|
-
}
|
|
382
414
|
}
|
|
383
415
|
}
|
|
384
416
|
}
|
|
@@ -387,7 +419,9 @@ export class SmolGoogle extends BaseClient {
|
|
|
387
419
|
// Yield tool calls
|
|
388
420
|
const toolCalls = [];
|
|
389
421
|
for (const tc of toolCallsMap.values()) {
|
|
390
|
-
const toolCall = new ToolCall(tc.id, tc.name, tc.arguments
|
|
422
|
+
const toolCall = new ToolCall(tc.id, tc.name, tc.arguments, {
|
|
423
|
+
thoughtSignature: tc.thoughtSignature,
|
|
424
|
+
});
|
|
391
425
|
toolCalls.push(toolCall);
|
|
392
426
|
yield { type: "tool_call", toolCall };
|
|
393
427
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "smoltalk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.1",
|
|
4
4
|
"description": "A common interface for LLM APIs",
|
|
5
5
|
"homepage": "https://github.com/egonSchiele/smoltalk",
|
|
6
6
|
"files": [
|
|
@@ -28,12 +28,12 @@
|
|
|
28
28
|
"author": "Aditya Bhargava",
|
|
29
29
|
"license": "ISC",
|
|
30
30
|
"dependencies": {
|
|
31
|
-
"@anthropic-ai/sdk": "^0.
|
|
32
|
-
"@google/genai": "^
|
|
31
|
+
"@anthropic-ai/sdk": "^0.109.0",
|
|
32
|
+
"@google/genai": "^2.10.0",
|
|
33
33
|
"nanoid": "^5.1.6",
|
|
34
34
|
"ollama": "^0.6.3",
|
|
35
|
-
"openai": "^6.
|
|
36
|
-
"zod": "^4.3
|
|
35
|
+
"openai": "^6.45.0",
|
|
36
|
+
"zod": "^4.4.3"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"tsx": "^4.19.2"
|