@stackfactor/agent-utils 1.2.11 → 1.2.14

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/README.md CHANGED
@@ -300,3 +300,7 @@ The `config` object accepted by LangChain methods supports the following keys:
300
300
  ## License
301
301
 
302
302
  Not licensed — proprietary software of StackFactor Inc.
303
+
304
+ ## Development and release
305
+
306
+ See [docs/README.md](docs/README.md) — branch flow, how to cut a release, and troubleshooting.
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAgCA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA6lB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAstBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA1/B8B,GAAG,KAAG,MAAM;+CA/jB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+kBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAiiCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBAktB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA//B8B,GAAG,KAAG,MAAM;+CAprB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAosBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAsiCT,wBASE"}
@@ -12,7 +12,8 @@ const const_js_1 = __importDefault(require("./const.js"));
12
12
  const langchain_1 = require("langchain");
13
13
  const errorHandling_js_1 = __importDefault(require("./errorHandling.js"));
14
14
  const logger_js_1 = __importDefault(require("./logger.js"));
15
- const zod_to_json_schema_1 = require("zod-to-json-schema");
15
+ const zod_1 = require("zod");
16
+ const transform_json_schema_1 = require("@anthropic-ai/sdk/lib/transform-json-schema");
16
17
  const runtimeContext_js_1 = require("./runtimeContext.js");
17
18
  const JSON_ESCAPE_INSTRUCTION = `
18
19
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
@@ -23,6 +24,74 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
23
24
  - Backslashes → \\\\
24
25
  Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
25
26
  `.trim();
27
+ /**
28
+ * Providers for which `getLLMModel` configures native, schema-constrained JSON
29
+ * output (OpenAI `response_format`, Anthropic `output_config.format`, Gemini
30
+ * `responseSchema` + JSON mode). For these the model emits JSON directly, so the
31
+ * prompt-injected schema and the schema-validation retry are skipped. Other
32
+ * providers (DeepSeek/Kimi/GLM via the OpenAI-compatible shim) have no native
33
+ * support here and continue to rely on prompt instructions + validation.
34
+ * @param modelName - The model identifier being routed
35
+ * @returns `true` when native structured output is wired up for the provider
36
+ */
37
+ const supportsNativeSchema = (modelName) => modelName.startsWith("gpt-") ||
38
+ modelName.startsWith("claude-") ||
39
+ modelName.startsWith("gemini-");
40
+ /**
41
+ * Converts a Zod v4 schema to a JSON Schema using zod's built-in
42
+ * `z.toJSONSchema`. The classic `zod-to-json-schema` package only understands
43
+ * zod v3 and silently returns `{}` for v4 schemas, which would send empty
44
+ * schemas to every provider. Targets OpenAPI 3.0 — the dialect omits the
45
+ * `$schema` keyword and is the widest-compatible across providers — and inlines
46
+ * reused subschemas so the result carries no `$ref`/`$defs` (which Gemini's
47
+ * `responseSchema` rejects).
48
+ * @param schema - A Zod schema
49
+ * @returns The equivalent JSON Schema object
50
+ */
51
+ const buildJsonSchema = (schema) => zod_1.z.toJSONSchema(schema, { target: "openapi-3.0", reused: "inline" });
52
+ /**
53
+ * Recursively enforces OpenAI strict structured-output rules on a JSON Schema:
54
+ * every object gets `additionalProperties: false` and lists all of its
55
+ * properties in `required`. OpenAI's `strict: true` mode rejects optional
56
+ * properties, so all fields are marked required (matching OpenAI's own schema
57
+ * transform). Mutates and returns the schema.
58
+ * @param s - A JSON Schema object (mutated in place)
59
+ * @returns The same object, made strict-compatible
60
+ */
61
+ const strictifyJsonSchema = (s) => {
62
+ if (!s || typeof s !== "object")
63
+ return s;
64
+ if (s.type === "object" && s.properties) {
65
+ s.additionalProperties = false;
66
+ s.required = Object.keys(s.properties);
67
+ for (const key of Object.keys(s.properties)) {
68
+ strictifyJsonSchema(s.properties[key]);
69
+ }
70
+ }
71
+ if (s.type === "array" && s.items)
72
+ strictifyJsonSchema(s.items);
73
+ return s;
74
+ };
75
+ /**
76
+ * Recursively removes JSON Schema keywords that Google Gemini's `responseSchema`
77
+ * (an OpenAPI 3.0 subset) rejects — notably `additionalProperties`, which the
78
+ * API returns a 400 for. Mutates and returns the schema.
79
+ * @param s - A JSON Schema object (mutated in place)
80
+ * @returns The same object, accepted by Gemini's `responseSchema`
81
+ */
82
+ const sanitizeGeminiSchema = (s) => {
83
+ if (!s || typeof s !== "object")
84
+ return s;
85
+ delete s.additionalProperties;
86
+ if (s.properties) {
87
+ for (const key of Object.keys(s.properties)) {
88
+ sanitizeGeminiSchema(s.properties[key]);
89
+ }
90
+ }
91
+ if (s.items)
92
+ sanitizeGeminiSchema(s.items);
93
+ return s;
94
+ };
26
95
  const checkIfAIProviderConfigured = (config) => {
27
96
  if (!config ||
28
97
  !config.openAIAPIKey ||
@@ -469,28 +538,102 @@ const getOpenAICompatibleProvider = (modelName, config) => {
469
538
  }
470
539
  return null;
471
540
  };
541
+ /**
542
+ * Whether a model accepts the `temperature` sampling parameter at all. Some
543
+ * models reject it with an HTTP 400, so it must be omitted rather than clamped:
544
+ * - Anthropic removed sampling params (temperature/top_p/top_k) on Claude
545
+ * Opus 4.7 and later (Opus 4.7/4.8) and on the 5-series (Sonnet 5, Fable 5,
546
+ * Mythos 5). Opus 4.6, Sonnet 4.6, claude-3-5-sonnet, and Haiku 4.5 still
547
+ * accept it.
548
+ * - OpenAI reasoning models (the GPT-5 family and the o-series) reject it;
549
+ * gpt-4o still accepts it.
550
+ * DeepSeek, Kimi/Moonshot, GLM, and Gemini all accept temperature.
551
+ * @param modelName - the model identifier being routed
552
+ * @returns `true` when the model accepts a `temperature` parameter
553
+ */
554
+ const modelSupportsTemperature = (modelName) => {
555
+ if (!modelName)
556
+ return false;
557
+ // OpenAI reasoning families reject sampling params (gpt-5*, o1/o3/...).
558
+ if (/^gpt-5/.test(modelName) || /^o\d/.test(modelName))
559
+ return false;
560
+ // Anthropic dropped sampling params on Opus 4.7+ and the 5-series.
561
+ if (/^claude-opus-4-(7|8|9|\d\d)\b/.test(modelName))
562
+ return false;
563
+ if (/^claude-(sonnet|opus|haiku)-5\b/.test(modelName))
564
+ return false;
565
+ if (/^claude-(fable|mythos)-5\b/.test(modelName))
566
+ return false;
567
+ return true;
568
+ };
569
+ /**
570
+ * Per-provider valid `temperature` range, used to clamp caller-supplied values.
571
+ * Anthropic, Kimi (Moonshot), and GLM (Zhipu) cap at 1.0; OpenAI, Gemini, and
572
+ * DeepSeek accept up to 2.0. Only consulted for models that support temperature
573
+ * at all (see `modelSupportsTemperature`).
574
+ */
575
+ const getTemperatureRange = (modelName) => {
576
+ if (modelName.startsWith("claude-") ||
577
+ modelName.startsWith("kimi-") ||
578
+ modelName.startsWith("moonshot-") ||
579
+ modelName.startsWith("glm-")) {
580
+ return { min: 0, max: 1 };
581
+ }
582
+ // gpt-, gemini-, deepseek-
583
+ return { min: 0, max: 2 };
584
+ };
585
+ /**
586
+ * Resolves the `temperature` model setting for a given model. Applied in order:
587
+ * 1. presence/type — only a finite number is forwarded (`temperature: 0` is
588
+ * valid; `undefined`/`null`/non-numeric leaves the provider default);
589
+ * 2. support — models that reject the parameter (Opus 4.7+, GPT-5/o-series)
590
+ * get it omitted, with a warning, to avoid an HTTP 400;
591
+ * 3. range — values outside the provider's range are clamped, with a warning.
592
+ * Returns an object to spread into the LangChain model settings (`{}` when the
593
+ * field should be omitted).
594
+ */
595
+ const resolveTemperatureSetting = (modelName, config) => {
596
+ const temperature = config?.temperature;
597
+ if (typeof temperature !== "number" || !Number.isFinite(temperature)) {
598
+ return {};
599
+ }
600
+ if (!modelSupportsTemperature(modelName)) {
601
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Model "${modelName}" does not accept a temperature parameter; ignoring configured temperature ${temperature}.`);
602
+ return {};
603
+ }
604
+ const { min, max } = getTemperatureRange(modelName);
605
+ const clamped = Math.min(Math.max(temperature, min), max);
606
+ if (clamped !== temperature) {
607
+ logger_js_1.default.log(null, logger_js_1.default.levels.warn, `Temperature ${temperature} is outside the supported range [${min}, ${max}] for "${modelName}"; clamping to ${clamped}.`);
608
+ }
609
+ return { temperature: clamped };
610
+ };
472
611
  /**
473
612
  * Instantiates and returns the appropriate LangChain chat model based on the model
474
613
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
475
614
  * `ChatGoogleGenerativeAI`, and `gpt-` maps to `ChatOpenAI`. DeepSeek (`deepseek-`),
476
615
  * Kimi/Moonshot (`kimi-`, `moonshot-`), and GLM/Zhipu (`glm-`) models are routed
477
616
  * through `ChatOpenAI` against each provider's OpenAI-compatible endpoint. When a Zod
478
- * `schema` is provided for a GPT model, native `response_format` with `json_schema` is
479
- * configured for structured output. Throws a `BAD_REQUEST` error for unrecognised
480
- * model names.
617
+ * `schema` is provided, native structured output is configured per provider: OpenAI
618
+ * via `response_format` with `json_schema`, Anthropic via `output_config.format`, and
619
+ * Gemini via JSON mode (`json: true`) plus `responseSchema`. In every case the model
620
+ * emits JSON as the message text, so the caller's parse/validate pipeline is unchanged.
621
+ * The schema is ignored for the OpenAI-compatible providers (DeepSeek/Kimi/GLM), which
622
+ * have no native structured-output support here. Throws a `BAD_REQUEST` error for
623
+ * unrecognised model names.
481
624
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
482
625
  * `"gemini-1.5-pro"`, `"deepseek-chat"`, `"kimi-k2-0905-preview"`, `"glm-4.6"`
483
626
  * @param config - Configuration object containing API keys (`openAIAPIKey`,
484
627
  * `anthropicAPIKey`, `googleAPIKey`, `deepSeekAPIKey`, `kimiAPIKey`, `glmAPIKey`),
485
628
  * optional `maxTokens`, and optional `temperature`
486
- * @param schema - Optional Zod schema used to configure structured JSON output for
487
- * OpenAI GPT models via `response_format`; ignored for other providers
488
- * @returns A configured LangChain chat model instance
629
+ * @param schema - Optional Zod schema used to configure native structured JSON output
630
+ * for GPT / Claude / Gemini models; ignored for OpenAI-compatible providers
631
+ * @returns A configured LangChain chat model (or bound runnable) instance
489
632
  */
490
633
  const getLLMModel = (modelName, config, schema = null) => {
491
- const modelSettings = {
492
- ...(config.temperature ? { temperature: config.temperature } : {}),
493
- };
634
+ // Resolve `temperature` with presence/support/range handling (see
635
+ // resolveTemperatureSetting). Applied uniformly to every provider below.
636
+ const modelSettings = resolveTemperatureSetting(modelName, config);
494
637
  // Claude models (Anthropic)
495
638
  if (modelName.startsWith("claude-")) {
496
639
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -499,23 +642,53 @@ const getLLMModel = (modelName, config, schema = null) => {
499
642
  // through the streaming endpoint internally — callers see no interface
500
643
  // change, but the guard is bypassed.
501
644
  const maxTokens = config.maxTokens || 16384;
645
+ // Native structured output: Anthropic's `output_config.format` constrains
646
+ // decoding to the schema while still emitting JSON as the message text, so
647
+ // the downstream streaming + parse/validate pipeline is unchanged.
648
+ // `transformJSONSchema` applies the same strict-schema normalisation
649
+ // (additionalProperties: false, required fields) that @langchain/anthropic
650
+ // uses internally, which the API requires.
651
+ const outputConfig = schema
652
+ ? {
653
+ outputConfig: {
654
+ format: {
655
+ type: "json_schema",
656
+ schema: (0, transform_json_schema_1.transformJSONSchema)(zod_1.z.toJSONSchema(schema)),
657
+ },
658
+ },
659
+ }
660
+ : {};
502
661
  return new anthropic_1.ChatAnthropic({
503
662
  apiKey: config.anthropicAPIKey,
504
663
  maxTokens,
505
664
  modelName: modelName,
506
665
  streaming: maxTokens > 16384,
507
666
  invocationKwargs: { cache_control: { type: "ephemeral" } },
667
+ ...outputConfig,
508
668
  ...modelSettings,
509
669
  });
510
670
  }
511
671
  // Gemini models (Google)
512
672
  else if (modelName.startsWith("gemini-")) {
513
- return new google_genai_1.ChatGoogleGenerativeAI({
673
+ const model = new google_genai_1.ChatGoogleGenerativeAI({
514
674
  apiKey: config.googleAPIKey,
515
675
  maxOutputTokens: config.maxTokens || 200000,
516
676
  model: modelName,
677
+ // `json: true` forces responseMimeType=application/json, guaranteeing the
678
+ // model returns valid JSON text (never prose) for the parse pipeline.
679
+ ...(schema ? { json: true } : {}),
517
680
  ...modelSettings,
518
681
  });
682
+ // `responseSchema` additionally constrains the output shape. It is a
683
+ // call-time option (not a constructor field), so it is bound onto the model
684
+ // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
685
+ // `additionalProperties` is stripped. Output stays text, so the
686
+ // parse/validate pipeline is unchanged.
687
+ if (schema) {
688
+ const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
689
+ return model.withConfig({ responseSchema: jsonSchema });
690
+ }
691
+ return model;
519
692
  }
520
693
  // GPT models (OpenAI)
521
694
  else if (modelName.startsWith("gpt-")) {
@@ -527,7 +700,7 @@ const getLLMModel = (modelName, config, schema = null) => {
527
700
  };
528
701
  // Use native response_format with JSON schema for structured output
529
702
  if (schema) {
530
- const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
703
+ const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
531
704
  openAISettings.modelKwargs = {
532
705
  response_format: {
533
706
  type: "json_schema",
@@ -1027,7 +1200,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1027
1200
  }
1028
1201
  if (onProgressReport) {
1029
1202
  // Streaming mode: use server-side chunk-based progress for all models
1030
- const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
1203
+ const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1031
1204
  const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1032
1205
  // Build messages with JSON instructions if needed
1033
1206
  let messagesToSend;
@@ -1036,7 +1209,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1036
1209
  ? "Respond with valid JSON."
1037
1210
  : JSON_ESCAPE_INSTRUCTION;
1038
1211
  if (schema && !useNativeSchema) {
1039
- const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
1212
+ const jsonSchema = buildJsonSchema(schema);
1040
1213
  systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
1041
1214
  }
1042
1215
  if (messages.length > 0 && messages[0].role === "system") {
@@ -1174,17 +1347,22 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1174
1347
  }
1175
1348
  }
1176
1349
  else {
1177
- // Non-streaming mode: use native response_format for OpenAI when schema is provided
1178
- const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
1350
+ // Non-streaming mode: use native structured output when the provider
1351
+ // supports it (OpenAI/Anthropic/Gemini); otherwise fall back to prompt
1352
+ // instructions + validation retry.
1353
+ const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1179
1354
  const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1180
- // Add escape instruction to help LLM produce valid JSON (only if expecting JSON)
1181
- // For non-OpenAI models with schema, also include schema in prompt as fallback
1355
+ // Add escape instruction to help LLM produce valid JSON (only if expecting JSON).
1356
+ // When native structured output is in use the model is already constrained,
1357
+ // so a light instruction suffices and the schema is not re-injected.
1182
1358
  let messagesToSend;
1183
1359
  if (expectsJsonResponse) {
1184
- let systemContent = JSON_ESCAPE_INSTRUCTION;
1185
- // Include schema in prompt for non-OpenAI models (OpenAI uses native response_format)
1186
- if (schema && !modelName.startsWith("gpt-")) {
1187
- const jsonSchema = (0, zod_to_json_schema_1.zodToJsonSchema)(schema, { target: "openApi3" });
1360
+ let systemContent = useNativeSchema
1361
+ ? "Respond with valid JSON."
1362
+ : JSON_ESCAPE_INSTRUCTION;
1363
+ // Include the schema in the prompt only when not using native output.
1364
+ if (schema && !useNativeSchema) {
1365
+ const jsonSchema = buildJsonSchema(schema);
1188
1366
  systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
1189
1367
  }
1190
1368
  if (messages.length > 0 && messages[0].role === "system") {
@@ -1 +1 @@
1
- {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAgCA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBA6lB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDAstBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA1/B8B,GAAG,KAAG,MAAM;+CA/jB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCA+kBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAiiCT,wBASE"}
1
+ {"version":3,"file":"langChain.d.ts","sourceRoot":"","sources":["../../src/langChain.ts"],"names":[],"mappings":"AAuGA;;;;;GAKG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE;QAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAAA;KAAE,CAAC;CACxC,CAAC;;0CAE2C,GAAG,KAAG,IAAI;wBAktB/C,MAAM,aACD,MAAM,gBACH,MAAM,SACb,GAAG,EAAE,kBACI,GAAG,UACX,GAAG,KACV,GAAG;sBA0BG,GAAG,UACF,MAAM,UACN,GAAG,eACC,QAAQ,GAAG,IAAI,iBACb,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;oCA4XF,MAAM,UACT,GAAG,UACH,GAAG,oBACO,GAAG,eACT,MAAM,eACN,MAAM,wBACG,OAAO,WACpB,GAAG,cACA,MAAM,UACV,GAAG,EAAE,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;sDA2tBF,MAAM,UACT,GAAG,UACH,MAAM,YACL,GAAG,iBACE,YAAY,GAAG,IAAI,KAChC,OAAO,CAAC,GAAG,CAAC;0CA//B8B,GAAG,KAAG,MAAM;+CAprB9C,YAAY,GAAG,IAAI,GAAG,SAAS,aAC7B,MAAM,kBACD,MAAM,UACd,GAAG,KACV,IAAI;mCAosBU,MAAM,mBACJ,MAAM,EAAE,KACxB,MAAM;;AAsiCT,wBASE"}
@@ -7,7 +7,8 @@ import constants from "./const.js";
7
7
  import { createAgent as createLangChainAgent } from "langchain";
8
8
  import errorHandlingHelper from "./errorHandling.js";
9
9
  import logger from "./logger.js";
10
- import { zodToJsonSchema } from "zod-to-json-schema";
10
+ import { z } from "zod";
11
+ import { transformJSONSchema } from "@anthropic-ai/sdk/lib/transform-json-schema";
11
12
  import { getAbortSignal } from "./runtimeContext.js";
12
13
  const JSON_ESCAPE_INSTRUCTION = `
13
14
  CRITICAL - Your response must be valid JSON. Escape ALL special characters in string values:
@@ -18,6 +19,74 @@ CRITICAL - Your response must be valid JSON. Escape ALL special characters in st
18
19
  - Backslashes → \\\\
19
20
  Do NOT include raw newlines, tabs, or unescaped quotes inside JSON string values.
20
21
  `.trim();
22
+ /**
23
+ * Providers for which `getLLMModel` configures native, schema-constrained JSON
24
+ * output (OpenAI `response_format`, Anthropic `output_config.format`, Gemini
25
+ * `responseSchema` + JSON mode). For these the model emits JSON directly, so the
26
+ * prompt-injected schema and the schema-validation retry are skipped. Other
27
+ * providers (DeepSeek/Kimi/GLM via the OpenAI-compatible shim) have no native
28
+ * support here and continue to rely on prompt instructions + validation.
29
+ * @param modelName - The model identifier being routed
30
+ * @returns `true` when native structured output is wired up for the provider
31
+ */
32
+ const supportsNativeSchema = (modelName) => modelName.startsWith("gpt-") ||
33
+ modelName.startsWith("claude-") ||
34
+ modelName.startsWith("gemini-");
35
+ /**
36
+ * Converts a Zod v4 schema to a JSON Schema using zod's built-in
37
+ * `z.toJSONSchema`. The classic `zod-to-json-schema` package only understands
38
+ * zod v3 and silently returns `{}` for v4 schemas, which would send empty
39
+ * schemas to every provider. Targets OpenAPI 3.0 — the dialect omits the
40
+ * `$schema` keyword and is the widest-compatible across providers — and inlines
41
+ * reused subschemas so the result carries no `$ref`/`$defs` (which Gemini's
42
+ * `responseSchema` rejects).
43
+ * @param schema - A Zod schema
44
+ * @returns The equivalent JSON Schema object
45
+ */
46
+ const buildJsonSchema = (schema) => z.toJSONSchema(schema, { target: "openapi-3.0", reused: "inline" });
47
+ /**
48
+ * Recursively enforces OpenAI strict structured-output rules on a JSON Schema:
49
+ * every object gets `additionalProperties: false` and lists all of its
50
+ * properties in `required`. OpenAI's `strict: true` mode rejects optional
51
+ * properties, so all fields are marked required (matching OpenAI's own schema
52
+ * transform). Mutates and returns the schema.
53
+ * @param s - A JSON Schema object (mutated in place)
54
+ * @returns The same object, made strict-compatible
55
+ */
56
+ const strictifyJsonSchema = (s) => {
57
+ if (!s || typeof s !== "object")
58
+ return s;
59
+ if (s.type === "object" && s.properties) {
60
+ s.additionalProperties = false;
61
+ s.required = Object.keys(s.properties);
62
+ for (const key of Object.keys(s.properties)) {
63
+ strictifyJsonSchema(s.properties[key]);
64
+ }
65
+ }
66
+ if (s.type === "array" && s.items)
67
+ strictifyJsonSchema(s.items);
68
+ return s;
69
+ };
70
+ /**
71
+ * Recursively removes JSON Schema keywords that Google Gemini's `responseSchema`
72
+ * (an OpenAPI 3.0 subset) rejects — notably `additionalProperties`, which the
73
+ * API returns a 400 for. Mutates and returns the schema.
74
+ * @param s - A JSON Schema object (mutated in place)
75
+ * @returns The same object, accepted by Gemini's `responseSchema`
76
+ */
77
+ const sanitizeGeminiSchema = (s) => {
78
+ if (!s || typeof s !== "object")
79
+ return s;
80
+ delete s.additionalProperties;
81
+ if (s.properties) {
82
+ for (const key of Object.keys(s.properties)) {
83
+ sanitizeGeminiSchema(s.properties[key]);
84
+ }
85
+ }
86
+ if (s.items)
87
+ sanitizeGeminiSchema(s.items);
88
+ return s;
89
+ };
21
90
  const checkIfAIProviderConfigured = (config) => {
22
91
  if (!config ||
23
92
  !config.openAIAPIKey ||
@@ -464,28 +533,102 @@ const getOpenAICompatibleProvider = (modelName, config) => {
464
533
  }
465
534
  return null;
466
535
  };
536
+ /**
537
+ * Whether a model accepts the `temperature` sampling parameter at all. Some
538
+ * models reject it with an HTTP 400, so it must be omitted rather than clamped:
539
+ * - Anthropic removed sampling params (temperature/top_p/top_k) on Claude
540
+ * Opus 4.7 and later (Opus 4.7/4.8) and on the 5-series (Sonnet 5, Fable 5,
541
+ * Mythos 5). Opus 4.6, Sonnet 4.6, claude-3-5-sonnet, and Haiku 4.5 still
542
+ * accept it.
543
+ * - OpenAI reasoning models (the GPT-5 family and the o-series) reject it;
544
+ * gpt-4o still accepts it.
545
+ * DeepSeek, Kimi/Moonshot, GLM, and Gemini all accept temperature.
546
+ * @param modelName - the model identifier being routed
547
+ * @returns `true` when the model accepts a `temperature` parameter
548
+ */
549
+ const modelSupportsTemperature = (modelName) => {
550
+ if (!modelName)
551
+ return false;
552
+ // OpenAI reasoning families reject sampling params (gpt-5*, o1/o3/...).
553
+ if (/^gpt-5/.test(modelName) || /^o\d/.test(modelName))
554
+ return false;
555
+ // Anthropic dropped sampling params on Opus 4.7+ and the 5-series.
556
+ if (/^claude-opus-4-(7|8|9|\d\d)\b/.test(modelName))
557
+ return false;
558
+ if (/^claude-(sonnet|opus|haiku)-5\b/.test(modelName))
559
+ return false;
560
+ if (/^claude-(fable|mythos)-5\b/.test(modelName))
561
+ return false;
562
+ return true;
563
+ };
564
+ /**
565
+ * Per-provider valid `temperature` range, used to clamp caller-supplied values.
566
+ * Anthropic, Kimi (Moonshot), and GLM (Zhipu) cap at 1.0; OpenAI, Gemini, and
567
+ * DeepSeek accept up to 2.0. Only consulted for models that support temperature
568
+ * at all (see `modelSupportsTemperature`).
569
+ */
570
+ const getTemperatureRange = (modelName) => {
571
+ if (modelName.startsWith("claude-") ||
572
+ modelName.startsWith("kimi-") ||
573
+ modelName.startsWith("moonshot-") ||
574
+ modelName.startsWith("glm-")) {
575
+ return { min: 0, max: 1 };
576
+ }
577
+ // gpt-, gemini-, deepseek-
578
+ return { min: 0, max: 2 };
579
+ };
580
+ /**
581
+ * Resolves the `temperature` model setting for a given model. Applied in order:
582
+ * 1. presence/type — only a finite number is forwarded (`temperature: 0` is
583
+ * valid; `undefined`/`null`/non-numeric leaves the provider default);
584
+ * 2. support — models that reject the parameter (Opus 4.7+, GPT-5/o-series)
585
+ * get it omitted, with a warning, to avoid an HTTP 400;
586
+ * 3. range — values outside the provider's range are clamped, with a warning.
587
+ * Returns an object to spread into the LangChain model settings (`{}` when the
588
+ * field should be omitted).
589
+ */
590
+ const resolveTemperatureSetting = (modelName, config) => {
591
+ const temperature = config?.temperature;
592
+ if (typeof temperature !== "number" || !Number.isFinite(temperature)) {
593
+ return {};
594
+ }
595
+ if (!modelSupportsTemperature(modelName)) {
596
+ logger.log(null, logger.levels.warn, `Model "${modelName}" does not accept a temperature parameter; ignoring configured temperature ${temperature}.`);
597
+ return {};
598
+ }
599
+ const { min, max } = getTemperatureRange(modelName);
600
+ const clamped = Math.min(Math.max(temperature, min), max);
601
+ if (clamped !== temperature) {
602
+ logger.log(null, logger.levels.warn, `Temperature ${temperature} is outside the supported range [${min}, ${max}] for "${modelName}"; clamping to ${clamped}.`);
603
+ }
604
+ return { temperature: clamped };
605
+ };
467
606
  /**
468
607
  * Instantiates and returns the appropriate LangChain chat model based on the model
469
608
  * name prefix. `claude-` maps to `ChatAnthropic`, `gemini-` maps to
470
609
  * `ChatGoogleGenerativeAI`, and `gpt-` maps to `ChatOpenAI`. DeepSeek (`deepseek-`),
471
610
  * Kimi/Moonshot (`kimi-`, `moonshot-`), and GLM/Zhipu (`glm-`) models are routed
472
611
  * through `ChatOpenAI` against each provider's OpenAI-compatible endpoint. When a Zod
473
- * `schema` is provided for a GPT model, native `response_format` with `json_schema` is
474
- * configured for structured output. Throws a `BAD_REQUEST` error for unrecognised
475
- * model names.
612
+ * `schema` is provided, native structured output is configured per provider: OpenAI
613
+ * via `response_format` with `json_schema`, Anthropic via `output_config.format`, and
614
+ * Gemini via JSON mode (`json: true`) plus `responseSchema`. In every case the model
615
+ * emits JSON as the message text, so the caller's parse/validate pipeline is unchanged.
616
+ * The schema is ignored for the OpenAI-compatible providers (DeepSeek/Kimi/GLM), which
617
+ * have no native structured-output support here. Throws a `BAD_REQUEST` error for
618
+ * unrecognised model names.
476
619
  * @param modelName - The model identifier, e.g. `"gpt-4o"`, `"claude-3-5-sonnet"`,
477
620
  * `"gemini-1.5-pro"`, `"deepseek-chat"`, `"kimi-k2-0905-preview"`, `"glm-4.6"`
478
621
  * @param config - Configuration object containing API keys (`openAIAPIKey`,
479
622
  * `anthropicAPIKey`, `googleAPIKey`, `deepSeekAPIKey`, `kimiAPIKey`, `glmAPIKey`),
480
623
  * optional `maxTokens`, and optional `temperature`
481
- * @param schema - Optional Zod schema used to configure structured JSON output for
482
- * OpenAI GPT models via `response_format`; ignored for other providers
483
- * @returns A configured LangChain chat model instance
624
+ * @param schema - Optional Zod schema used to configure native structured JSON output
625
+ * for GPT / Claude / Gemini models; ignored for OpenAI-compatible providers
626
+ * @returns A configured LangChain chat model (or bound runnable) instance
484
627
  */
485
628
  const getLLMModel = (modelName, config, schema = null) => {
486
- const modelSettings = {
487
- ...(config.temperature ? { temperature: config.temperature } : {}),
488
- };
629
+ // Resolve `temperature` with presence/support/range handling (see
630
+ // resolveTemperatureSetting). Applied uniformly to every provider below.
631
+ const modelSettings = resolveTemperatureSetting(modelName, config);
489
632
  // Claude models (Anthropic)
490
633
  if (modelName.startsWith("claude-")) {
491
634
  // Anthropic's SDK rejects non-streamed requests when max_tokens is large
@@ -494,23 +637,53 @@ const getLLMModel = (modelName, config, schema = null) => {
494
637
  // through the streaming endpoint internally — callers see no interface
495
638
  // change, but the guard is bypassed.
496
639
  const maxTokens = config.maxTokens || 16384;
640
+ // Native structured output: Anthropic's `output_config.format` constrains
641
+ // decoding to the schema while still emitting JSON as the message text, so
642
+ // the downstream streaming + parse/validate pipeline is unchanged.
643
+ // `transformJSONSchema` applies the same strict-schema normalisation
644
+ // (additionalProperties: false, required fields) that @langchain/anthropic
645
+ // uses internally, which the API requires.
646
+ const outputConfig = schema
647
+ ? {
648
+ outputConfig: {
649
+ format: {
650
+ type: "json_schema",
651
+ schema: transformJSONSchema(z.toJSONSchema(schema)),
652
+ },
653
+ },
654
+ }
655
+ : {};
497
656
  return new ChatAnthropic({
498
657
  apiKey: config.anthropicAPIKey,
499
658
  maxTokens,
500
659
  modelName: modelName,
501
660
  streaming: maxTokens > 16384,
502
661
  invocationKwargs: { cache_control: { type: "ephemeral" } },
662
+ ...outputConfig,
503
663
  ...modelSettings,
504
664
  });
505
665
  }
506
666
  // Gemini models (Google)
507
667
  else if (modelName.startsWith("gemini-")) {
508
- return new ChatGoogleGenerativeAI({
668
+ const model = new ChatGoogleGenerativeAI({
509
669
  apiKey: config.googleAPIKey,
510
670
  maxOutputTokens: config.maxTokens || 200000,
511
671
  model: modelName,
672
+ // `json: true` forces responseMimeType=application/json, guaranteeing the
673
+ // model returns valid JSON text (never prose) for the parse pipeline.
674
+ ...(schema ? { json: true } : {}),
512
675
  ...modelSettings,
513
676
  });
677
+ // `responseSchema` additionally constrains the output shape. It is a
678
+ // call-time option (not a constructor field), so it is bound onto the model
679
+ // via `withConfig`. Gemini's schema is an OpenAPI 3.0 subset, so
680
+ // `additionalProperties` is stripped. Output stays text, so the
681
+ // parse/validate pipeline is unchanged.
682
+ if (schema) {
683
+ const jsonSchema = sanitizeGeminiSchema(buildJsonSchema(schema));
684
+ return model.withConfig({ responseSchema: jsonSchema });
685
+ }
686
+ return model;
514
687
  }
515
688
  // GPT models (OpenAI)
516
689
  else if (modelName.startsWith("gpt-")) {
@@ -522,7 +695,7 @@ const getLLMModel = (modelName, config, schema = null) => {
522
695
  };
523
696
  // Use native response_format with JSON schema for structured output
524
697
  if (schema) {
525
- const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
698
+ const jsonSchema = strictifyJsonSchema(buildJsonSchema(schema));
526
699
  openAISettings.modelKwargs = {
527
700
  response_format: {
528
701
  type: "json_schema",
@@ -1022,7 +1195,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1022
1195
  }
1023
1196
  if (onProgressReport) {
1024
1197
  // Streaming mode: use server-side chunk-based progress for all models
1025
- const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
1198
+ const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1026
1199
  const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1027
1200
  // Build messages with JSON instructions if needed
1028
1201
  let messagesToSend;
@@ -1031,7 +1204,7 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1031
1204
  ? "Respond with valid JSON."
1032
1205
  : JSON_ESCAPE_INSTRUCTION;
1033
1206
  if (schema && !useNativeSchema) {
1034
- const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
1207
+ const jsonSchema = buildJsonSchema(schema);
1035
1208
  systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
1036
1209
  }
1037
1210
  if (messages.length > 0 && messages[0].role === "system") {
@@ -1169,17 +1342,22 @@ const runPromptWithModel = async (modelName, config, prompt, onProgressReport, m
1169
1342
  }
1170
1343
  }
1171
1344
  else {
1172
- // Non-streaming mode: use native response_format for OpenAI when schema is provided
1173
- const useNativeSchema = expectsJsonResponse && schema && modelName.startsWith("gpt-");
1345
+ // Non-streaming mode: use native structured output when the provider
1346
+ // supports it (OpenAI/Anthropic/Gemini); otherwise fall back to prompt
1347
+ // instructions + validation retry.
1348
+ const useNativeSchema = expectsJsonResponse && !!schema && supportsNativeSchema(modelName);
1174
1349
  const llm = getLLMModel(modelName, config, useNativeSchema ? schema : null);
1175
- // Add escape instruction to help LLM produce valid JSON (only if expecting JSON)
1176
- // For non-OpenAI models with schema, also include schema in prompt as fallback
1350
+ // Add escape instruction to help LLM produce valid JSON (only if expecting JSON).
1351
+ // When native structured output is in use the model is already constrained,
1352
+ // so a light instruction suffices and the schema is not re-injected.
1177
1353
  let messagesToSend;
1178
1354
  if (expectsJsonResponse) {
1179
- let systemContent = JSON_ESCAPE_INSTRUCTION;
1180
- // Include schema in prompt for non-OpenAI models (OpenAI uses native response_format)
1181
- if (schema && !modelName.startsWith("gpt-")) {
1182
- const jsonSchema = zodToJsonSchema(schema, { target: "openApi3" });
1355
+ let systemContent = useNativeSchema
1356
+ ? "Respond with valid JSON."
1357
+ : JSON_ESCAPE_INSTRUCTION;
1358
+ // Include the schema in the prompt only when not using native output.
1359
+ if (schema && !useNativeSchema) {
1360
+ const jsonSchema = buildJsonSchema(schema);
1183
1361
  systemContent += `\n\nYour response MUST conform to this JSON schema:\n${JSON.stringify(jsonSchema, null, 2)}`;
1184
1362
  }
1185
1363
  if (messages.length > 0 && messages[0].role === "system") {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "1.2.11",
6
+ "version": "1.2.14",
7
7
  "description": "",
8
8
  "main": "dist/cjs/index.js",
9
9
  "module": "dist/esm/index.js",
@@ -13,7 +13,12 @@
13
13
  ],
14
14
  "scripts": {
15
15
  "clean": "rm -rf dist",
16
- "build": "npm run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && echo '{\"type\":\"module\"}' > dist/esm/package.json"
16
+ "build": "npm run clean && tsc -p tsconfig.cjs.json && tsc -p tsconfig.esm.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json && echo '{\"type\":\"module\"}' > dist/esm/package.json",
17
+ "work:start": "bash scripts/work-start.sh",
18
+ "work:commit": "bash scripts/work-commit.sh",
19
+ "work:release": "bash scripts/release.sh",
20
+ "release": "bash scripts/release.sh",
21
+ "release:fanout": "bash scripts/release-fanout.sh"
17
22
  },
18
23
  "repository": {
19
24
  "type": "git",
@@ -51,8 +56,7 @@
51
56
  "mongoose": "^9.3.1",
52
57
  "openai": "^6.32.0",
53
58
  "winston": "^3.19.0",
54
- "zod": "^4.3.6",
55
- "zod-to-json-schema": "^3.25.1"
59
+ "zod": "^4.3.6"
56
60
  },
57
61
  "devDependencies": {
58
62
  "typescript": "^5.9.3"