smoltalk 0.13.1 → 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.
@@ -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
- genConfig.responseJsonSchema = responseFormatToJsonSchema(config.responseFormat);
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.debug("Gemini does not support streaming responses with both tool calls and structured response formats. Response format will be ignored.");
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;
@@ -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;
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "smoltalk",
3
- "version": "0.13.1",
3
+ "version": "0.13.2",
4
4
  "description": "A common interface for LLM APIs",
5
5
  "homepage": "https://github.com/egonSchiele/smoltalk",
6
6
  "files": [