@f5-sales-demo/pi-ai 19.51.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.
Files changed (128) hide show
  1. package/CHANGELOG.md +1997 -0
  2. package/README.md +1160 -0
  3. package/package.json +135 -0
  4. package/src/api-registry.ts +95 -0
  5. package/src/auth-storage.ts +2694 -0
  6. package/src/cli.ts +493 -0
  7. package/src/index.ts +42 -0
  8. package/src/model-cache.ts +97 -0
  9. package/src/model-manager.ts +349 -0
  10. package/src/model-thinking.ts +561 -0
  11. package/src/models.json +49439 -0
  12. package/src/models.json.d.ts +9 -0
  13. package/src/models.ts +56 -0
  14. package/src/prompts/turn-aborted-guidance.md +4 -0
  15. package/src/provider-details.ts +81 -0
  16. package/src/provider-models/descriptors.ts +285 -0
  17. package/src/provider-models/google.ts +90 -0
  18. package/src/provider-models/index.ts +4 -0
  19. package/src/provider-models/openai-compat.ts +2074 -0
  20. package/src/provider-models/special.ts +106 -0
  21. package/src/providers/amazon-bedrock.ts +706 -0
  22. package/src/providers/anthropic.ts +1682 -0
  23. package/src/providers/azure-openai-responses.ts +391 -0
  24. package/src/providers/cursor/gen/agent_pb.ts +15274 -0
  25. package/src/providers/cursor/proto/agent.proto +3526 -0
  26. package/src/providers/cursor/proto/buf.gen.yaml +6 -0
  27. package/src/providers/cursor/proto/buf.yaml +17 -0
  28. package/src/providers/cursor.ts +2218 -0
  29. package/src/providers/github-copilot-headers.ts +140 -0
  30. package/src/providers/gitlab-duo.ts +381 -0
  31. package/src/providers/google-gemini-cli.ts +1133 -0
  32. package/src/providers/google-shared.ts +354 -0
  33. package/src/providers/google-vertex.ts +436 -0
  34. package/src/providers/google.ts +381 -0
  35. package/src/providers/kimi.ts +151 -0
  36. package/src/providers/openai-codex/constants.ts +43 -0
  37. package/src/providers/openai-codex/request-transformer.ts +158 -0
  38. package/src/providers/openai-codex/response-handler.ts +81 -0
  39. package/src/providers/openai-codex-responses.ts +2345 -0
  40. package/src/providers/openai-completions-compat.ts +159 -0
  41. package/src/providers/openai-completions.ts +1290 -0
  42. package/src/providers/openai-responses-shared.ts +452 -0
  43. package/src/providers/openai-responses.ts +519 -0
  44. package/src/providers/register-builtins.ts +329 -0
  45. package/src/providers/synthetic.ts +154 -0
  46. package/src/providers/transform-messages.ts +234 -0
  47. package/src/rate-limit-utils.ts +84 -0
  48. package/src/stream.ts +728 -0
  49. package/src/types.ts +546 -0
  50. package/src/usage/claude.ts +337 -0
  51. package/src/usage/gemini.ts +248 -0
  52. package/src/usage/github-copilot.ts +421 -0
  53. package/src/usage/google-antigravity.ts +200 -0
  54. package/src/usage/kimi.ts +286 -0
  55. package/src/usage/minimax-code.ts +31 -0
  56. package/src/usage/openai-codex.ts +387 -0
  57. package/src/usage/zai.ts +247 -0
  58. package/src/usage.ts +130 -0
  59. package/src/utils/abort.ts +36 -0
  60. package/src/utils/anthropic-auth.ts +293 -0
  61. package/src/utils/discovery/antigravity.ts +261 -0
  62. package/src/utils/discovery/codex.ts +371 -0
  63. package/src/utils/discovery/cursor.ts +306 -0
  64. package/src/utils/discovery/gemini.ts +248 -0
  65. package/src/utils/discovery/index.ts +5 -0
  66. package/src/utils/discovery/openai-compatible.ts +224 -0
  67. package/src/utils/event-stream.ts +209 -0
  68. package/src/utils/http-inspector.ts +165 -0
  69. package/src/utils/idle-iterator.ts +176 -0
  70. package/src/utils/json-parse.ts +28 -0
  71. package/src/utils/oauth/alibaba-coding-plan.ts +59 -0
  72. package/src/utils/oauth/anthropic.ts +134 -0
  73. package/src/utils/oauth/api-key-validation.ts +92 -0
  74. package/src/utils/oauth/callback-server.ts +276 -0
  75. package/src/utils/oauth/cerebras.ts +59 -0
  76. package/src/utils/oauth/cloudflare-ai-gateway.ts +48 -0
  77. package/src/utils/oauth/cursor.ts +157 -0
  78. package/src/utils/oauth/github-copilot.ts +358 -0
  79. package/src/utils/oauth/gitlab-duo.ts +123 -0
  80. package/src/utils/oauth/google-antigravity.ts +275 -0
  81. package/src/utils/oauth/google-gemini-cli.ts +334 -0
  82. package/src/utils/oauth/huggingface.ts +62 -0
  83. package/src/utils/oauth/index.ts +512 -0
  84. package/src/utils/oauth/kagi.ts +47 -0
  85. package/src/utils/oauth/kilo.ts +87 -0
  86. package/src/utils/oauth/kimi.ts +251 -0
  87. package/src/utils/oauth/litellm.ts +81 -0
  88. package/src/utils/oauth/lm-studio.ts +40 -0
  89. package/src/utils/oauth/minimax-code.ts +78 -0
  90. package/src/utils/oauth/moonshot.ts +59 -0
  91. package/src/utils/oauth/nanogpt.ts +51 -0
  92. package/src/utils/oauth/nvidia.ts +70 -0
  93. package/src/utils/oauth/oauth.html +199 -0
  94. package/src/utils/oauth/ollama.ts +47 -0
  95. package/src/utils/oauth/openai-codex.ts +190 -0
  96. package/src/utils/oauth/opencode.ts +49 -0
  97. package/src/utils/oauth/parallel.ts +46 -0
  98. package/src/utils/oauth/perplexity.ts +200 -0
  99. package/src/utils/oauth/pkce.ts +18 -0
  100. package/src/utils/oauth/qianfan.ts +58 -0
  101. package/src/utils/oauth/qwen-portal.ts +60 -0
  102. package/src/utils/oauth/synthetic.ts +60 -0
  103. package/src/utils/oauth/tavily.ts +46 -0
  104. package/src/utils/oauth/together.ts +59 -0
  105. package/src/utils/oauth/types.ts +89 -0
  106. package/src/utils/oauth/venice.ts +59 -0
  107. package/src/utils/oauth/vercel-ai-gateway.ts +47 -0
  108. package/src/utils/oauth/vllm.ts +40 -0
  109. package/src/utils/oauth/xiaomi.ts +88 -0
  110. package/src/utils/oauth/zai.ts +60 -0
  111. package/src/utils/oauth/zenmux.ts +51 -0
  112. package/src/utils/overflow.ts +134 -0
  113. package/src/utils/retry-after.ts +110 -0
  114. package/src/utils/retry.ts +93 -0
  115. package/src/utils/schema/CONSTRAINTS.md +160 -0
  116. package/src/utils/schema/adapt.ts +20 -0
  117. package/src/utils/schema/compatibility.ts +397 -0
  118. package/src/utils/schema/dereference.ts +93 -0
  119. package/src/utils/schema/equality.ts +93 -0
  120. package/src/utils/schema/fields.ts +147 -0
  121. package/src/utils/schema/index.ts +9 -0
  122. package/src/utils/schema/normalize-cca.ts +479 -0
  123. package/src/utils/schema/sanitize-google.ts +212 -0
  124. package/src/utils/schema/strict-mode.ts +385 -0
  125. package/src/utils/schema/types.ts +5 -0
  126. package/src/utils/tool-choice.ts +81 -0
  127. package/src/utils/validation.ts +664 -0
  128. package/src/utils.ts +147 -0
@@ -0,0 +1,664 @@
1
+ import { structuredCloneJSON } from "@f5xc-salesdemos/pi-utils";
2
+ import Ajv from "ajv";
3
+ import addFormats from "ajv-formats";
4
+ import type { Tool, ToolCall } from "../types";
5
+
6
+ // ============================================================================
7
+ // Type Coercion Utilities
8
+ // ============================================================================
9
+ //
10
+ // LLMs sometimes produce tool arguments where a value that should be a number,
11
+ // boolean, array, or object is instead passed as a JSON-encoded string. For
12
+ // example, an array parameter might arrive as `"[1, 2, 3]"` instead of `[1, 2, 3]`.
13
+ //
14
+ // Rather than rejecting these outright, we attempt automatic coercion:
15
+ // 1. AJV validates the arguments and reports type errors
16
+ // 2. For each type error where the actual value is a string, we check if
17
+ // parsing it as JSON yields a value matching the expected type
18
+ // 3. If so, we replace the string with the parsed value and re-validate
19
+ //
20
+ // This is intentionally conservative: we only parse strings that look like
21
+ // valid JSON literals (objects, arrays, booleans, null, numbers) and only
22
+ // accept the result if it matches the schema's expected type.
23
+ // ============================================================================
24
+
25
+ /** Regex matching valid JSON number literals (integers, decimals, scientific notation) */
26
+ const JSON_NUMBER_PATTERN = /^[+-]?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
27
+
28
+ /** Regex matching numeric strings (allows leading zeros) */
29
+ const NUMERIC_STRING_PATTERN = /^[+-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
30
+
31
+ /**
32
+ * Normalizes AJV's `params.type` into a consistent string array.
33
+ * AJV may report the expected type as a single string or an array of strings
34
+ * (for union types like `["string", "null"]`).
35
+ */
36
+ function normalizeExpectedTypes(typeParam: unknown): string[] {
37
+ if (typeof typeParam === "string") return [typeParam];
38
+ if (Array.isArray(typeParam)) {
39
+ return typeParam.filter((entry): entry is string => typeof entry === "string");
40
+ }
41
+ return [];
42
+ }
43
+
44
+ /**
45
+ * Checks if a value matches any of the expected JSON Schema types.
46
+ * Used to verify that a parsed JSON value is actually what the schema wants.
47
+ */
48
+ function matchesExpectedType(value: unknown, expectedTypes: string[]): boolean {
49
+ return expectedTypes.some(type => {
50
+ switch (type) {
51
+ case "string":
52
+ return typeof value === "string";
53
+ case "number":
54
+ return typeof value === "number" && Number.isFinite(value);
55
+ case "integer":
56
+ return typeof value === "number" && Number.isInteger(value);
57
+ case "boolean":
58
+ return typeof value === "boolean";
59
+ case "null":
60
+ return value === null;
61
+ case "array":
62
+ return Array.isArray(value);
63
+ case "object":
64
+ return value !== null && typeof value === "object" && !Array.isArray(value);
65
+ default:
66
+ return false;
67
+ }
68
+ });
69
+ }
70
+
71
+ function tryParseNumberString(value: string, expectedTypes: string[]): { value: unknown; changed: boolean } {
72
+ if (!expectedTypes.includes("number") && !expectedTypes.includes("integer")) {
73
+ return { value, changed: false };
74
+ }
75
+
76
+ const trimmed = value.trim();
77
+ if (!trimmed || !NUMERIC_STRING_PATTERN.test(trimmed)) {
78
+ return { value, changed: false };
79
+ }
80
+
81
+ const parsed = Number(trimmed);
82
+ if (!Number.isFinite(parsed)) {
83
+ return { value, changed: false };
84
+ }
85
+
86
+ if (!matchesExpectedType(parsed, expectedTypes)) {
87
+ return { value, changed: false };
88
+ }
89
+
90
+ return { value: parsed, changed: true };
91
+ }
92
+
93
+ function tryParseLeadingJsonContainer(value: string): unknown | undefined {
94
+ const firstChar = value[0];
95
+ const closingChar = firstChar === "{" ? "}" : firstChar === "[" ? "]" : undefined;
96
+ if (!closingChar) return undefined;
97
+
98
+ let depth = 0;
99
+ let inString = false;
100
+ let escaped = false;
101
+
102
+ for (let index = 0; index < value.length; index += 1) {
103
+ const char = value[index];
104
+
105
+ if (inString) {
106
+ if (escaped) {
107
+ escaped = false;
108
+ continue;
109
+ }
110
+ if (char === "\\") {
111
+ escaped = true;
112
+ continue;
113
+ }
114
+ if (char === '"') inString = false;
115
+ continue;
116
+ }
117
+
118
+ if (char === '"') {
119
+ inString = true;
120
+ continue;
121
+ }
122
+
123
+ if (char === firstChar) {
124
+ depth += 1;
125
+ continue;
126
+ }
127
+
128
+ if (char !== closingChar) continue;
129
+ depth -= 1;
130
+ if (depth !== 0) continue;
131
+
132
+ const prefix = value.slice(0, index + 1);
133
+ try {
134
+ return JSON.parse(prefix) as unknown;
135
+ } catch {
136
+ // LLMs sometimes emit literal `\n` or `\t` between JSON tokens
137
+ // (e.g. `[{...}\n]`). Convert these to real whitespace and retry.
138
+ const cleaned = cleanLiteralEscapes(prefix);
139
+ if (cleaned !== prefix) {
140
+ try {
141
+ return JSON.parse(cleaned) as unknown;
142
+ } catch {}
143
+ }
144
+ // Also try single-char healing on the extracted prefix.
145
+ return tryHealMalformedJson(prefix);
146
+ }
147
+ }
148
+
149
+ return undefined;
150
+ }
151
+
152
+ /**
153
+ * Replace literal `\n`, `\t`, `\r` sequences that appear OUTSIDE of JSON
154
+ * strings with actual whitespace. LLMs sometimes produce these when they
155
+ * confuse the tool-call encoding with the content encoding.
156
+ */
157
+ function cleanLiteralEscapes(value: string): string {
158
+ let result = "";
159
+ let inString = false;
160
+ let i = 0;
161
+ while (i < value.length) {
162
+ const ch = value[i];
163
+ if (inString) {
164
+ if (ch === "\\" && i + 1 < value.length) {
165
+ result += ch + value[i + 1];
166
+ i += 2;
167
+ continue;
168
+ }
169
+ if (ch === '"') inString = false;
170
+ result += ch;
171
+ i += 1;
172
+ continue;
173
+ }
174
+ if (ch === '"') {
175
+ inString = true;
176
+ result += ch;
177
+ i += 1;
178
+ continue;
179
+ }
180
+ // Outside a string: replace literal \n, \t, \r with whitespace
181
+ if (ch === "\\" && i + 1 < value.length) {
182
+ const next = value[i + 1];
183
+ if (next === "n" || next === "t" || next === "r") {
184
+ result += " ";
185
+ i += 2;
186
+ continue;
187
+ }
188
+ }
189
+ result += ch;
190
+ i += 1;
191
+ }
192
+ return result;
193
+ }
194
+
195
+ /** Maximum single-character edits to attempt when healing malformed JSON. */
196
+ const MAX_HEAL_DISTANCE = 3;
197
+ const BRACKET_CHARS = ["[", "]", "{", "}"] as const;
198
+
199
+ /**
200
+ * Attempts to heal near-valid JSON by applying single-character edits near the
201
+ * end of the string. LLMs (especially smaller ones) sometimes produce JSON with
202
+ * a single misplaced, extra, or wrong bracket at the end — e.g. `"}]"` becomes
203
+ * `"]}"` or gets an extra `}` appended. This function tries:
204
+ * 1. Removing a single character from the last few positions
205
+ * 2. Replacing a single character in the last few positions with each bracket type
206
+ *
207
+ * Returns the parsed value on success, undefined on failure.
208
+ */
209
+ function tryHealMalformedJson(value: string): unknown | undefined {
210
+ // Verify it actually fails to parse
211
+ try {
212
+ return JSON.parse(value) as unknown;
213
+ } catch {}
214
+
215
+ // Only attempt edits within the last few characters — the error is always
216
+ // a bracket issue at the tail for the class of LLM mistakes this targets.
217
+ const tailStart = Math.max(0, value.length - (MAX_HEAL_DISTANCE * 2 + 1));
218
+
219
+ // Strategy 1: remove a single character from the tail
220
+ for (let i = tailStart; i < value.length; i += 1) {
221
+ const candidate = value.slice(0, i) + value.slice(i + 1);
222
+ try {
223
+ return JSON.parse(candidate) as unknown;
224
+ } catch {}
225
+ }
226
+
227
+ // Strategy 2: replace a single character in the tail with each bracket type
228
+ for (let i = tailStart; i < value.length; i += 1) {
229
+ const original = value[i];
230
+ for (const replacement of BRACKET_CHARS) {
231
+ if (replacement === original) continue;
232
+ const candidate = value.slice(0, i) + replacement + value.slice(i + 1);
233
+ try {
234
+ return JSON.parse(candidate) as unknown;
235
+ } catch {}
236
+ }
237
+ }
238
+
239
+ return undefined;
240
+ }
241
+
242
+ /**
243
+ * Attempts to parse a string as JSON if it looks like a JSON literal and
244
+ * the parsed result matches one of the expected types.
245
+ *
246
+ * Only attempts parsing for strings that syntactically look like JSON:
247
+ * - Objects: `{...}`
248
+ * - Arrays: `[...]`
249
+ * - Literals: `true`, `false`, `null`, or numeric strings
250
+ *
251
+ * Returns `{ changed: true }` only if parsing succeeded AND the result
252
+ * matches an expected type. This prevents false positives like parsing
253
+ * the string `"123"` when the schema actually wants a string.
254
+ */
255
+ function tryParseJsonForTypes(value: string, expectedTypes: string[]): { value: unknown; changed: boolean } {
256
+ const trimmed = value.trim();
257
+ if (!trimmed) return { value, changed: false };
258
+
259
+ const numberCoercion = tryParseNumberString(trimmed, expectedTypes);
260
+ if (numberCoercion.changed) {
261
+ return numberCoercion;
262
+ }
263
+
264
+ // Quick syntactic checks to avoid unnecessary parse attempts
265
+ const looksJsonObject = trimmed.startsWith("{");
266
+ const looksJsonArray = trimmed.startsWith("[");
267
+ const looksJsonLiteral =
268
+ trimmed === "true" || trimmed === "false" || trimmed === "null" || JSON_NUMBER_PATTERN.test(trimmed);
269
+
270
+ if (!looksJsonObject && !looksJsonArray && !looksJsonLiteral) {
271
+ return { value, changed: false };
272
+ }
273
+
274
+ try {
275
+ const parsed = JSON.parse(trimmed) as unknown;
276
+ // If the string was "null", we parsed it to actual null.
277
+ // Accept this even if null isn't in expectedTypes - the LLM meant "no value".
278
+ // normalizeOptionalNullsForSchema will strip it from optional fields, and
279
+ // AJV will correctly error on required fields.
280
+ if (parsed === null && trimmed === "null") {
281
+ return { value: null, changed: true };
282
+ }
283
+ // For non-null values, only accept if the parsed type matches what the schema expects
284
+ if (matchesExpectedType(parsed, expectedTypes)) {
285
+ return { value: parsed, changed: true };
286
+ }
287
+ } catch {
288
+ if (looksJsonObject || looksJsonArray) {
289
+ // Try extracting a valid JSON prefix (handles trailing junk after balanced container)
290
+ const leading = tryParseLeadingJsonContainer(trimmed);
291
+ if (leading !== undefined && matchesExpectedType(leading, expectedTypes)) {
292
+ return { value: leading, changed: true };
293
+ }
294
+ // Try healing single-character bracket errors near the end of the string
295
+ const healed = tryHealMalformedJson(trimmed);
296
+ if (healed !== undefined && matchesExpectedType(healed, expectedTypes)) {
297
+ return { value: healed, changed: true };
298
+ }
299
+ }
300
+ return { value, changed: false };
301
+ }
302
+
303
+ return { value, changed: false };
304
+ }
305
+
306
+ // ============================================================================
307
+ // JSON Pointer Utilities (RFC 6901)
308
+ // ============================================================================
309
+ //
310
+ // AJV reports error locations using JSON Pointer syntax (e.g., `/foo/0/bar`).
311
+ // These utilities allow reading and writing values at those paths.
312
+ // ============================================================================
313
+
314
+ /**
315
+ * Decodes a JSON Pointer string into path segments.
316
+ * Handles RFC 6901 escape sequences: ~1 -> /, ~0 -> ~
317
+ */
318
+ function decodeJsonPointer(pointer: string): string[] {
319
+ if (!pointer) return [];
320
+ return pointer
321
+ .split("/")
322
+ .slice(1) // Remove leading empty segment from initial "/"
323
+ .map(segment => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
324
+ }
325
+
326
+ /**
327
+ * Retrieves a value from a nested object/array structure using a JSON Pointer.
328
+ * Returns undefined if the path doesn't exist or traversal fails.
329
+ */
330
+ function getValueAtPointer(root: unknown, pointer: string): unknown {
331
+ if (!pointer) return root;
332
+ const segments = decodeJsonPointer(pointer);
333
+ let current: unknown = root;
334
+
335
+ for (const segment of segments) {
336
+ if (current === null || current === undefined) return undefined;
337
+ if (Array.isArray(current)) {
338
+ const index = Number(segment);
339
+ if (!Number.isInteger(index)) return undefined;
340
+ current = current[index];
341
+ continue;
342
+ }
343
+ if (typeof current !== "object") return undefined;
344
+ current = (current as Record<string, unknown>)[segment];
345
+ }
346
+
347
+ return current;
348
+ }
349
+
350
+ /**
351
+ * Sets a value in a nested object/array structure using a JSON Pointer.
352
+ * Mutates the structure in-place. Returns the root (possibly unchanged if
353
+ * the path was invalid).
354
+ */
355
+ function setValueAtPointer(root: unknown, pointer: string, value: unknown): unknown {
356
+ if (!pointer) return value;
357
+ const segments = decodeJsonPointer(pointer);
358
+ let current: unknown = root;
359
+
360
+ // Navigate to the parent of the target location
361
+ for (let index = 0; index < segments.length - 1; index += 1) {
362
+ const segment = segments[index];
363
+ if (current === null || current === undefined) return root;
364
+ if (Array.isArray(current)) {
365
+ const arrayIndex = Number(segment);
366
+ if (!Number.isInteger(arrayIndex)) return root;
367
+ current = current[arrayIndex];
368
+ continue;
369
+ }
370
+ if (typeof current !== "object") return root;
371
+ current = (current as Record<string, unknown>)[segment];
372
+ }
373
+
374
+ // Set the value at the final segment
375
+ const lastSegment = segments[segments.length - 1];
376
+ if (Array.isArray(current)) {
377
+ const arrayIndex = Number(lastSegment);
378
+ if (!Number.isInteger(arrayIndex)) return root;
379
+ current[arrayIndex] = value;
380
+ return root;
381
+ }
382
+
383
+ if (typeof current !== "object" || current === null) return root;
384
+ (current as Record<string, unknown>)[lastSegment] = value;
385
+ return root;
386
+ }
387
+
388
+ function normalizeOptionalNullsForSchema(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
389
+ if (value === null || value === undefined) return { value, changed: false };
390
+ if (schema === null || typeof schema !== "object") return { value, changed: false };
391
+
392
+ const schemaObject = schema as Record<string, unknown>;
393
+
394
+ const normalizeAnyOfLike = (keyword: "anyOf" | "oneOf"): { value: unknown; changed: boolean } => {
395
+ const branches = schemaObject[keyword];
396
+ if (!Array.isArray(branches)) return { value, changed: false };
397
+
398
+ let changedCandidate: { value: unknown; changed: true } | null = null;
399
+
400
+ for (const branch of branches) {
401
+ const normalized = normalizeOptionalNullsForSchema(branch, value);
402
+ if (!normalized.changed) continue;
403
+
404
+ try {
405
+ const validateBranch = ajv.compile(branch);
406
+ if (validateBranch(normalized.value)) {
407
+ return normalized;
408
+ }
409
+ } catch {
410
+ // Ignore branch-level compilation/validation errors and keep scanning.
411
+ }
412
+
413
+ if (!changedCandidate) {
414
+ changedCandidate = { value: normalized.value, changed: true };
415
+ }
416
+ }
417
+
418
+ return changedCandidate ?? { value, changed: false };
419
+ };
420
+
421
+ const anyOfNormalization = normalizeAnyOfLike("anyOf");
422
+ if (anyOfNormalization.changed) return anyOfNormalization;
423
+
424
+ const oneOfNormalization = normalizeAnyOfLike("oneOf");
425
+ if (oneOfNormalization.changed) return oneOfNormalization;
426
+
427
+ if (Array.isArray(schemaObject.allOf)) {
428
+ let changed = false;
429
+ let nextValue: unknown = value;
430
+ for (const branch of schemaObject.allOf) {
431
+ const normalized = normalizeOptionalNullsForSchema(branch, nextValue);
432
+ if (!normalized.changed) continue;
433
+ nextValue = normalized.value;
434
+ changed = true;
435
+ }
436
+ if (changed) return { value: nextValue, changed: true };
437
+ }
438
+
439
+ if (Array.isArray(value)) {
440
+ const itemSchema = schemaObject.items;
441
+ if (itemSchema === null || typeof itemSchema !== "object" || Array.isArray(itemSchema)) {
442
+ return { value, changed: false };
443
+ }
444
+
445
+ let changed = false;
446
+ let nextValue = value;
447
+ for (let i = 0; i < value.length; i += 1) {
448
+ const normalized = normalizeOptionalNullsForSchema(itemSchema, value[i]);
449
+ if (!normalized.changed) continue;
450
+ if (!changed) {
451
+ nextValue = [...value];
452
+ changed = true;
453
+ }
454
+ nextValue[i] = normalized.value;
455
+ }
456
+ return { value: changed ? nextValue : value, changed };
457
+ }
458
+
459
+ // Coerce string → number/integer when the schema branch declares those types.
460
+ // This fixes anyOf:[{type:"number"},{type:"null"}] (i.e. Optional<number>) where
461
+ // AJV reports an "anyOf" error rather than a "type" error, bypassing
462
+ // coerceArgsFromErrors which only handles keyword:"type" errors.
463
+ if ((schemaObject.type === "number" || schemaObject.type === "integer") && typeof value === "string") {
464
+ return tryParseNumberString(value, [schemaObject.type as string]);
465
+ }
466
+
467
+ if (schemaObject.type !== "object") return { value, changed: false };
468
+ if (typeof value !== "object" || value === null) return { value, changed: false };
469
+ if (Array.isArray(value)) return { value, changed: false };
470
+ if (schemaObject.properties === null || typeof schemaObject.properties !== "object") {
471
+ return { value, changed: false };
472
+ }
473
+
474
+ const properties = schemaObject.properties as Record<string, unknown>;
475
+ const required = new Set(Array.isArray(schemaObject.required) ? (schemaObject.required as string[]) : []);
476
+
477
+ let changed = false;
478
+ let nextValue = value as Record<string, unknown>;
479
+
480
+ for (const [key, propertySchema] of Object.entries(properties)) {
481
+ if (!(key in nextValue)) continue;
482
+ const currentValue = nextValue[key];
483
+
484
+ // Strip null and the string "null" from optional fields.
485
+ // The LLM sometimes outputs string "null" to mean "no value".
486
+ if ((currentValue === null || currentValue === "null") && !required.has(key)) {
487
+ if (!changed) {
488
+ nextValue = { ...nextValue };
489
+ changed = true;
490
+ }
491
+ delete nextValue[key];
492
+ continue;
493
+ }
494
+ const normalized = normalizeOptionalNullsForSchema(propertySchema, currentValue);
495
+ if (!normalized.changed) continue;
496
+
497
+ if (!changed) {
498
+ nextValue = { ...nextValue };
499
+ changed = true;
500
+ }
501
+ nextValue[key] = normalized.value;
502
+ }
503
+
504
+ return { value: changed ? nextValue : value, changed };
505
+ }
506
+
507
+ /**
508
+ * Attempts to fix type errors by parsing JSON-encoded strings.
509
+ *
510
+ * When AJV reports type errors, this function checks if the offending values
511
+ * are strings that contain valid JSON matching the expected type. If so, it
512
+ * returns a new args object with those strings replaced by their parsed values.
513
+ *
514
+ * The function is designed to be safe and conservative:
515
+ * - Only processes "type" errors (not format, pattern, etc.)
516
+ * - Only attempts coercion on string values
517
+ * - Only accepts parsed results that match the expected type
518
+ * - Clones the args object before mutation (copy-on-write)
519
+ */
520
+ function coerceArgsFromErrors(
521
+ args: unknown,
522
+ errors: Array<{ keyword?: string; instancePath?: string; params?: { type?: unknown } }> | null | undefined,
523
+ ): { value: unknown; changed: boolean } {
524
+ if (!errors || errors.length === 0) return { value: args, changed: false };
525
+
526
+ let changed = false;
527
+ let nextArgs: unknown = args;
528
+
529
+ for (const error of errors) {
530
+ // Only handle type mismatch errors
531
+ if (error.keyword !== "type") continue;
532
+
533
+ const instancePath = error.instancePath ?? "";
534
+ const expectedTypes = normalizeExpectedTypes(error.params?.type);
535
+ if (expectedTypes.length === 0) continue;
536
+
537
+ // Get the current value at the error location
538
+ const currentValue = getValueAtPointer(nextArgs, instancePath);
539
+ if (typeof currentValue !== "string") continue;
540
+
541
+ // Try to parse the string as JSON
542
+ const result = tryParseJsonForTypes(currentValue, expectedTypes);
543
+ if (!result.changed) continue;
544
+
545
+ // Clone on first modification (copy-on-write)
546
+ if (!changed) {
547
+ nextArgs = structuredCloneJSON(nextArgs);
548
+ changed = true;
549
+ }
550
+ nextArgs = setValueAtPointer(nextArgs, instancePath, result.value);
551
+ }
552
+
553
+ return { value: changed ? nextArgs : args, changed };
554
+ }
555
+
556
+ // Create a singleton AJV instance with formats (only if not in browser extension)
557
+ // AJV requires 'unsafe-eval' CSP which is not allowed in Manifest V3
558
+ //
559
+ // Silent logger: MCP servers may declare non-standard format keywords (e.g. "uint")
560
+ // which cause Ajv to emit console.warn() with strict:false — corrupting TUI output.
561
+ const ajv = new Ajv({
562
+ allErrors: true,
563
+ strict: false,
564
+ logger: false,
565
+ });
566
+ addFormats(ajv);
567
+
568
+ // Cache compiled validators by schema object identity to avoid
569
+ // re-compiling the same tool schema on every call.
570
+ const compiledSchemaCache = new WeakMap<object, import("ajv").ValidateFunction>();
571
+ function compileSchema(schema: object): import("ajv").ValidateFunction {
572
+ let validate = compiledSchemaCache.get(schema);
573
+ if (!validate) {
574
+ validate = ajv.compile(schema);
575
+ compiledSchemaCache.set(schema, validate);
576
+ }
577
+ return validate;
578
+ }
579
+
580
+ const MAX_TYPE_COERCION_PASSES = 5;
581
+
582
+ /**
583
+ * Finds a tool by name and validates the tool call arguments against its TypeBox schema
584
+ * @param tools Array of tool definitions
585
+ * @param toolCall The tool call from the LLM
586
+ * @returns The validated arguments
587
+ * @throws Error if tool is not found or validation fails
588
+ */
589
+ export function validateToolCall(tools: Tool[], toolCall: ToolCall): ToolCall["arguments"] {
590
+ const tool = tools.find(t => t.name === toolCall.name);
591
+ if (!tool) {
592
+ throw new Error(`Tool "${toolCall.name}" not found`);
593
+ }
594
+ return validateToolArguments(tool, toolCall);
595
+ }
596
+
597
+ /**
598
+ * Validates tool call arguments against the tool's TypeBox schema
599
+ * @param tool The tool definition with TypeBox schema
600
+ * @param toolCall The tool call from the LLM
601
+ * @returns The validated arguments
602
+ * @throws Error with formatted message if validation fails
603
+ */
604
+ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall["arguments"] {
605
+ const originalArgs = toolCall.arguments;
606
+
607
+ const validate = compileSchema(tool.parameters);
608
+
609
+ // Always normalize first - strip null and string "null" from optional fields.
610
+ // This handles LLM outputting string "null" to mean "no value" even when
611
+ // validation would pass (e.g., optional string field where "null" is a valid string).
612
+ let normalizedArgs: unknown = originalArgs;
613
+ let changed = false;
614
+
615
+ const initialNormalization = normalizeOptionalNullsForSchema(tool.parameters, normalizedArgs);
616
+ if (initialNormalization.changed) {
617
+ normalizedArgs = initialNormalization.value;
618
+ changed = true;
619
+ }
620
+
621
+ // Validate after normalization
622
+ if (validate(normalizedArgs)) {
623
+ return normalizedArgs as ToolCall["arguments"];
624
+ }
625
+
626
+ for (let pass = 0; pass < MAX_TYPE_COERCION_PASSES; pass += 1) {
627
+ const coercion = coerceArgsFromErrors(normalizedArgs, validate.errors);
628
+ if (!coercion.changed) break;
629
+
630
+ normalizedArgs = coercion.value;
631
+ changed = true;
632
+
633
+ const nullNormalization = normalizeOptionalNullsForSchema(tool.parameters, normalizedArgs);
634
+ if (nullNormalization.changed) {
635
+ normalizedArgs = nullNormalization.value;
636
+ }
637
+
638
+ if (validate(normalizedArgs)) {
639
+ return normalizedArgs as ToolCall["arguments"];
640
+ }
641
+ }
642
+
643
+ // Format validation errors nicely
644
+ const errors =
645
+ validate.errors
646
+ ?.map((err: any) => {
647
+ const path = err.instancePath ? err.instancePath.substring(1) : err.params.missingProperty || "root";
648
+ return ` - ${path}: ${err.message}`;
649
+ })
650
+ .join("\n") || "Unknown validation error";
651
+
652
+ const receivedArgs = changed
653
+ ? {
654
+ original: originalArgs,
655
+ normalized: normalizedArgs,
656
+ }
657
+ : originalArgs;
658
+
659
+ const errorMessage = `Validation failed for tool "${
660
+ toolCall.name
661
+ }":\n${errors}\n\nReceived arguments:\n${JSON.stringify(receivedArgs, null, 2)}`;
662
+
663
+ throw new Error(errorMessage);
664
+ }