smoltalk 0.0.34 → 0.0.35

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.
@@ -1,5 +1,5 @@
1
1
  import { BaseMessage, MessageClass } from "./BaseMessage.js";
2
- import { TextPart } from "../../types.js";
2
+ import { TextPart, ThinkingBlock } from "../../types.js";
3
3
  import { ChatCompletionMessageParam } from "openai/resources";
4
4
  import { Content } from "@google/genai";
5
5
  import { ToolCall, ToolCallJSON } from "../ToolCall.js";
@@ -12,6 +12,7 @@ export type AssistantMessageJSON = {
12
12
  audio: any | null | undefined;
13
13
  refusal: string | null | undefined;
14
14
  toolCalls: ToolCallJSON[] | undefined;
15
+ thinkingBlocks: ThinkingBlock[] | undefined;
15
16
  };
16
17
  export declare class AssistantMessage extends BaseMessage implements MessageClass {
17
18
  _role: "assistant";
@@ -20,12 +21,14 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
20
21
  _audio?: any | null;
21
22
  _refusal?: string | null;
22
23
  _toolCalls?: ToolCall[];
24
+ _thinkingBlocks?: ThinkingBlock[];
23
25
  _rawData?: any;
24
26
  constructor(content: string | Array<TextPart> | null, options?: {
25
27
  name?: string;
26
28
  audio?: any | null;
27
29
  refusal?: string | null;
28
30
  toolCalls?: ToolCall[];
31
+ thinkingBlocks?: ThinkingBlock[];
29
32
  rawData?: any;
30
33
  });
31
34
  get content(): string;
@@ -36,6 +39,7 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
36
39
  get refusal(): string | null | undefined;
37
40
  get toolCalls(): ToolCall[] | undefined;
38
41
  get rawData(): any;
42
+ get thinkingBlocks(): ThinkingBlock[] | undefined;
39
43
  toJSON(): AssistantMessageJSON;
40
44
  static fromJSON(json: any): AssistantMessage;
41
45
  toOpenAIMessage(): ChatCompletionMessageParam;
@@ -45,6 +49,10 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
45
49
  toAnthropicMessage(): {
46
50
  role: "assistant";
47
51
  content: string | Array<{
52
+ type: "thinking";
53
+ thinking: string;
54
+ signature: string;
55
+ } | {
48
56
  type: "text";
49
57
  text: string;
50
58
  } | {
@@ -7,6 +7,7 @@ export class AssistantMessage extends BaseMessage {
7
7
  _audio;
8
8
  _refusal;
9
9
  _toolCalls;
10
+ _thinkingBlocks;
10
11
  _rawData;
11
12
  constructor(content, options = {}) {
12
13
  super();
@@ -15,6 +16,7 @@ export class AssistantMessage extends BaseMessage {
15
16
  this._audio = options.audio;
16
17
  this._refusal = options.refusal;
17
18
  this._toolCalls = options.toolCalls;
19
+ this._thinkingBlocks = options.thinkingBlocks;
18
20
  this._rawData = options.rawData;
19
21
  }
20
22
  get content() {
@@ -46,6 +48,9 @@ export class AssistantMessage extends BaseMessage {
46
48
  get rawData() {
47
49
  return this._rawData;
48
50
  }
51
+ get thinkingBlocks() {
52
+ return this._thinkingBlocks;
53
+ }
49
54
  toJSON() {
50
55
  return {
51
56
  role: this.role,
@@ -54,6 +59,7 @@ export class AssistantMessage extends BaseMessage {
54
59
  audio: this.audio,
55
60
  refusal: this.refusal,
56
61
  toolCalls: this.toolCalls?.map((tc) => tc.toJSON()),
62
+ thinkingBlocks: this._thinkingBlocks,
57
63
  };
58
64
  }
59
65
  static fromJSON(json) {
@@ -64,6 +70,7 @@ export class AssistantMessage extends BaseMessage {
64
70
  toolCalls: json.toolCalls
65
71
  ? json.toolCalls.map((tcJson) => ToolCall.fromJSON(tcJson))
66
72
  : undefined,
73
+ thinkingBlocks: json.thinkingBlocks,
67
74
  rawData: json.rawData,
68
75
  });
69
76
  }
@@ -95,6 +102,12 @@ export class AssistantMessage extends BaseMessage {
95
102
  }
96
103
  toGoogleMessage() {
97
104
  const parts = [];
105
+ // Prepend thought parts with their signatures so Gemini can resume reasoning
106
+ if (this._thinkingBlocks) {
107
+ for (const block of this._thinkingBlocks) {
108
+ parts.push({ thought: true, text: block.text, thoughtSignature: block.signature });
109
+ }
110
+ }
98
111
  if (this.content) {
99
112
  parts.push({ text: this.content });
100
113
  }
@@ -119,10 +132,18 @@ export class AssistantMessage extends BaseMessage {
119
132
  ? this._content.length > 0
120
133
  : this._content.length > 0);
121
134
  const hasToolCalls = this._toolCalls && this._toolCalls.length > 0;
122
- if (!hasToolCalls) {
135
+ const hasThinking = this._thinkingBlocks && this._thinkingBlocks.length > 0;
136
+ // If only text and no thinking/tool calls, use string shorthand
137
+ if (!hasToolCalls && !hasThinking) {
123
138
  return { role: "assistant", content: this.content };
124
139
  }
125
140
  const blocks = [];
141
+ // Thinking blocks must come first (Anthropic requires this ordering)
142
+ if (hasThinking) {
143
+ for (const block of this._thinkingBlocks) {
144
+ blocks.push({ type: "thinking", thinking: block.text, signature: block.signature });
145
+ }
146
+ }
126
147
  if (hasText) {
127
148
  const text = typeof this._content === "string"
128
149
  ? this._content
@@ -131,7 +152,7 @@ export class AssistantMessage extends BaseMessage {
131
152
  blocks.push({ type: "text", text });
132
153
  }
133
154
  }
134
- for (const tc of this._toolCalls) {
155
+ for (const tc of this._toolCalls ?? []) {
135
156
  blocks.push({ type: "tool_use", id: tc.id, name: tc.name, input: tc.arguments });
136
157
  }
137
158
  return { role: "assistant", content: blocks };
@@ -25,6 +25,10 @@ export declare function assistantMessage(content: string | Array<TextPart> | nul
25
25
  audio?: any | null;
26
26
  refusal?: string | null;
27
27
  toolCalls?: Array<any>;
28
+ thinkingBlocks?: Array<{
29
+ text: string;
30
+ signature: string;
31
+ }>;
28
32
  rawData?: any;
29
33
  }): AssistantMessage;
30
34
  export declare function developerMessage(content: string | Array<TextPart>, options?: {
@@ -64,16 +64,20 @@ export class SmolAnthropic extends BaseClient {
64
64
  description: tool.description,
65
65
  }))
66
66
  : undefined;
67
- return { system, messages: anthropicMessages, tools };
67
+ const thinking = config.thinking?.enabled
68
+ ? { type: "enabled", budget_tokens: config.thinking.budgetTokens ?? 5000 }
69
+ : undefined;
70
+ return { system, messages: anthropicMessages, tools, thinking };
68
71
  }
69
72
  async _textSync(config) {
70
- const { system, messages, tools } = this.buildRequest(config);
73
+ const { system, messages, tools, thinking } = this.buildRequest(config);
71
74
  this.logger.debug("Sending request to Anthropic:", {
72
75
  model: this.model,
73
76
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
74
77
  messages,
75
78
  system,
76
79
  tools,
80
+ thinking,
77
81
  });
78
82
  const response = await this.client.messages.create({
79
83
  model: this.model,
@@ -81,6 +85,7 @@ export class SmolAnthropic extends BaseClient {
81
85
  messages,
82
86
  ...(system && { system }),
83
87
  ...(tools && { tools }),
88
+ ...(thinking && { thinking }),
84
89
  ...(config.temperature !== undefined && {
85
90
  temperature: config.temperature,
86
91
  }),
@@ -90,6 +95,7 @@ export class SmolAnthropic extends BaseClient {
90
95
  this.logger.debug("Response from Anthropic:", response);
91
96
  let output = null;
92
97
  const toolCalls = [];
98
+ const thinkingBlocks = [];
93
99
  for (const block of response.content) {
94
100
  if (block.type === "text") {
95
101
  output = (output ?? "") + block.text;
@@ -97,24 +103,30 @@ export class SmolAnthropic extends BaseClient {
97
103
  else if (block.type === "tool_use") {
98
104
  toolCalls.push(new ToolCall(block.id, block.name, block.input));
99
105
  }
106
+ else if (block.type === "thinking") {
107
+ const b = block;
108
+ thinkingBlocks.push({ text: b.thinking, signature: b.signature });
109
+ }
100
110
  }
101
111
  const { usage, cost } = this.calculateUsageAndCost(response.usage);
102
112
  return success({
103
113
  output,
104
114
  toolCalls,
115
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
105
116
  usage,
106
117
  cost,
107
118
  model: this.model,
108
119
  });
109
120
  }
110
121
  async *_textStream(config) {
111
- const { system, messages, tools } = this.buildRequest(config);
122
+ const { system, messages, tools, thinking } = this.buildRequest(config);
112
123
  this.logger.debug("Sending streaming request to Anthropic:", {
113
124
  model: this.model,
114
125
  max_tokens: config.maxTokens ?? DEFAULT_MAX_TOKENS,
115
126
  messages,
116
127
  system,
117
128
  tools,
129
+ thinking,
118
130
  });
119
131
  const stream = await this.client.messages.create({
120
132
  model: this.model,
@@ -122,6 +134,7 @@ export class SmolAnthropic extends BaseClient {
122
134
  messages,
123
135
  ...(system && { system }),
124
136
  ...(tools && { tools }),
137
+ ...(thinking && { thinking }),
125
138
  ...(config.temperature !== undefined && {
126
139
  temperature: config.temperature,
127
140
  }),
@@ -131,19 +144,25 @@ export class SmolAnthropic extends BaseClient {
131
144
  let content = "";
132
145
  // Track tool blocks by index: index -> { id, name, arguments (partial JSON) }
133
146
  const toolBlocks = new Map();
147
+ // Track thinking blocks by index: index -> { text, signature }
148
+ const thinkingBlockMap = new Map();
134
149
  let inputTokens = 0;
135
150
  let outputTokens = 0;
136
151
  for await (const event of stream) {
137
152
  if (event.type === "message_start") {
138
153
  inputTokens = event.message.usage.input_tokens;
139
154
  }
140
- else if (event.type === "content_block_start" &&
141
- event.content_block.type === "tool_use") {
142
- toolBlocks.set(event.index, {
143
- id: event.content_block.id,
144
- name: event.content_block.name,
145
- arguments: "",
146
- });
155
+ else if (event.type === "content_block_start") {
156
+ if (event.content_block.type === "tool_use") {
157
+ toolBlocks.set(event.index, {
158
+ id: event.content_block.id,
159
+ name: event.content_block.name,
160
+ arguments: "",
161
+ });
162
+ }
163
+ else if (event.content_block.type === "thinking") {
164
+ thinkingBlockMap.set(event.index, { text: "", signature: "" });
165
+ }
147
166
  }
148
167
  else if (event.type === "content_block_delta") {
149
168
  if (event.delta.type === "text_delta") {
@@ -156,6 +175,25 @@ export class SmolAnthropic extends BaseClient {
156
175
  block.arguments += event.delta.partial_json;
157
176
  }
158
177
  }
178
+ else if (event.delta.type === "thinking_delta") {
179
+ const block = thinkingBlockMap.get(event.index);
180
+ if (block) {
181
+ block.text += event.delta.thinking;
182
+ }
183
+ }
184
+ else if (event.delta.type === "signature_delta") {
185
+ const block = thinkingBlockMap.get(event.index);
186
+ if (block) {
187
+ block.signature = event.delta.signature;
188
+ }
189
+ }
190
+ }
191
+ else if (event.type === "content_block_stop") {
192
+ // Emit thinking chunk once the block is fully assembled
193
+ const thinkingBlock = thinkingBlockMap.get(event.index);
194
+ if (thinkingBlock) {
195
+ yield { type: "thinking", text: thinkingBlock.text, signature: thinkingBlock.signature };
196
+ }
159
197
  }
160
198
  else if (event.type === "message_delta") {
161
199
  outputTokens = event.usage.output_tokens;
@@ -168,6 +206,7 @@ export class SmolAnthropic extends BaseClient {
168
206
  toolCalls.push(toolCall);
169
207
  yield { type: "tool_call", toolCall };
170
208
  }
209
+ const thinkingBlocks = Array.from(thinkingBlockMap.values());
171
210
  const usage = {
172
211
  inputTokens,
173
212
  outputTokens,
@@ -179,6 +218,7 @@ export class SmolAnthropic extends BaseClient {
179
218
  result: {
180
219
  output: content || null,
181
220
  toolCalls,
221
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
182
222
  usage,
183
223
  cost,
184
224
  model: this.model,
@@ -91,6 +91,7 @@ export class SmolGoogle extends BaseClient {
91
91
  this.logger.debug("Response from Google Gemini:", JSON.stringify(result, null, 2));
92
92
  const output = result.text || null;
93
93
  const toolCalls = [];
94
+ const thinkingBlocks = [];
94
95
  result.candidates?.forEach((candidate) => {
95
96
  if (candidate.content && candidate.content.parts) {
96
97
  candidate.content.parts.forEach((part) => {
@@ -98,6 +99,13 @@ export class SmolGoogle extends BaseClient {
98
99
  const functionCall = part.functionCall;
99
100
  toolCalls.push(new ToolCall("", functionCall.name, functionCall.args));
100
101
  }
102
+ // Capture thought parts (thought: true indicates a thinking part)
103
+ if (part.thoughtSignature) {
104
+ thinkingBlocks.push({
105
+ text: part.text || "",
106
+ signature: part.thoughtSignature,
107
+ });
108
+ }
101
109
  });
102
110
  }
103
111
  });
@@ -107,6 +115,7 @@ export class SmolGoogle extends BaseClient {
107
115
  return success({
108
116
  output,
109
117
  toolCalls,
118
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
110
119
  usage,
111
120
  cost,
112
121
  model: request.model,
@@ -118,6 +127,7 @@ export class SmolGoogle extends BaseClient {
118
127
  const stream = await this.client.models.generateContentStream(request);
119
128
  let content = "";
120
129
  const toolCallsMap = new Map();
130
+ const thinkingBlocks = [];
121
131
  let usage;
122
132
  let cost;
123
133
  for await (const chunk of stream) {
@@ -127,22 +137,28 @@ export class SmolGoogle extends BaseClient {
127
137
  usage = usageAndCost.usage;
128
138
  cost = usageAndCost.cost;
129
139
  }
130
- // Handle text content
131
- if (chunk.text) {
132
- content += chunk.text;
133
- yield { type: "text", text: chunk.text };
134
- }
135
- // Handle function calls
136
- if (chunk.functionCalls) {
137
- for (const functionCall of chunk.functionCalls) {
138
- const id = functionCall.id || functionCall.name || "";
139
- const name = functionCall.name || "";
140
- if (!toolCallsMap.has(id)) {
141
- toolCallsMap.set(id, {
142
- id: id,
143
- name: name,
144
- arguments: functionCall.args,
145
- });
140
+ // Iterate raw parts to capture thought signatures and regular content
141
+ for (const candidate of chunk.candidates || []) {
142
+ for (const part of candidate?.content?.parts || []) {
143
+ const p = part;
144
+ if (p.thoughtSignature) {
145
+ const block = {
146
+ text: p.text || "",
147
+ signature: p.thoughtSignature,
148
+ };
149
+ thinkingBlocks.push(block);
150
+ yield { type: "thinking", text: block.text, signature: block.signature };
151
+ }
152
+ else if (p.text) {
153
+ content += p.text;
154
+ yield { type: "text", text: p.text };
155
+ }
156
+ else if (p.functionCall) {
157
+ const id = p.functionCall.id || p.functionCall.name || "";
158
+ const name = p.functionCall.name || "";
159
+ if (!toolCallsMap.has(id)) {
160
+ toolCallsMap.set(id, { id, name, arguments: p.functionCall.args });
161
+ }
146
162
  }
147
163
  }
148
164
  }
@@ -160,6 +176,7 @@ export class SmolGoogle extends BaseClient {
160
176
  result: {
161
177
  output: content || null,
162
178
  toolCalls,
179
+ ...(thinkingBlocks.length > 0 && { thinkingBlocks }),
163
180
  usage,
164
181
  cost,
165
182
  model: request.model,
package/dist/types.d.ts CHANGED
@@ -5,6 +5,10 @@ import { Message } from "./classes/message/index.js";
5
5
  import { ToolCall } from "./classes/ToolCall.js";
6
6
  import { ModelConfig, ModelName, Provider } from "./models.js";
7
7
  import { Result } from "./types/result.js";
8
+ export type ThinkingBlock = {
9
+ text: string;
10
+ signature: string;
11
+ };
8
12
  export type PromptConfig = {
9
13
  messages: Message[];
10
14
  tools?: {
@@ -19,6 +23,10 @@ export type PromptConfig = {
19
23
  parallelToolCalls?: boolean;
20
24
  responseFormat?: ZodType;
21
25
  stream?: boolean;
26
+ thinking?: {
27
+ enabled: boolean;
28
+ budgetTokens?: number;
29
+ };
22
30
  responseFormatOptions?: Partial<{
23
31
  name: string;
24
32
  strict: boolean;
@@ -65,6 +73,7 @@ export type CostEstimate = {
65
73
  export type PromptResult = {
66
74
  output: string | null;
67
75
  toolCalls: ToolCall[];
76
+ thinkingBlocks?: ThinkingBlock[];
68
77
  usage?: TokenUsage;
69
78
  cost?: CostEstimate;
70
79
  model?: ModelName | ModelConfig;
@@ -72,6 +81,10 @@ export type PromptResult = {
72
81
  export type StreamChunk = {
73
82
  type: "text";
74
83
  text: string;
84
+ } | {
85
+ type: "thinking";
86
+ text: string;
87
+ signature?: string;
75
88
  } | {
76
89
  type: "tool_call";
77
90
  toolCall: ToolCall;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "scripts": {
@@ -47,4 +47,4 @@
47
47
  "ollama": "^0.6.3",
48
48
  "openai": "^6.15.0"
49
49
  }
50
- }
50
+ }