@jaypie/llm 1.2.40 → 1.2.41

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.
@@ -34,6 +34,12 @@ export declare class OperateLoop {
34
34
  private createContext;
35
35
  private buildInitialRequest;
36
36
  private executeOneTurn;
37
+ /**
38
+ * Backfill declared array fields when a `format` is supplied. A declared
39
+ * `format` is a schema contract: an empty array field should surface as `[]`
40
+ * rather than be dropped by a provider/model that omits empty lists.
41
+ */
42
+ private applyFormatArrayDefaults;
37
43
  /**
38
44
  * Sync the current input state from the updated provider request.
39
45
  * This is necessary because appendToolResult modifies the provider-specific request,
@@ -0,0 +1,17 @@
1
+ import { z } from "zod/v4";
2
+ import { JsonObject, NaturalSchema } from "@jaypie/types";
3
+ type Format = JsonObject | NaturalSchema | z.ZodType;
4
+ /**
5
+ * Ensure every array field declared in `format` is present in `content` as an
6
+ * array. A declared `format` is a schema contract: an empty list should surface
7
+ * as `[]`, not be dropped from the response. Some providers/models omit empty
8
+ * array fields entirely, leaving consumers to read `.length` on `undefined`.
9
+ *
10
+ * Only mutates a (cloned) structured object; strings and non-objects pass
11
+ * through untouched.
12
+ */
13
+ export declare function fillFormatArrays({ content, format, }: {
14
+ content: JsonObject;
15
+ format: Format;
16
+ }): JsonObject;
17
+ export {};
@@ -1,5 +1,6 @@
1
1
  export * from "./determineModelProvider.js";
2
2
  export * from "./extractReasoning.js";
3
+ export * from "./fillFormatArrays.js";
3
4
  export * from "./formatOperateInput.js";
4
5
  export * from "./formatOperateMessage.js";
5
6
  export * from "./jsonSchemaToOpenApi3.js";
package/dist/esm/index.js CHANGED
@@ -572,6 +572,154 @@ function extractReasoning(history) {
572
572
  return reasoningTexts;
573
573
  }
574
574
 
575
+ function naturalZodSchema(definition) {
576
+ if (Array.isArray(definition)) {
577
+ if (definition.length === 0) {
578
+ // Handle empty array - accept any[]
579
+ return z.array(z.any());
580
+ }
581
+ else if (definition.length === 1) {
582
+ // Handle array types
583
+ const itemType = definition[0];
584
+ switch (itemType) {
585
+ case String:
586
+ return z.array(z.string());
587
+ case Number:
588
+ return z.array(z.number());
589
+ case Boolean:
590
+ return z.array(z.boolean());
591
+ default:
592
+ if (typeof itemType === "object") {
593
+ // Handle array of objects
594
+ return z.array(naturalZodSchema(itemType));
595
+ }
596
+ // Handle enum arrays
597
+ return z.enum(definition);
598
+ }
599
+ }
600
+ else {
601
+ // Handle enum arrays
602
+ return z.enum(definition);
603
+ }
604
+ }
605
+ else if (definition && typeof definition === "object") {
606
+ if (Object.keys(definition).length === 0) {
607
+ // Handle empty object - accept any key-value pairs
608
+ return z.record(z.string(), z.any());
609
+ }
610
+ else {
611
+ // Handle object with properties
612
+ const schemaShape = {};
613
+ for (const [key, value] of Object.entries(definition)) {
614
+ schemaShape[key] = naturalZodSchema(value);
615
+ }
616
+ return z.object(schemaShape);
617
+ }
618
+ }
619
+ else {
620
+ switch (definition) {
621
+ case String:
622
+ return z.string();
623
+ case Number:
624
+ return z.number();
625
+ case Boolean:
626
+ return z.boolean();
627
+ case Object:
628
+ return z.record(z.string(), z.any());
629
+ case Array:
630
+ return z.array(z.any());
631
+ default:
632
+ throw new Error(`Unsupported type: ${definition}`);
633
+ }
634
+ }
635
+ }
636
+
637
+ //
638
+ //
639
+ // Helpers
640
+ //
641
+ function isPlainObject(value) {
642
+ return typeof value === "object" && value !== null && !Array.isArray(value);
643
+ }
644
+ /**
645
+ * Convert a `format` declaration (Zod schema, NaturalSchema, or a JSON Schema
646
+ * JsonObject) into a plain JSON Schema we can walk. Mirrors the conversion the
647
+ * provider adapters perform in `formatOutputSchema`, but without any
648
+ * provider-specific sanitization.
649
+ */
650
+ function formatToJsonSchema(format) {
651
+ if (format instanceof z.ZodType) {
652
+ return z.toJSONSchema(format);
653
+ }
654
+ if (isPlainObject(format) && format.type === "json_schema") {
655
+ const clone = structuredClone(format);
656
+ clone.type = "object";
657
+ return clone;
658
+ }
659
+ try {
660
+ return z.toJSONSchema(naturalZodSchema(format));
661
+ }
662
+ catch {
663
+ return undefined;
664
+ }
665
+ }
666
+ /**
667
+ * Walk a JSON Schema alongside a parsed value, filling any declared array field
668
+ * that is absent (`undefined`/`null`) with `[]`. Recurses into object
669
+ * properties and array items so nested declared arrays are also backfilled.
670
+ */
671
+ function fillFromSchema(schema, value) {
672
+ if (!isPlainObject(schema)) {
673
+ return value;
674
+ }
675
+ const type = schema.type;
676
+ const isArray = type === "array" || (type === undefined && "items" in schema);
677
+ if (isArray) {
678
+ if (value === undefined || value === null) {
679
+ return [];
680
+ }
681
+ const items = schema.items;
682
+ if (Array.isArray(value) && isPlainObject(items)) {
683
+ return value.map((entry) => fillFromSchema(items, entry));
684
+ }
685
+ return value;
686
+ }
687
+ const isObject = type === "object" || (type === undefined && "properties" in schema);
688
+ if (isObject) {
689
+ if (!isPlainObject(value)) {
690
+ return value;
691
+ }
692
+ const properties = schema.properties;
693
+ if (isPlainObject(properties)) {
694
+ for (const [key, propSchema] of Object.entries(properties)) {
695
+ value[key] = fillFromSchema(propSchema, value[key]);
696
+ }
697
+ }
698
+ return value;
699
+ }
700
+ return value;
701
+ }
702
+ //
703
+ //
704
+ // Main
705
+ //
706
+ /**
707
+ * Ensure every array field declared in `format` is present in `content` as an
708
+ * array. A declared `format` is a schema contract: an empty list should surface
709
+ * as `[]`, not be dropped from the response. Some providers/models omit empty
710
+ * array fields entirely, leaving consumers to read `.length` on `undefined`.
711
+ *
712
+ * Only mutates a (cloned) structured object; strings and non-objects pass
713
+ * through untouched.
714
+ */
715
+ function fillFormatArrays({ content, format, }) {
716
+ const schema = formatToJsonSchema(format);
717
+ if (!schema) {
718
+ return content;
719
+ }
720
+ return fillFromSchema(schema, structuredClone(content));
721
+ }
722
+
575
723
  /**
576
724
  * Converts a string to a standardized LlmInputMessage
577
725
  * @param input - String to format
@@ -699,68 +847,6 @@ function maxTurnsFromOptions(options) {
699
847
  return 1;
700
848
  }
701
849
 
702
- function naturalZodSchema(definition) {
703
- if (Array.isArray(definition)) {
704
- if (definition.length === 0) {
705
- // Handle empty array - accept any[]
706
- return z.array(z.any());
707
- }
708
- else if (definition.length === 1) {
709
- // Handle array types
710
- const itemType = definition[0];
711
- switch (itemType) {
712
- case String:
713
- return z.array(z.string());
714
- case Number:
715
- return z.array(z.number());
716
- case Boolean:
717
- return z.array(z.boolean());
718
- default:
719
- if (typeof itemType === "object") {
720
- // Handle array of objects
721
- return z.array(naturalZodSchema(itemType));
722
- }
723
- // Handle enum arrays
724
- return z.enum(definition);
725
- }
726
- }
727
- else {
728
- // Handle enum arrays
729
- return z.enum(definition);
730
- }
731
- }
732
- else if (definition && typeof definition === "object") {
733
- if (Object.keys(definition).length === 0) {
734
- // Handle empty object - accept any key-value pairs
735
- return z.record(z.string(), z.any());
736
- }
737
- else {
738
- // Handle object with properties
739
- const schemaShape = {};
740
- for (const [key, value] of Object.entries(definition)) {
741
- schemaShape[key] = naturalZodSchema(value);
742
- }
743
- return z.object(schemaShape);
744
- }
745
- }
746
- else {
747
- switch (definition) {
748
- case String:
749
- return z.string();
750
- case Number:
751
- return z.number();
752
- case Boolean:
753
- return z.boolean();
754
- case Object:
755
- return z.record(z.string(), z.any());
756
- case Array:
757
- return z.array(z.any());
758
- default:
759
- throw new Error(`Unsupported type: ${definition}`);
760
- }
761
- }
762
- }
763
-
764
850
  //
765
851
  // Constants
766
852
  //
@@ -6089,7 +6175,7 @@ class OperateLoop {
6089
6175
  if (this.adapter.hasStructuredOutput(response)) {
6090
6176
  const structuredOutput = this.adapter.extractStructuredOutput(response);
6091
6177
  if (structuredOutput) {
6092
- state.responseBuilder.setContent(structuredOutput);
6178
+ state.responseBuilder.setContent(this.applyFormatArrayDefaults(structuredOutput, options));
6093
6179
  state.responseBuilder.complete();
6094
6180
  return false; // Stop loop
6095
6181
  }
@@ -6214,7 +6300,7 @@ class OperateLoop {
6214
6300
  }
6215
6301
  }
6216
6302
  // No tool calls or no toolkit - we're done
6217
- state.responseBuilder.setContent(parsed.content);
6303
+ state.responseBuilder.setContent(this.applyFormatArrayDefaults(parsed.content, options));
6218
6304
  state.responseBuilder.complete();
6219
6305
  // Add final history items
6220
6306
  const historyItems = this.adapter.responseToHistoryItems(parsed.raw);
@@ -6223,6 +6309,20 @@ class OperateLoop {
6223
6309
  }
6224
6310
  return false; // Stop loop
6225
6311
  }
6312
+ /**
6313
+ * Backfill declared array fields when a `format` is supplied. A declared
6314
+ * `format` is a schema contract: an empty array field should surface as `[]`
6315
+ * rather than be dropped by a provider/model that omits empty lists.
6316
+ */
6317
+ applyFormatArrayDefaults(content, options) {
6318
+ if (!options.format) {
6319
+ return content;
6320
+ }
6321
+ if (typeof content !== "object" || content === null) {
6322
+ return content;
6323
+ }
6324
+ return fillFormatArrays({ content, format: options.format });
6325
+ }
6226
6326
  /**
6227
6327
  * Sync the current input state from the updated provider request.
6228
6328
  * This is necessary because appendToolResult modifies the provider-specific request,