smoltalk 0.15.1 → 0.15.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/README.md CHANGED
@@ -210,6 +210,7 @@ Two provider notes:
210
210
  | `stream` | `boolean` | If `true`, returns an `AsyncGenerator<StreamChunk>` instead of a `Promise`. |
211
211
  | `thinking` | `{ enabled, budgetTokens? }` | Enable extended thinking / thought signatures (Anthropic and Google). |
212
212
  | `reasoningEffort` | `"low" \| "medium" \| "high"` | Provider-agnostic reasoning effort level. |
213
+ | `logprobs` | `{ top?: number }` | Return each generated token's log probability on `result.logprobs`, with `top` alternatives per token. OpenAI only; other providers ignore it. |
213
214
  | `maxMessages` | `number` | If the message list exceeds this count, returns a failure instead of calling the API. |
214
215
  | `abortSignal` | `AbortSignal` | Cancel an in-flight request. |
215
216
  | `toolLoopDetection` | `ToolLoopDetection` | Detect and break tool-call loops. See below. |
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { BaseMessage, MessageClass } from "./BaseMessage.js";
3
- import { CostEstimate, TextPart, ThinkingBlock, TokenUsage } from "../../types.js";
3
+ import { CostEstimate, TextPart, ThinkingBlock, TokenLogprob, TokenUsage } from "../../types.js";
4
4
  import { ChatCompletionMessageParam } from "openai/resources";
5
5
  import { Content } from "@google/genai";
6
6
  import { ToolCall } from "../ToolCall.js";
@@ -25,6 +25,14 @@ export declare const AssistantMessageJSONSchema: z.ZodObject<{
25
25
  text: z.ZodString;
26
26
  signature: z.ZodString;
27
27
  }, z.core.$strip>>>;
28
+ logprobs: z.ZodOptional<z.ZodArray<z.ZodObject<{
29
+ token: z.ZodString;
30
+ logprob: z.ZodNumber;
31
+ top: z.ZodOptional<z.ZodArray<z.ZodObject<{
32
+ token: z.ZodString;
33
+ logprob: z.ZodNumber;
34
+ }, z.core.$strip>>>;
35
+ }, z.core.$strip>>>;
28
36
  rawData: z.ZodOptional<z.ZodAny>;
29
37
  usage: z.ZodOptional<z.ZodObject<{
30
38
  inputTokens: z.ZodNumber;
@@ -54,6 +62,7 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
54
62
  _refusal?: string | null;
55
63
  _toolCalls?: ToolCall[];
56
64
  _thinkingBlocks?: ThinkingBlock[];
65
+ _logprobs?: TokenLogprob[];
57
66
  _rawData?: any;
58
67
  _usage?: TokenUsage;
59
68
  _cost?: CostEstimate;
@@ -63,6 +72,7 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
63
72
  refusal?: string | null;
64
73
  toolCalls?: ToolCall[];
65
74
  thinkingBlocks?: ThinkingBlock[];
75
+ logprobs?: TokenLogprob[];
66
76
  rawData?: any;
67
77
  usage?: TokenUsage;
68
78
  cost?: CostEstimate;
@@ -76,6 +86,7 @@ export declare class AssistantMessage extends BaseMessage implements MessageClas
76
86
  get toolCalls(): ToolCall[] | undefined;
77
87
  get rawData(): any;
78
88
  get thinkingBlocks(): ThinkingBlock[] | undefined;
89
+ get logprobs(): TokenLogprob[] | undefined;
79
90
  get usage(): TokenUsage | undefined;
80
91
  get cost(): CostEstimate | undefined;
81
92
  toJSON(): AssistantMessageJSON;
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { BaseMessage } from "./BaseMessage.js";
3
- import { CostEstimateSchema, TextPartSchema, ThinkingBlockSchema, TokenUsageSchema, } from "../../types.js";
3
+ import { CostEstimateSchema, TextPartSchema, ThinkingBlockSchema, TokenLogprobSchema, TokenUsageSchema, } from "../../types.js";
4
4
  import { ToolCall, ToolCallJSONSchema } from "../ToolCall.js";
5
5
  import { getLogger } from "../../util/logger.js";
6
6
  export const AssistantMessageJSONSchema = z.object({
@@ -11,6 +11,7 @@ export const AssistantMessageJSONSchema = z.object({
11
11
  refusal: z.string().nullable().optional(),
12
12
  toolCalls: z.array(ToolCallJSONSchema).optional(),
13
13
  thinkingBlocks: z.array(ThinkingBlockSchema).optional(),
14
+ logprobs: z.array(TokenLogprobSchema).optional(),
14
15
  rawData: z.any().optional(),
15
16
  usage: TokenUsageSchema.optional(),
16
17
  cost: CostEstimateSchema.optional(),
@@ -23,6 +24,7 @@ export class AssistantMessage extends BaseMessage {
23
24
  _refusal;
24
25
  _toolCalls;
25
26
  _thinkingBlocks;
27
+ _logprobs;
26
28
  _rawData;
27
29
  _usage;
28
30
  _cost;
@@ -34,6 +36,7 @@ export class AssistantMessage extends BaseMessage {
34
36
  this._refusal = options.refusal;
35
37
  this._toolCalls = options.toolCalls;
36
38
  this._thinkingBlocks = options.thinkingBlocks;
39
+ this._logprobs = options.logprobs;
37
40
  this._rawData = options.rawData;
38
41
  this._usage = options.usage;
39
42
  this._cost = options.cost;
@@ -65,6 +68,9 @@ export class AssistantMessage extends BaseMessage {
65
68
  get thinkingBlocks() {
66
69
  return this._thinkingBlocks;
67
70
  }
71
+ get logprobs() {
72
+ return this._logprobs;
73
+ }
68
74
  get usage() {
69
75
  return this._usage;
70
76
  }
@@ -80,6 +86,7 @@ export class AssistantMessage extends BaseMessage {
80
86
  refusal: this.refusal,
81
87
  toolCalls: this.toolCalls?.map((tc) => tc.toJSON()),
82
88
  thinkingBlocks: this._thinkingBlocks,
89
+ logprobs: this._logprobs,
83
90
  usage: this._usage,
84
91
  cost: this._cost,
85
92
  rawData: this._rawData,
@@ -100,6 +107,7 @@ export class AssistantMessage extends BaseMessage {
100
107
  refusal: result.data.refusal,
101
108
  toolCalls: result.data.toolCalls?.map((tc) => ToolCall.fromJSON(tc)),
102
109
  thinkingBlocks: result.data.thinkingBlocks,
110
+ logprobs: result.data.logprobs,
103
111
  rawData: result.data.rawData,
104
112
  usage: result.data.usage,
105
113
  cost: result.data.cost,
@@ -8,7 +8,7 @@ import type { AssistantMessageJSON } from "./AssistantMessage.js";
8
8
  import type { DeveloperMessageJSON } from "./DeveloperMessage.js";
9
9
  import type { SystemMessageJSON } from "./SystemMessage.js";
10
10
  import type { ToolMessageJSON } from "./ToolMessage.js";
11
- import { CostEstimate, TextPart, TokenUsage, UserContentInput, ImagePart, FilePart, AudioPart } from "../../types.js";
11
+ import { CostEstimate, TextPart, TokenLogprob, TokenUsage, UserContentInput, ImagePart, FilePart, AudioPart } from "../../types.js";
12
12
  import type { ImageRef, BlobRef } from "../../util/blobRef.js";
13
13
  export * from "./AssistantMessage.js";
14
14
  export * from "./BaseMessage.js";
@@ -38,6 +38,7 @@ export declare function assistantMessage(content: string | Array<TextPart> | nul
38
38
  text: string;
39
39
  signature: string;
40
40
  }>;
41
+ logprobs?: TokenLogprob[];
41
42
  rawData?: any;
42
43
  usage?: TokenUsage;
43
44
  cost?: CostEstimate;
@@ -0,0 +1,37 @@
1
+ import type { TokenAlternative, TokenLogprob } from "../types.js";
2
+ export type OpenAILogprob = {
3
+ token: string;
4
+ logprob: number;
5
+ top_logprobs?: TokenAlternative[];
6
+ };
7
+ /** OpenAI's per-token entries (chat `choices[].logprobs.content` and
8
+ * Responses `output_text.logprobs`) as smoltalk's shape. Undefined when
9
+ * there are none, so the result field stays absent. */
10
+ export declare function fromOpenAILogprobs(entries: OpenAILogprob[] | null | undefined): TokenLogprob[] | undefined;
11
+ /** How many alternatives per token a `logprobs` option asks for, or
12
+ * undefined for none. `top: 0` and a missing `top` both mean none. */
13
+ export declare function topAlternatives(option: {
14
+ top?: number;
15
+ } | undefined): number | undefined;
16
+ type OpenAIChatLogprobParams = {
17
+ logprobs?: true;
18
+ top_logprobs?: number;
19
+ };
20
+ /** The chat API's request parameters for a `logprobs` option: `logprobs: true`
21
+ * whenever the option is set, `top_logprobs` only when alternatives were
22
+ * asked for (the API rejects `top_logprobs` without `logprobs: true`). */
23
+ export declare function openAIChatLogprobParams(option: {
24
+ top?: number;
25
+ } | undefined): OpenAIChatLogprobParams;
26
+ type ResponsesOutputPart = {
27
+ type: string;
28
+ logprobs?: OpenAILogprob[];
29
+ };
30
+ type ResponsesOutputItem = {
31
+ type: string;
32
+ content?: ResponsesOutputPart[];
33
+ };
34
+ /** The logprobs of every `output_text` part of a Responses API `output`,
35
+ * in order, as smoltalk's shape. */
36
+ export declare function responsesOutputLogprobs(output: ResponsesOutputItem[]): TokenLogprob[] | undefined;
37
+ export {};
@@ -0,0 +1,52 @@
1
+ /** One entry in smoltalk's shape. `top` is present only when there are
2
+ * alternatives, so an entry without them serializes without the key. */
3
+ function tokenLogprob(token, logprob, top) {
4
+ if (top.length === 0) {
5
+ return { token, logprob };
6
+ }
7
+ return { token, logprob, top };
8
+ }
9
+ /** OpenAI's wire alternative also carries a `bytes` field, which smoltalk
10
+ * drops; this copy keeps only the token and its logprob. */
11
+ function openAIAlternative(entry) {
12
+ return { token: entry.token, logprob: entry.logprob };
13
+ }
14
+ /** OpenAI's per-token entries (chat `choices[].logprobs.content` and
15
+ * Responses `output_text.logprobs`) as smoltalk's shape. Undefined when
16
+ * there are none, so the result field stays absent. */
17
+ export function fromOpenAILogprobs(entries) {
18
+ if (!entries || entries.length === 0) {
19
+ return undefined;
20
+ }
21
+ return entries.map((entry) => tokenLogprob(entry.token, entry.logprob, (entry.top_logprobs ?? []).map(openAIAlternative)));
22
+ }
23
+ /** How many alternatives per token a `logprobs` option asks for, or
24
+ * undefined for none. `top: 0` and a missing `top` both mean none. */
25
+ export function topAlternatives(option) {
26
+ if (option?.top === undefined || option.top <= 0) {
27
+ return undefined;
28
+ }
29
+ return option.top;
30
+ }
31
+ /** The chat API's request parameters for a `logprobs` option: `logprobs: true`
32
+ * whenever the option is set, `top_logprobs` only when alternatives were
33
+ * asked for (the API rejects `top_logprobs` without `logprobs: true`). */
34
+ export function openAIChatLogprobParams(option) {
35
+ if (option === undefined) {
36
+ return {};
37
+ }
38
+ const top = topAlternatives(option);
39
+ if (top === undefined) {
40
+ return { logprobs: true };
41
+ }
42
+ return { logprobs: true, top_logprobs: top };
43
+ }
44
+ /** The logprobs of every `output_text` part of a Responses API `output`,
45
+ * in order, as smoltalk's shape. */
46
+ export function responsesOutputLogprobs(output) {
47
+ const textParts = output
48
+ .filter((item) => item.type === "message")
49
+ .flatMap((item) => item.content ?? [])
50
+ .filter((part) => part.type === "output_text");
51
+ return fromOpenAILogprobs(textParts.flatMap((part) => part.logprobs ?? []));
52
+ }
@@ -10,6 +10,7 @@ import { extractHttpErrorFields } from "../util/httpError.js";
10
10
  import { zodToOpenAITool } from "../util/tool.js";
11
11
  import { responseFormatToJsonSchema } from "../util/jsonSchema.js";
12
12
  import { normalizeOpenAIStopReason } from "../util/stopReason.js";
13
+ import { fromOpenAILogprobs, openAIChatLogprobParams, } from "./logprobs.js";
13
14
  import { Model } from "../model.js";
14
15
  export class SmolOpenAi extends BaseClient {
15
16
  client;
@@ -138,6 +139,7 @@ export class SmolOpenAi extends BaseClient {
138
139
  ...(config.reasoningEffort && {
139
140
  reasoning_effort: config.reasoningEffort,
140
141
  }),
142
+ ...openAIChatLogprobParams(config.logprobs),
141
143
  ...this.maxTokensParam(config),
142
144
  ...sanitizeAttributes(config.rawAttributes),
143
145
  ...this.buildRequestExtras(config),
@@ -217,6 +219,7 @@ export class SmolOpenAi extends BaseClient {
217
219
  const { usage, cost } = this.calculateUsageAndCost(completion.usage, rawResponse);
218
220
  const hostedToolResults = this.parseHostedToolResults(completion, config);
219
221
  const rawStopReason = completion.choices[0]?.finish_reason ?? undefined;
222
+ const logprobs = fromOpenAILogprobs(completion.choices[0]?.logprobs?.content);
220
223
  const result = {
221
224
  output,
222
225
  toolCalls,
@@ -231,6 +234,9 @@ export class SmolOpenAi extends BaseClient {
231
234
  if (hostedToolResults.length > 0) {
232
235
  result.hostedToolResults = hostedToolResults;
233
236
  }
237
+ if (logprobs !== undefined) {
238
+ result.logprobs = logprobs;
239
+ }
234
240
  return success(result);
235
241
  }
236
242
  async *_textStream(config) {
@@ -254,10 +260,15 @@ export class SmolOpenAi extends BaseClient {
254
260
  let usage;
255
261
  let cost;
256
262
  let rawStopReason;
263
+ const logprobEntries = [];
257
264
  for await (const chunk of completion) {
258
265
  const chunkFinish = chunk.choices?.[0]?.finish_reason;
259
266
  if (chunkFinish)
260
267
  rawStopReason = chunkFinish;
268
+ const chunkLogprobs = chunk.choices?.[0]?.logprobs?.content;
269
+ if (chunkLogprobs) {
270
+ logprobEntries.push(...chunkLogprobs);
271
+ }
261
272
  // Extract usage from the final chunk
262
273
  if (chunk.usage) {
263
274
  // Header-based cost (LiteLLM) is unsupported while streaming.
@@ -309,6 +320,7 @@ export class SmolOpenAi extends BaseClient {
309
320
  toolCalls.push(toolCall);
310
321
  yield { type: "tool_call", toolCall };
311
322
  }
323
+ const logprobs = fromOpenAILogprobs(logprobEntries);
312
324
  const result = {
313
325
  output: content || null,
314
326
  toolCalls,
@@ -320,6 +332,9 @@ export class SmolOpenAi extends BaseClient {
320
332
  if (rawStopReason) {
321
333
  result.rawStopReason = rawStopReason;
322
334
  }
335
+ if (logprobs !== undefined) {
336
+ result.logprobs = logprobs;
337
+ }
323
338
  yield { type: "done", result };
324
339
  }
325
340
  }
@@ -7,6 +7,7 @@ import { BaseClient } from "./baseClient.js";
7
7
  import { zodToOpenAIResponsesTool } from "../util/tool.js";
8
8
  import { responseFormatToJsonSchema } from "../util/jsonSchema.js";
9
9
  import { normalizeOpenAIResponsesStopReason } from "../util/stopReason.js";
10
+ import { fromOpenAILogprobs, responsesOutputLogprobs, topAlternatives, } from "./logprobs.js";
10
11
  import { sanitizeAttributes } from "../util/util.js";
11
12
  import { WEB_SEARCH, webSearchResult, applyHostedToolCost } from "../util/hostedTools.js";
12
13
  import { Model } from "../model.js";
@@ -131,6 +132,13 @@ export class SmolOpenAiResponses extends BaseClient {
131
132
  if (config.reasoningEffort) {
132
133
  request.reasoning = { effort: config.reasoningEffort };
133
134
  }
135
+ if (config.logprobs !== undefined) {
136
+ request.include = ["message.output_text.logprobs"];
137
+ const top = topAlternatives(config.logprobs);
138
+ if (top !== undefined) {
139
+ request.top_logprobs = top;
140
+ }
141
+ }
134
142
  Object.assign(request, sanitizeAttributes(config.rawAttributes));
135
143
  return request;
136
144
  }
@@ -196,6 +204,7 @@ export class SmolOpenAiResponses extends BaseClient {
196
204
  const { results: hostedToolResults, cost: finalCost } = applyHostedToolCost(parsed, cost, this.getModel(), this.config.modelData);
197
205
  const incompleteReason = response.incomplete_details?.reason;
198
206
  const rawStopReason = incompleteReason ?? response.status ?? undefined;
207
+ const logprobs = responsesOutputLogprobs(response.output);
199
208
  const result = {
200
209
  output,
201
210
  toolCalls,
@@ -210,6 +219,9 @@ export class SmolOpenAiResponses extends BaseClient {
210
219
  if (hostedToolResults.length > 0) {
211
220
  result.hostedToolResults = hostedToolResults;
212
221
  }
222
+ if (logprobs !== undefined) {
223
+ result.logprobs = logprobs;
224
+ }
213
225
  return success(result);
214
226
  }
215
227
  async *_textStream(config) {
@@ -231,6 +243,7 @@ export class SmolOpenAiResponses extends BaseClient {
231
243
  let usage;
232
244
  let cost;
233
245
  let finalResponse;
246
+ const logprobEntries = [];
234
247
  for await (const event of stream) {
235
248
  if (event.type === "response.completed" ||
236
249
  event.type === "response.incomplete") {
@@ -242,6 +255,10 @@ export class SmolOpenAiResponses extends BaseClient {
242
255
  yield { type: "text", text: event.delta };
243
256
  break;
244
257
  }
258
+ case "response.output_text.done": {
259
+ logprobEntries.push(...(event.logprobs ?? []));
260
+ break;
261
+ }
245
262
  case "response.function_call_arguments.delta": {
246
263
  const existing = functionCalls.get(event.item_id);
247
264
  if (existing) {
@@ -306,6 +323,7 @@ export class SmolOpenAiResponses extends BaseClient {
306
323
  }
307
324
  const incompleteReason = finalResponse?.incomplete_details?.reason;
308
325
  const rawStopReason = incompleteReason ?? finalResponse?.status ?? undefined;
326
+ const logprobs = fromOpenAILogprobs(logprobEntries);
309
327
  const result = {
310
328
  output: content || null,
311
329
  toolCalls,
@@ -317,6 +335,9 @@ export class SmolOpenAiResponses extends BaseClient {
317
335
  if (rawStopReason) {
318
336
  result.rawStopReason = rawStopReason;
319
337
  }
338
+ if (logprobs !== undefined) {
339
+ result.logprobs = logprobs;
340
+ }
320
341
  yield { type: "done", result };
321
342
  }
322
343
  }
package/dist/types.d.ts CHANGED
@@ -108,6 +108,12 @@ export type SmolConfig = {
108
108
  };
109
109
  /** Provider-agnostic reasoning effort level. */
110
110
  reasoningEffort?: "low" | "medium" | "high";
111
+ /** Ask for the probability of each generated token. `top` is how many
112
+ * alternatives to return per token, 0 or absent for none. Honoured by
113
+ * OpenAI (both APIs); other providers ignore it. */
114
+ logprobs?: {
115
+ top?: number;
116
+ };
111
117
  responseFormatOptions?: Partial<{
112
118
  name: string;
113
119
  strict: boolean;
@@ -154,6 +160,9 @@ export type PromptResult = {
154
160
  output: string | null;
155
161
  toolCalls: ToolCall[];
156
162
  thinkingBlocks?: ThinkingBlock[];
163
+ /** Per-token log probabilities, when the call asked for them and the
164
+ * provider returned them. Absent otherwise. */
165
+ logprobs?: TokenLogprob[];
157
166
  usage?: TokenUsage;
158
167
  cost?: CostEstimate;
159
168
  model?: ModelName;
@@ -167,7 +176,7 @@ export type PromptResult = {
167
176
  * with probabilities here. */
168
177
  rawData?: unknown;
169
178
  };
170
- export declare function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }: Partial<PromptResult>): PromptResult;
179
+ export declare function promptResult({ output, toolCalls, thinkingBlocks, logprobs, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }: Partial<PromptResult>): PromptResult;
171
180
  export type StreamChunk = {
172
181
  type: "text";
173
182
  text: string;
@@ -216,3 +225,24 @@ export declare const ThinkingBlockSchema: z.ZodObject<{
216
225
  text: z.ZodString;
217
226
  signature: z.ZodString;
218
227
  }, z.z.core.$strip>;
228
+ /** A token the model could have produced at a position, and the log of
229
+ * its probability. */
230
+ export type TokenAlternative = {
231
+ token: string;
232
+ logprob: number;
233
+ };
234
+ /** One generated token and the log of its probability. `top` holds the
235
+ * most likely alternatives at that position when the call asked for them. */
236
+ export type TokenLogprob = {
237
+ token: string;
238
+ logprob: number;
239
+ top?: TokenAlternative[];
240
+ };
241
+ export declare const TokenLogprobSchema: z.ZodObject<{
242
+ token: z.ZodString;
243
+ logprob: z.ZodNumber;
244
+ top: z.ZodOptional<z.ZodArray<z.ZodObject<{
245
+ token: z.ZodString;
246
+ logprob: z.ZodNumber;
247
+ }, z.z.core.$strip>>>;
248
+ }, z.z.core.$strip>;
package/dist/types.js CHANGED
@@ -4,11 +4,12 @@ import z from "zod";
4
4
  export * from "./types/costEstimate.js";
5
5
  export * from "./types/tokenUsage.js";
6
6
  export * from "./types/stopReason.js";
7
- export function promptResult({ output, toolCalls, thinkingBlocks, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }) {
7
+ export function promptResult({ output, toolCalls, thinkingBlocks, logprobs, usage, cost, model, hostedToolResults, stopReason, rawStopReason, rawData, }) {
8
8
  return {
9
9
  output: output || null,
10
10
  toolCalls: toolCalls || [],
11
11
  thinkingBlocks: thinkingBlocks,
12
+ logprobs,
12
13
  usage,
13
14
  cost,
14
15
  model,
@@ -22,3 +23,12 @@ export const ThinkingBlockSchema = z.object({
22
23
  text: z.string(),
23
24
  signature: z.string(),
24
25
  });
26
+ const TokenAlternativeSchema = z.object({
27
+ token: z.string(),
28
+ logprob: z.number(),
29
+ });
30
+ export const TokenLogprobSchema = z.object({
31
+ token: z.string(),
32
+ logprob: z.number(),
33
+ top: z.array(TokenAlternativeSchema).optional(),
34
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.15.1",
3
+ "version": "0.15.2",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [