@f5-sales-demo/pi-ai 21.10.0 → 21.11.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "@f5-sales-demo/pi-ai",
4
- "version": "21.10.0",
4
+ "version": "21.11.2",
5
5
  "description": "Unified LLM API with automatic model discovery and provider configuration",
6
6
  "homepage": "https://github.com/f5-sales-demo/xcsh",
7
7
  "author": "Can Boluk",
@@ -46,7 +46,7 @@
46
46
  "@anthropic-ai/sdk": "^0.115",
47
47
  "@aws-sdk/client-bedrock-runtime": "^3",
48
48
  "@bufbuild/protobuf": "^2.11",
49
- "@f5-sales-demo/pi-utils": "21.10.0",
49
+ "@f5-sales-demo/pi-utils": "21.11.2",
50
50
  "@google/genai": "^2.15",
51
51
  "@sinclair/typebox": "^0.34",
52
52
  "@smithy/node-http-handler": "^4.4",
@@ -90,155 +90,6 @@ function tryParseNumberString(value: string, expectedTypes: string[]): { value:
90
90
  return { value: parsed, changed: true };
91
91
  }
92
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
93
  /**
243
94
  * Attempts to parse a string as JSON if it looks like a JSON literal and
244
95
  * the parsed result matches one of the expected types.
@@ -285,18 +136,6 @@ function tryParseJsonForTypes(value: string, expectedTypes: string[]): { value:
285
136
  return { value: parsed, changed: true };
286
137
  }
287
138
  } 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
139
  return { value, changed: false };
301
140
  }
302
141
 
@@ -513,6 +352,115 @@ function normalizeOptionalNullsForSchema(schema: unknown, value: unknown): { val
513
352
  return { value: changed ? nextValue : value, changed };
514
353
  }
515
354
 
355
+ const PROTOTYPE_SENSITIVE_KEYS = new Set(["__proto__", "constructor", "prototype"]);
356
+
357
+ /**
358
+ * Strictly decodes JSON strings at schema nodes that explicitly accept an
359
+ * object or array, then continues normalization inside the decoded value.
360
+ * This schema-first pass is not limited by the number or shape of AJV errors.
361
+ */
362
+ function normalizeJsonContainersForSchema(schema: unknown, value: unknown): { value: unknown; changed: boolean } {
363
+ if (schema === null || typeof schema !== "object" || Array.isArray(schema)) {
364
+ return { value, changed: false };
365
+ }
366
+
367
+ const schemaObject = schema as Record<string, unknown>;
368
+ for (const keyword of ["anyOf", "oneOf"] as const) {
369
+ const branches = schemaObject[keyword];
370
+ if (!Array.isArray(branches)) continue;
371
+
372
+ let changedCandidate: { value: unknown; changed: true } | undefined;
373
+ for (const branch of branches) {
374
+ const normalized = normalizeJsonContainersForSchema(branch, value);
375
+ if (!normalized.changed) continue;
376
+ changedCandidate ??= normalized as { value: unknown; changed: true };
377
+ try {
378
+ if (ajv.compile(branch)(normalized.value)) return normalized;
379
+ } catch {
380
+ // The full tool-schema compilation reports invalid schemas consistently.
381
+ }
382
+ }
383
+ if (changedCandidate) return changedCandidate;
384
+ }
385
+
386
+ let normalizedValue = value;
387
+ let changed = false;
388
+ const declaredTypes = normalizeExpectedTypes(schemaObject.type);
389
+ const containerTypes = declaredTypes.filter(type => type === "array" || type === "object");
390
+
391
+ if (typeof normalizedValue === "string" && containerTypes.length > 0) {
392
+ const trimmed = normalizedValue.trim();
393
+ const hasMatchingDelimiter =
394
+ (containerTypes.includes("array") && trimmed.startsWith("[") && trimmed.endsWith("]")) ||
395
+ (containerTypes.includes("object") && trimmed.startsWith("{") && trimmed.endsWith("}"));
396
+ if (hasMatchingDelimiter) {
397
+ try {
398
+ const parsed = JSON.parse(trimmed) as unknown;
399
+ if (matchesExpectedType(parsed, containerTypes)) {
400
+ normalizedValue = parsed;
401
+ changed = true;
402
+ }
403
+ } catch {
404
+ return { value, changed: false };
405
+ }
406
+ }
407
+ }
408
+
409
+ if (Array.isArray(schemaObject.allOf)) {
410
+ for (const branch of schemaObject.allOf) {
411
+ const normalized = normalizeJsonContainersForSchema(branch, normalizedValue);
412
+ if (!normalized.changed) continue;
413
+ normalizedValue = normalized.value;
414
+ changed = true;
415
+ }
416
+ }
417
+
418
+ if (Array.isArray(normalizedValue)) {
419
+ const itemSchema = schemaObject.items;
420
+ if (itemSchema !== null && typeof itemSchema === "object" && !Array.isArray(itemSchema)) {
421
+ let nextValue = normalizedValue;
422
+ for (let index = 0; index < normalizedValue.length; index += 1) {
423
+ const normalized = normalizeJsonContainersForSchema(itemSchema, normalizedValue[index]);
424
+ if (!normalized.changed) continue;
425
+ if (nextValue === normalizedValue) nextValue = [...normalizedValue];
426
+ nextValue[index] = normalized.value;
427
+ changed = true;
428
+ }
429
+ normalizedValue = nextValue;
430
+ }
431
+ return { value: changed ? normalizedValue : value, changed };
432
+ }
433
+
434
+ if (normalizedValue === null || typeof normalizedValue !== "object") {
435
+ return { value: changed ? normalizedValue : value, changed };
436
+ }
437
+
438
+ const properties =
439
+ schemaObject.properties !== null &&
440
+ typeof schemaObject.properties === "object" &&
441
+ !Array.isArray(schemaObject.properties)
442
+ ? (schemaObject.properties as Record<string, unknown>)
443
+ : undefined;
444
+ if (!properties) return { value: changed ? normalizedValue : value, changed };
445
+
446
+ let nextRecord = normalizedValue as Record<string, unknown>;
447
+ for (const [key, propertySchema] of Object.entries(properties)) {
448
+ if (PROTOTYPE_SENSITIVE_KEYS.has(key) || !Object.hasOwn(nextRecord, key)) continue;
449
+ const normalized = normalizeJsonContainersForSchema(propertySchema, nextRecord[key]);
450
+ if (!normalized.changed) continue;
451
+ if (nextRecord === normalizedValue) nextRecord = { ...nextRecord };
452
+ Object.defineProperty(nextRecord, key, {
453
+ configurable: true,
454
+ enumerable: true,
455
+ value: normalized.value,
456
+ writable: true,
457
+ });
458
+ changed = true;
459
+ }
460
+
461
+ return { value: changed ? nextRecord : value, changed };
462
+ }
463
+
516
464
  /**
517
465
  * Attempts to fix type errors by parsing JSON-encoded strings.
518
466
  *
@@ -620,6 +568,11 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
620
568
  // validation would pass (e.g., optional string field where "null" is a valid string).
621
569
  let normalizedArgs: unknown = originalArgs;
622
570
  let changed = false;
571
+ const containerNormalization = normalizeJsonContainersForSchema(tool.parameters, normalizedArgs);
572
+ if (containerNormalization.changed) {
573
+ normalizedArgs = containerNormalization.value;
574
+ changed = true;
575
+ }
623
576
 
624
577
  const initialNormalization = normalizeOptionalNullsForSchema(tool.parameters, normalizedArgs);
625
578
  if (initialNormalization.changed) {
@@ -639,6 +592,11 @@ export function validateToolArguments(tool: Tool, toolCall: ToolCall): ToolCall[
639
592
  normalizedArgs = coercion.value;
640
593
  changed = true;
641
594
 
595
+ const containerNormalization = normalizeJsonContainersForSchema(tool.parameters, normalizedArgs);
596
+ if (containerNormalization.changed) {
597
+ normalizedArgs = containerNormalization.value;
598
+ }
599
+
642
600
  const nullNormalization = normalizeOptionalNullsForSchema(tool.parameters, normalizedArgs);
643
601
  if (nullNormalization.changed) {
644
602
  normalizedArgs = nullNormalization.value;