@oh-my-pi/pi-ai 15.1.4 → 15.1.6

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/CHANGELOG.md CHANGED
@@ -2,6 +2,13 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [15.1.6] - 2026-05-19
6
+
7
+ ### Fixed
8
+
9
+ - Fixed `{}` (empty JSON Schema, the wire representation of `z.unknown()`) being passed verbatim to grammar-constrained samplers (llama.cpp, etc.) in `additionalProperties`, `items`, and other schema-valued positions across **every provider** (OpenAI, Anthropic, Google, Ollama, Bedrock, Cursor). Grammar builders treat `{}` as "generate an empty object" rather than "any JSON value", causing open-typed fields (e.g. `extra.title` from `z.record(z.string(), z.unknown())`) to always emit `{}` instead of the intended string/number/etc. `toolWireSchema` now applies a new `normalizeEmptySchemas` pass (exported) to both the Zod and TypeBox/raw-JSON-Schema branches, converting `{}` → `true` (semantically identical per JSON Schema draft 2020-12 §4.3.1) in all schema-valued positions. Strict-mode opt-out is preserved across all providers: OpenAI's `hasUnrepresentableStrictObjectMap` hits the `=== true` branch instead of the `isJsonObject({})` branch (same result); Anthropic's `normalizeAnthropicStrictSchemaNode` opts out via `additionalProperties !== false` (still true for `true`); Google's `normalizeSchemaForGoogle` strips `additionalProperties` regardless (pre-existing). ([#1179](https://github.com/can1357/oh-my-pi/issues/1179))
10
+ - Fixed `pi-ai login <provider>` crashing with `Unknown provider` for providers that only the `auth-storage` `login()` switch knew about (perplexity, alibaba-coding-plan, gitlab-duo, huggingface, opencode-zen/go, lm-studio, ollama, cerebras, fireworks, qianfan, synthetic, venice, litellm, moonshot, together, cloudflare/vercel ai gateways, vllm, qwen-portal, nvidia, xiaomi, and any custom OAuth provider). The CLI now delegates to `SqliteAuthCredentialStore.login()` instead of duplicating a smaller switch, so the auth-broker `omp auth-broker login <provider>` flow works for every registered OAuth provider.
11
+
5
12
  ## [15.1.4] - 2026-05-19
6
13
  ### Changed
7
14
 
@@ -1,2 +1,4 @@
1
1
  export type JsonObject = Record<string, unknown>;
2
2
  export declare function isJsonObject(value: unknown): value is JsonObject;
3
+ /** True when `value` is a plain JSON object with no own enumerable keys. */
4
+ export declare function isJsonObjectEmpty(value: JsonObject): boolean;
@@ -26,11 +26,29 @@ import type { Tool } from "../../types";
26
26
  * impostors do not.
27
27
  */
28
28
  export declare function isZodSchema(value: unknown): value is ZodType;
29
+ /**
30
+ * Normalize `{}` (empty JSON Schema = `z.unknown()` / unconstrained value) to
31
+ * boolean `true` in every schema-valued position. JSON Schema draft 2020-12
32
+ * §4.3.1: `{}` and `true` are semantically equivalent ("any JSON value").
33
+ * Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
34
+ * "generate an empty object" rather than "any JSON value", causing open-typed
35
+ * fields like `extra.title` (from `z.record(z.string(), z.unknown())`) to
36
+ * always emit `{}` instead of the intended string/number/etc. (issue #1179).
37
+ *
38
+ * Mutates in place. Provider-agnostic — applied to every tool wire schema so
39
+ * Anthropic, Google, OpenAI, Ollama, Bedrock, and Cursor all see the
40
+ * normalized form, regardless of whether the source was Zod or TypeBox.
41
+ */
42
+ export declare function normalizeEmptySchemas(node: unknown): void;
29
43
  /** Convert a Zod schema into the JSON Schema shape providers consume. */
30
44
  export declare function zodToWireSchema(schema: ZodType): Record<string, unknown>;
31
45
  /**
32
46
  * Resolve a tool's parameters to a JSON Schema object suitable for sending
33
47
  * over the wire. Zod schemas are converted (and cached); legacy TypeBox / raw
34
48
  * JSON Schema parameters are upgraded to draft 2020-12 (and cached).
49
+ *
50
+ * Both branches finish with `normalizeEmptySchemas` so every provider —
51
+ * OpenAI, Anthropic, Google, Ollama, Bedrock, Cursor — sees `{}` normalized
52
+ * to `true` in schema-valued positions (issue #1179).
35
53
  */
36
54
  export declare function toolWireSchema(tool: Tool): Record<string, unknown>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@oh-my-pi/pi-ai",
4
- "version": "15.1.4",
4
+ "version": "15.1.6",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://omp.sh",
7
7
  "author": "Can Boluk",
@@ -43,7 +43,7 @@
43
43
  "dependencies": {
44
44
  "@anthropic-ai/sdk": "^0.94.0",
45
45
  "@bufbuild/protobuf": "^2.12.0",
46
- "@oh-my-pi/pi-utils": "15.1.4",
46
+ "@oh-my-pi/pi-utils": "15.1.6",
47
47
  "openai": "^6.36.0",
48
48
  "partial-json": "^0.1.7",
49
49
  "zod": "4.4.3"
package/src/cli.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env bun
2
2
  import * as readline from "node:readline";
3
- import { SqliteAuthCredentialStore } from "./auth-storage";
3
+ import { AuthStorage, SqliteAuthCredentialStore } from "./auth-storage";
4
4
  import { getOAuthProviders } from "./utils/oauth";
5
- import type { OAuthCredentials, OAuthProvider } from "./utils/oauth/types";
5
+ import type { OAuthProvider } from "./utils/oauth/types";
6
6
 
7
7
  const PROVIDERS = getOAuthProviders();
8
8
 
@@ -58,289 +58,29 @@ function prompt(rl: readline.Interface, question: string): Promise<string> {
58
58
 
59
59
  async function login(provider: OAuthProvider): Promise<void> {
60
60
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
61
-
62
61
  const promptFn = (msg: string) => prompt(rl, `${msg} `);
63
- const storage = await SqliteAuthCredentialStore.open();
62
+ const store = await SqliteAuthCredentialStore.open();
63
+ const storage = new AuthStorage(store);
64
+ await storage.reload();
64
65
 
65
66
  try {
66
- let credentials: OAuthCredentials;
67
-
68
- switch (provider) {
69
- case "anthropic": {
70
- const { loginAnthropic } = await import("./utils/oauth/anthropic");
71
- credentials = await loginAnthropic({
72
- onAuth(info) {
73
- const { url } = info;
74
- console.log(`\nOpen this URL in your browser:\n${url}\n`);
75
- },
76
- onProgress(message) {
77
- console.log(message);
78
- },
79
- });
80
- break;
81
- }
82
-
83
- case "github-copilot": {
84
- const { loginGitHubCopilot } = await import("./utils/oauth/github-copilot");
85
- credentials = await loginGitHubCopilot({
86
- onAuth(url, instructions) {
87
- console.log(`\nOpen this URL in your browser:\n${url}`);
88
- if (instructions) console.log(instructions);
89
- console.log();
90
- },
91
- async onPrompt(p) {
92
- return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
93
- },
94
- });
95
- break;
96
- }
97
-
98
- case "google-gemini-cli": {
99
- const { loginGeminiCli } = await import("./utils/oauth/google-gemini-cli");
100
- credentials = await loginGeminiCli({
101
- onAuth(info) {
102
- const { url, instructions } = info;
103
- console.log(`\nOpen this URL in your browser:\n${url}`);
104
- if (instructions) console.log(instructions);
105
- console.log();
106
- },
107
- });
108
- break;
109
- }
110
-
111
- case "google-antigravity": {
112
- const { loginAntigravity } = await import("./utils/oauth/google-antigravity");
113
- credentials = await loginAntigravity({
114
- onAuth(info) {
115
- const { url, instructions } = info;
116
- console.log(`\nOpen this URL in your browser:\n${url}`);
117
- if (instructions) console.log(instructions);
118
- console.log();
119
- },
120
- });
121
- break;
122
- }
123
- case "openai-codex": {
124
- const { loginOpenAICodex } = await import("./utils/oauth/openai-codex");
125
- credentials = await loginOpenAICodex({
126
- onAuth(info) {
127
- const { url, instructions } = info;
128
- console.log(`\nOpen this URL in your browser:\n${url}`);
129
- if (instructions) console.log(instructions);
130
- console.log();
131
- },
132
- async onPrompt(p) {
133
- return await promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
134
- },
135
- });
136
- break;
137
- }
138
-
139
- case "kimi-code": {
140
- const { loginKimi } = await import("./utils/oauth/kimi");
141
- credentials = await loginKimi({
142
- onAuth(info) {
143
- const { url, instructions } = info;
144
- console.log(`\nOpen this URL in your browser:\n${url}`);
145
- if (instructions) console.log(instructions);
146
- console.log();
147
- },
148
- });
149
- break;
150
- }
151
- case "kilo": {
152
- const { loginKilo } = await import("./utils/oauth/kilo");
153
- credentials = await loginKilo({
154
- onAuth(info) {
155
- const { url, instructions } = info;
156
- console.log(`\nOpen this URL in your browser:\n${url}`);
157
- if (instructions) console.log(instructions);
158
- console.log();
159
- },
160
- });
161
- break;
162
- }
163
- case "kagi": {
164
- const { loginKagi } = await import("./utils/oauth/kagi");
165
- const apiKey = await loginKagi({
166
- onAuth(info) {
167
- const { url, instructions } = info;
168
- console.log(`\nOpen this URL in your browser:\n${url}`);
169
- if (instructions) console.log(instructions);
170
- console.log();
171
- },
172
- onPrompt(p) {
173
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
174
- },
175
- });
176
- storage.saveApiKey(provider, apiKey);
177
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
178
- return;
179
- }
180
- case "tavily": {
181
- const { loginTavily } = await import("./utils/oauth/tavily");
182
- const apiKey = await loginTavily({
183
- onAuth(info) {
184
- const { url, instructions } = info;
185
- console.log(`\nOpen this URL in your browser:\n${url}`);
186
- if (instructions) console.log(instructions);
187
- console.log();
188
- },
189
- onPrompt(p) {
190
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
191
- },
192
- });
193
- storage.saveApiKey(provider, apiKey);
194
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
195
- return;
196
- }
197
- case "parallel": {
198
- const { loginParallel } = await import("./utils/oauth/parallel");
199
- const apiKey = await loginParallel({
200
- onAuth(info) {
201
- const { url, instructions } = info;
202
- console.log(`\nOpen this URL in your browser:\n${url}`);
203
- if (instructions) console.log(instructions);
204
- console.log();
205
- },
206
- onPrompt(p) {
207
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
208
- },
209
- });
210
- storage.saveApiKey(provider, apiKey);
211
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
212
- return;
213
- }
214
-
215
- case "cursor": {
216
- const { loginCursor } = await import("./utils/oauth/cursor");
217
- credentials = await loginCursor(
218
- url => {
219
- console.log(`\nOpen this URL in your browser:\n${url}\n`);
220
- },
221
- () => {
222
- console.log("Waiting for browser authentication...");
223
- },
224
- );
225
- break;
226
- }
227
-
228
- case "zai": {
229
- const { loginZai } = await import("./utils/oauth/zai");
230
- const apiKey = await loginZai({
231
- onAuth(info) {
232
- const { url, instructions } = info;
233
- console.log(`\nOpen this URL in your browser:\n${url}`);
234
- if (instructions) console.log(instructions);
235
- console.log();
236
- },
237
- onPrompt(p) {
238
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
239
- },
240
- });
241
- storage.saveApiKey(provider, apiKey);
242
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
243
- return;
244
- }
245
-
246
- case "nanogpt": {
247
- const { loginNanoGPT } = await import("./utils/oauth/nanogpt");
248
- const apiKey = await loginNanoGPT({
249
- onAuth(info) {
250
- const { url, instructions } = info;
251
- console.log(`\nOpen this URL in your browser:\n${url}`);
252
- if (instructions) console.log(instructions);
253
- console.log();
254
- },
255
- onPrompt(p) {
256
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
257
- },
258
- });
259
- storage.saveApiKey(provider, apiKey);
260
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
261
- return;
262
- }
263
-
264
- case "zenmux": {
265
- const { loginZenMux } = await import("./utils/oauth/zenmux");
266
- const apiKey = await loginZenMux({
267
- onAuth(info) {
268
- const { url, instructions } = info;
269
- console.log(`\nOpen this URL in your browser:\n${url}`);
270
- if (instructions) console.log(instructions);
271
- console.log();
272
- },
273
- onPrompt(p) {
274
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
275
- },
276
- });
277
- storage.saveApiKey(provider, apiKey);
278
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
279
- return;
280
- }
281
- case "ollama-cloud": {
282
- const { loginOllamaCloud } = await import("./utils/oauth/ollama-cloud");
283
- const apiKey = await loginOllamaCloud({
284
- onAuth(info) {
285
- const { url, instructions } = info;
286
- console.log(`\nOpen this URL in your browser:\n${url}`);
287
- if (instructions) console.log(instructions);
288
- console.log();
289
- },
290
- onPrompt(p) {
291
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
292
- },
293
- });
294
- storage.saveApiKey(provider, apiKey);
295
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
296
- return;
297
- }
298
-
299
- case "minimax-code": {
300
- const { loginMiniMaxCode } = await import("./utils/oauth/minimax-code");
301
- const apiKey = await loginMiniMaxCode({
302
- onAuth(info) {
303
- const { url, instructions } = info;
304
- console.log(`\nOpen this URL in your browser:\n${url}`);
305
- if (instructions) console.log(instructions);
306
- console.log();
307
- },
308
- onPrompt(p) {
309
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
310
- },
311
- });
312
- storage.saveApiKey(provider, apiKey);
313
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
314
- return;
315
- }
316
-
317
- case "minimax-code-cn": {
318
- const { loginMiniMaxCodeCn } = await import("./utils/oauth/minimax-code");
319
- const apiKey = await loginMiniMaxCodeCn({
320
- onAuth(info) {
321
- const { url, instructions } = info;
322
- console.log(`\nOpen this URL in your browser:\n${url}`);
323
- if (instructions) console.log(instructions);
324
- console.log();
325
- },
326
- onPrompt(p) {
327
- return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
328
- },
329
- });
330
- storage.saveApiKey(provider, apiKey);
331
- console.log(`\nAPI key saved to ~/.omp/agent/agent.db`);
332
- return;
333
- }
334
-
335
- default:
336
- throw new Error(`Unknown provider: ${provider}`);
337
- }
338
-
339
- storage.saveOAuth(provider, credentials);
340
-
67
+ await storage.login(provider, {
68
+ onAuth(info) {
69
+ const { url, instructions } = info;
70
+ console.log(`\nOpen this URL in your browser:\n${url}`);
71
+ if (instructions) console.log(instructions);
72
+ console.log();
73
+ },
74
+ onProgress(message) {
75
+ console.log(message);
76
+ },
77
+ onPrompt(p) {
78
+ return promptFn(`${p.message}${p.placeholder ? ` (${p.placeholder})` : ""}:`);
79
+ },
80
+ });
341
81
  console.log(`\nCredentials saved to ~/.omp/agent/agent.db`);
342
82
  } finally {
343
- storage.close();
83
+ store.close();
344
84
  rl.close();
345
85
  }
346
86
  }
@@ -21,7 +21,7 @@ import {
21
21
  import { isValidJsonSchema } from "./meta-validator";
22
22
  import { type DescriptionSpillFormat, spillToDescription } from "./spill";
23
23
  import { enter, epochNext, exit, once, stamp } from "./stamps";
24
- import { isJsonObject, type JsonObject } from "./types";
24
+ import { isJsonObject, isJsonObjectEmpty, type JsonObject } from "./types";
25
25
  import { decontaminateZodInstance } from "./zod-decontaminate";
26
26
 
27
27
  export type ResidualSchemaIncompatibility = "type-array" | "type-null" | "nullable" | "combiners";
@@ -907,6 +907,15 @@ export const normalizeSchemaForOpenAIResponses: (schema: JsonObject) => JsonObje
907
907
  function normalizeOpenAIResponsesSchemaNode(value: unknown, cache: WeakMap<JsonObject, JsonObject>): unknown {
908
908
  if (!isJsonObject(value)) return value;
909
909
 
910
+ // `{}` (empty JSON Schema) ≡ `true` (JSON Schema draft 2020-12 §4.3.1).
911
+ // Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
912
+ // "generate an empty object" rather than "any JSON value" (issue #1179).
913
+ // `toolWireSchema` already runs `normalizeEmptySchemas` upstream, but this
914
+ // guard remains as a safety net for callers that invoke
915
+ // `sanitizeSchemaForOpenAIResponses` directly on a schema that bypassed
916
+ // the wire-schema pipeline (e.g. provider-specific fixtures, debug paths).
917
+ if (isJsonObjectEmpty(value)) return true;
918
+
910
919
  const cached = cache.get(value);
911
920
  if (cached) return cached;
912
921
 
@@ -3,3 +3,9 @@ export type JsonObject = Record<string, unknown>;
3
3
  export function isJsonObject(value: unknown): value is JsonObject {
4
4
  return !!value && typeof value === "object" && !Array.isArray(value);
5
5
  }
6
+
7
+ /** True when `value` is a plain JSON object with no own enumerable keys. */
8
+ export function isJsonObjectEmpty(value: JsonObject): boolean {
9
+ for (const _ in value) return false;
10
+ return true;
11
+ }
@@ -62,16 +62,47 @@ const kJsonWireSchema = Symbol("pi.schema.json.wire");
62
62
  * treat defaulted fields as optional; Zod inverts this and keeps them
63
63
  * required at the input boundary, then materializes the default).
64
64
  * - Strip the noisy safe-integer bounds Zod injects for `z.number().int()`.
65
+ *
66
+ * The empty-schema normalization (`{}` → `true`, see `normalizeEmptySchemas`)
67
+ * runs separately from `toolWireSchema` so both Zod and TypeBox tools get it.
65
68
  */
66
69
  function postProcess(schema: Record<string, unknown>): Record<string, unknown> {
67
70
  delete schema.$schema;
68
71
  walk(schema);
72
+ normalizeEmptySchemas(schema);
69
73
  return schema;
70
74
  }
71
75
 
72
76
  const SAFE_INTEGER_MAX = Number.MAX_SAFE_INTEGER;
73
77
  const SAFE_INTEGER_MIN = Number.MIN_SAFE_INTEGER;
74
78
 
79
+ /** Keys whose values are a single JSON Schema (not an array or map). */
80
+ const SCHEMA_VALUE_KEYS = [
81
+ "additionalProperties",
82
+ "unevaluatedProperties",
83
+ "unevaluatedItems",
84
+ "items",
85
+ "contains",
86
+ "propertyNames",
87
+ "if",
88
+ "then",
89
+ "else",
90
+ "not",
91
+ ] as const;
92
+
93
+ /** Keys whose values are a map of `{ key: Schema }` entries. */
94
+ const SCHEMA_MAP_KEYS = ["properties", "patternProperties", "$defs", "definitions"] as const;
95
+
96
+ /** Keys whose values are an array of schemas. */
97
+ const SCHEMA_ARRAY_KEYS = ["anyOf", "oneOf", "allOf", "prefixItems"] as const;
98
+
99
+ /** True when `val` is a plain empty object `{}`. */
100
+ function isEmptyObject(val: unknown): val is Record<string, never> {
101
+ if (val === null || typeof val !== "object" || Array.isArray(val)) return false;
102
+ for (const _ in val as object) return false;
103
+ return true;
104
+ }
105
+
75
106
  function walk(node: unknown): void {
76
107
  if (Array.isArray(node)) {
77
108
  for (const child of node) walk(child);
@@ -107,6 +138,50 @@ function walk(node: unknown): void {
107
138
  for (const k in obj) walk(obj[k]);
108
139
  }
109
140
 
141
+ /**
142
+ * Normalize `{}` (empty JSON Schema = `z.unknown()` / unconstrained value) to
143
+ * boolean `true` in every schema-valued position. JSON Schema draft 2020-12
144
+ * §4.3.1: `{}` and `true` are semantically equivalent ("any JSON value").
145
+ * Grammar-constrained samplers (llama.cpp, etc.) treat the object form as
146
+ * "generate an empty object" rather than "any JSON value", causing open-typed
147
+ * fields like `extra.title` (from `z.record(z.string(), z.unknown())`) to
148
+ * always emit `{}` instead of the intended string/number/etc. (issue #1179).
149
+ *
150
+ * Mutates in place. Provider-agnostic — applied to every tool wire schema so
151
+ * Anthropic, Google, OpenAI, Ollama, Bedrock, and Cursor all see the
152
+ * normalized form, regardless of whether the source was Zod or TypeBox.
153
+ */
154
+ export function normalizeEmptySchemas(node: unknown): void {
155
+ if (Array.isArray(node)) {
156
+ for (const child of node) normalizeEmptySchemas(child);
157
+ return;
158
+ }
159
+ if (!node || typeof node !== "object") return;
160
+ const obj = node as Record<string, unknown>;
161
+
162
+ for (const key of SCHEMA_VALUE_KEYS) {
163
+ if (Object.hasOwn(obj, key) && isEmptyObject(obj[key])) obj[key] = true;
164
+ }
165
+ for (const mapKey of SCHEMA_MAP_KEYS) {
166
+ const map = obj[mapKey];
167
+ if (map !== null && typeof map === "object" && !Array.isArray(map)) {
168
+ for (const k in map as Record<string, unknown>) {
169
+ if (isEmptyObject((map as Record<string, unknown>)[k])) (map as Record<string, unknown>)[k] = true;
170
+ }
171
+ }
172
+ }
173
+ for (const arrKey of SCHEMA_ARRAY_KEYS) {
174
+ const arr = obj[arrKey];
175
+ if (Array.isArray(arr)) {
176
+ for (let i = 0; i < arr.length; i++) {
177
+ if (isEmptyObject(arr[i])) arr[i] = true;
178
+ }
179
+ }
180
+ }
181
+
182
+ for (const k in obj) normalizeEmptySchemas(obj[k]);
183
+ }
184
+
110
185
  /** Convert a Zod schema into the JSON Schema shape providers consume. */
111
186
  export function zodToWireSchema(schema: ZodType): Record<string, unknown> {
112
187
  return stamp(schema, kZodWireSchema, s => {
@@ -122,13 +197,17 @@ export function zodToWireSchema(schema: ZodType): Record<string, unknown> {
122
197
  * Resolve a tool's parameters to a JSON Schema object suitable for sending
123
198
  * over the wire. Zod schemas are converted (and cached); legacy TypeBox / raw
124
199
  * JSON Schema parameters are upgraded to draft 2020-12 (and cached).
200
+ *
201
+ * Both branches finish with `normalizeEmptySchemas` so every provider —
202
+ * OpenAI, Anthropic, Google, Ollama, Bedrock, Cursor — sees `{}` normalized
203
+ * to `true` in schema-valued positions (issue #1179).
125
204
  */
126
205
  export function toolWireSchema(tool: Tool): Record<string, unknown> {
127
206
  const params: TSchema = tool.parameters;
128
207
  if (isZodSchema(params)) return zodToWireSchema(params);
129
- return stamp(
130
- params as Record<string, unknown>,
131
- kJsonWireSchema,
132
- p => upgradeJsonSchemaTo202012(p) as Record<string, unknown>,
133
- );
208
+ return stamp(params as Record<string, unknown>, kJsonWireSchema, p => {
209
+ const upgraded = upgradeJsonSchemaTo202012(p) as Record<string, unknown>;
210
+ normalizeEmptySchemas(upgraded);
211
+ return upgraded;
212
+ });
134
213
  }