@juspay/neurolink 12.37.4 → 12.38.0

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 (49) hide show
  1. package/CHANGELOG.md +3 -3
  2. package/README.md +60 -50
  3. package/dist/browser/neurolink.min.js +423 -423
  4. package/dist/cli/commands/decide.d.ts +2 -2
  5. package/dist/cli/commands/decide.js +22 -3
  6. package/dist/cli/commands/setup.js +2 -1
  7. package/dist/cli/factories/commandFactory.js +1 -1
  8. package/dist/constants/enums.d.ts +9 -0
  9. package/dist/constants/enums.js +10 -0
  10. package/dist/core/baseProvider.d.ts +10 -1
  11. package/dist/core/baseProvider.js +35 -7
  12. package/dist/factories/providerDescriptors.d.ts +2 -0
  13. package/dist/factories/providerDescriptors.js +52 -6
  14. package/dist/factories/providerRegistry.js +9 -1
  15. package/dist/models/manifestRegistry.js +2 -0
  16. package/dist/models/manifests/anthropic.js +2 -2
  17. package/dist/models/manifests/xor.d.ts +12 -0
  18. package/dist/models/manifests/xor.js +30 -0
  19. package/dist/models/modelRegistry.d.ts +12 -1
  20. package/dist/models/modelRegistry.js +57 -5
  21. package/dist/neurolink.js +6 -0
  22. package/dist/providers/amazonSagemaker.js +1 -1
  23. package/dist/providers/anthropic/client.js +29 -5
  24. package/dist/providers/anthropic/structuredOutput.d.ts +4 -1
  25. package/dist/providers/anthropic/structuredOutput.js +13 -5
  26. package/dist/providers/catalog/cerebras.json +7 -8
  27. package/dist/providers/catalog/fireworks.json +10 -12
  28. package/dist/providers/googleVertex/client.d.ts +3 -3
  29. package/dist/providers/googleVertex/client.js +79 -26
  30. package/dist/providers/laya.js +1 -19
  31. package/dist/providers/openaiChatCompletionsBase.js +1 -1
  32. package/dist/providers/systemOneDecision.d.ts +21 -2
  33. package/dist/providers/systemOneDecision.js +64 -4
  34. package/dist/providers/xor.d.ts +48 -0
  35. package/dist/providers/xor.js +192 -0
  36. package/dist/proxy/vertexAnthropicFallback.d.ts +1 -12
  37. package/dist/proxy/vertexAnthropicFallback.js +43 -2
  38. package/dist/types/classifierRouter.d.ts +5 -4
  39. package/dist/types/cli.d.ts +6 -2
  40. package/dist/types/decision.d.ts +60 -1
  41. package/dist/types/model.d.ts +6 -0
  42. package/dist/types/providers.d.ts +10 -0
  43. package/dist/utils/decisionMedia.d.ts +9 -0
  44. package/dist/utils/decisionMedia.js +160 -0
  45. package/dist/utils/modelChoices.js +9 -1
  46. package/dist/utils/providerConfig.d.ts +5 -0
  47. package/dist/utils/providerConfig.js +17 -0
  48. package/docs-site/static/search-index.json +63 -46
  49. package/package.json +2 -1
@@ -41,14 +41,14 @@ import { buildNoOutputSentinel, stampNoOutputSpan, } from "../../utils/noOutputS
41
41
  import { convertZodToJsonSchema } from "../../utils/schemaConversion.js";
42
42
  import { resolveClaudeMaxTokens } from "../../utils/tokenLimits.js";
43
43
  import { toAnthropicImageBlock, fileToAnthropicBlock, } from "../anthropicImageBlocks.js";
44
- import { resolveSamplingParams } from "../../models/modelRegistry.js";
44
+ import { modelSupportsForcedToolChoice, resolveSamplingParams, } from "../../models/modelRegistry.js";
45
45
  import { createDeferredAnalytics, stringifyToolInput, } from "../openaiChatCompletionsClient.js";
46
46
  import { createStreamChannel } from "../../core/streamChannel.js";
47
47
  import { toNativeToolDeclarations } from "../../core/nativeToolFormat.js";
48
48
  import { ANTHROPIC_BETA_HEADERS } from "./constants.js";
49
49
  import { cacheControlOf, withLastToolCacheBreakpoint } from "./cacheControl.js";
50
50
  import { createNativeGenerateGuard } from "../../context/nativeGenerateGuard.js";
51
- import { appendFinalResultInstruction, appendFinalResultTool, FINAL_RESULT_TOOL_NAME, } from "./structuredOutput.js";
51
+ import { appendFinalResultInstruction, appendFinalResultTool, appendToolAnswerInstruction, FINAL_RESULT_TOOL_NAME, } from "./structuredOutput.js";
52
52
  // AnthropicProviderConfig is imported from types/providers.ts
53
53
  // Re-export for backward compatibility
54
54
  // Configuration helpers - now using consolidated utility
@@ -424,6 +424,23 @@ const toolChoiceToAnthropic = (choice) => {
424
424
  }
425
425
  return undefined;
426
426
  };
427
+ /**
428
+ * Claude 5.5 / 5.1 models answer a forced choice with a 400, so for them it
429
+ * becomes `auto`: the tool stays declared and the model still picks it.
430
+ */
431
+ const relaxForcedToolChoice = (modelId, choice) => {
432
+ if ((choice?.type !== "any" && choice?.type !== "tool") ||
433
+ modelSupportsForcedToolChoice(modelId)) {
434
+ return choice;
435
+ }
436
+ logger.warn(`[anthropic] ${modelId} rejects tool_choice "${choice.type}"; sending "auto" instead`);
437
+ return {
438
+ type: "auto",
439
+ ...(choice.disable_parallel_tool_use !== undefined
440
+ ? { disable_parallel_tool_use: choice.disable_parallel_tool_use }
441
+ : {}),
442
+ };
443
+ };
427
444
  /** Map Anthropic stop_reason onto the V3 unified finish reason. */
428
445
  const mapAnthropicStopReason = (raw) => {
429
446
  switch (raw) {
@@ -1107,8 +1124,15 @@ export class AnthropicProvider extends BaseProvider {
1107
1124
  input_schema: schema,
1108
1125
  },
1109
1126
  ];
1110
- toolChoice = { type: "tool", name: jsonTool };
1127
+ if (modelSupportsForcedToolChoice(modelId)) {
1128
+ toolChoice = { type: "tool", name: jsonTool };
1129
+ }
1130
+ else {
1131
+ toolChoice = undefined;
1132
+ system = appendToolAnswerInstruction(system, jsonTool);
1133
+ }
1111
1134
  }
1135
+ toolChoice = relaxForcedToolChoice(modelId, toolChoice);
1112
1136
  // Additive structured output: when the caller wants a schema AND real
1113
1137
  // tools, the forced-json path above cannot be used (it replaces the
1114
1138
  // tools array), and structured output is excluded for this surface
@@ -1458,7 +1482,7 @@ export class AnthropicProvider extends BaseProvider {
1458
1482
  options.disableInternalFallback === true;
1459
1483
  // The native loop bypasses BaseProvider.executeGeneration, so the turn
1460
1484
  // budget has to be composed here or it stops existing for this provider.
1461
- return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback);
1485
+ return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback, options);
1462
1486
  }
1463
1487
  async executeNativeGenerate(options) {
1464
1488
  const startTime = Date.now();
@@ -1717,7 +1741,7 @@ export class AnthropicProvider extends BaseProvider {
1717
1741
  }
1718
1742
  const modelId = this.modelName || getDefaultAnthropicModel();
1719
1743
  const anthropicToolChoice = shouldUseTools && anthropicTools && anthropicTools.length > 0
1720
- ? toolChoiceToAnthropic(resolveToolChoice(options, toolsRecord, shouldUseTools))
1744
+ ? relaxForcedToolChoice(modelId, toolChoiceToAnthropic(resolveToolChoice(options, toolsRecord, shouldUseTools)))
1721
1745
  : undefined;
1722
1746
  // Extended thinking: enabled when the caller supplies an explicit token
1723
1747
  // budget (mirrors the previous experimental_thinking gating). Thinking
@@ -17,8 +17,10 @@ import type Anthropic from "@anthropic-ai/sdk";
17
17
  */
18
18
  /** Internal tool name — filtered out of every returned tool call / execution. */
19
19
  export declare const FINAL_RESULT_TOOL_NAME = "final_result";
20
+ /** The system-prompt instruction to answer through the named tool. */
21
+ export declare function toolAnswerInstruction(toolName: string): string;
20
22
  /** Appended to the system prompt whenever the final_result tool is in play. */
21
- export declare const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
23
+ export declare const FINAL_RESULT_INSTRUCTION: string;
22
24
  /**
23
25
  * Build the `final_result` tool definition from a JSON Schema.
24
26
  *
@@ -47,6 +49,7 @@ export declare function appendFinalResultTool(tools: Anthropic.Messages.Tool[] |
47
49
  * existing one: rewriting a block that carries a `cache_control` marker would
48
50
  * change the cached prefix and invalidate the prompt cache on every turn.
49
51
  */
52
+ export declare function appendToolAnswerInstruction(system: string | Anthropic.Messages.TextBlockParam[] | undefined, toolName: string): string | Anthropic.Messages.TextBlockParam[];
50
53
  export declare function appendFinalResultInstruction(system: string | Anthropic.Messages.TextBlockParam[] | undefined): string | Anthropic.Messages.TextBlockParam[];
51
54
  /**
52
55
  * Canonical JSON text for a `final_result` payload.
@@ -19,8 +19,12 @@ import { inlineJsonSchema } from "../../utils/schemaConversion.js";
19
19
  /** Internal tool name — filtered out of every returned tool call / execution. */
20
20
  export const FINAL_RESULT_TOOL_NAME = "final_result";
21
21
  const FINAL_RESULT_TOOL_DESCRIPTION = "Return the final structured result. You MUST call this tool when you have gathered all information and are ready to provide the final answer. The arguments should contain the structured data matching the expected schema.";
22
+ /** The system-prompt instruction to answer through the named tool. */
23
+ export function toolAnswerInstruction(toolName) {
24
+ return `\n\nIMPORTANT: You MUST call the '${toolName}' tool to return your response in the required structured format. Do not respond with plain text - always use the ${toolName} tool.`;
25
+ }
22
26
  /** Appended to the system prompt whenever the final_result tool is in play. */
23
- export const FINAL_RESULT_INSTRUCTION = "\n\nIMPORTANT: You MUST call the 'final_result' tool to return your response in the required structured format. Do not respond with plain text - always use the final_result tool.";
27
+ export const FINAL_RESULT_INSTRUCTION = toolAnswerInstruction(FINAL_RESULT_TOOL_NAME);
24
28
  /**
25
29
  * Build the `final_result` tool definition from a JSON Schema.
26
30
  *
@@ -71,14 +75,18 @@ export function appendFinalResultTool(tools, jsonSchema) {
71
75
  * existing one: rewriting a block that carries a `cache_control` marker would
72
76
  * change the cached prefix and invalidate the prompt cache on every turn.
73
77
  */
74
- export function appendFinalResultInstruction(system) {
78
+ export function appendToolAnswerInstruction(system, toolName) {
79
+ const instruction = toolAnswerInstruction(toolName);
75
80
  if (system === undefined) {
76
- return FINAL_RESULT_INSTRUCTION.trim();
81
+ return instruction.trim();
77
82
  }
78
83
  if (typeof system === "string") {
79
- return system + FINAL_RESULT_INSTRUCTION;
84
+ return system + instruction;
80
85
  }
81
- return [...system, { type: "text", text: FINAL_RESULT_INSTRUCTION.trim() }];
86
+ return [...system, { type: "text", text: instruction.trim() }];
87
+ }
88
+ export function appendFinalResultInstruction(system) {
89
+ return appendToolAnswerInstruction(system, FINAL_RESULT_TOOL_NAME);
82
90
  }
83
91
  /**
84
92
  * Canonical JSON text for a `final_result` payload.
@@ -9,7 +9,7 @@
9
9
  },
10
10
  "models": {
11
11
  "default": "gpt-oss-120b",
12
- "fallbacks": ["gpt-oss-120b", "gemma-4-31b"],
12
+ "fallbacks": ["gpt-oss-120b"],
13
13
  "defaultContextWindow": 65536,
14
14
  "defaultMaxOutputTokens": 8192,
15
15
  "catalog": {
@@ -29,13 +29,12 @@
29
29
  "input": 0.99,
30
30
  "output": 1.49
31
31
  },
32
- "vision": true,
33
- "status": "production",
34
- "description": "Google Gemma 4 31B — vision-capable"
32
+ "vision": false,
33
+ "status": "retired",
34
+ "description": "Retired 2026-09 — gone from the authenticated /v1/models roster; the nightly vision test has failed on it since 2026-09-10"
35
35
  }
36
36
  },
37
- "visionModel": "gemma-4-31b",
38
- "topModels": ["gpt-oss-120b", "gemma-4-31b"]
37
+ "topModels": ["gpt-oss-120b"]
39
38
  },
40
39
  "capabilities": {
41
40
  "text": true,
@@ -68,8 +67,8 @@
68
67
  },
69
68
  "evidence": {
70
69
  "rosterVerified": {
71
- "date": "2026-08-27",
72
- "method": "authenticated GET /v1/models"
70
+ "date": "2026-09-28",
71
+ "method": "authenticated GET /v1/models — roster is now gpt-oss-120b and qwen-3.8-27b; gemma-4-31b is gone, so it is retired and Cerebras has no verified vision model. qwen-3.8-27b is not added: the account answers 402 payment_required, so no chat call could verify it"
73
72
  },
74
73
  "authProbe": {
75
74
  "date": "2026-08-26",
@@ -8,12 +8,11 @@
8
8
  "baseURL": "https://api.fireworks.ai/inference/v1"
9
9
  },
10
10
  "models": {
11
- "default": "accounts/fireworks/models/kimi-k2p6",
11
+ "default": "accounts/fireworks/models/kimi-k3",
12
12
  "fallbackModelName": "accounts/fireworks/models/gpt-oss-120b",
13
13
  "fallbacks": [
14
- "accounts/fireworks/models/kimi-k2p6",
15
- "accounts/fireworks/models/gpt-oss-120b",
16
14
  "accounts/fireworks/models/kimi-k3",
15
+ "accounts/fireworks/models/gpt-oss-120b",
17
16
  "accounts/fireworks/models/qwen3p8-max",
18
17
  "accounts/fireworks/models/glm-5p3"
19
18
  ],
@@ -22,9 +21,9 @@
22
21
  "catalog": {
23
22
  "accounts/fireworks/models/kimi-k2p6": {
24
23
  "enumMember": "KIMI_K2P6",
25
- "vision": true,
26
- "status": "production",
27
- "description": "Recommended - Kimi K2.6, Moonshot flagship; vision-capable"
24
+ "vision": false,
25
+ "status": "retired",
26
+ "description": "Retired 2026-09 — still on the Fireworks roster, but a real chat call returns 404 'not deployed' (since the 2026-09-26 nightly)"
28
27
  },
29
28
  "accounts/fireworks/models/gpt-oss-120b": {
30
29
  "enumMember": "GPT_OSS_120B",
@@ -36,7 +35,7 @@
36
35
  "enumMember": "KIMI_K3",
37
36
  "vision": true,
38
37
  "status": "production",
39
- "description": "Kimi K3 — newest Moonshot generation; vision-capable"
38
+ "description": "Recommended - Kimi K3, newest Moonshot generation; vision-capable"
40
39
  },
41
40
  "accounts/fireworks/models/qwen3p8-max": {
42
41
  "enumMember": "QWEN3P8_MAX",
@@ -106,9 +105,8 @@
106
105
  }
107
106
  },
108
107
  "topModels": [
109
- "accounts/fireworks/models/kimi-k2p6",
110
- "accounts/fireworks/models/gpt-oss-120b",
111
108
  "accounts/fireworks/models/kimi-k3",
109
+ "accounts/fireworks/models/gpt-oss-120b",
112
110
  "accounts/fireworks/models/qwen3p8-max"
113
111
  ]
114
112
  },
@@ -143,12 +141,12 @@
143
141
  },
144
142
  "evidence": {
145
143
  "rosterVerified": {
146
- "date": "2026-08-30",
144
+ "date": "2026-09-28",
147
145
  "method": "GET /models against the live account, cross-checked with a real chat call per id"
148
146
  },
149
147
  "liveMatrix": {
150
- "date": "2026-08-30",
151
- "result": "The previous default, deepseek-v4-pro, is the reason a roster listing is never sufficient evidence here: it is still listed but a real chat call hangs past 120s, so the default moved to kimi-k2p6 (200 OK). Six ids return 404 'not deployed', including all three old vision models. Vision did not disappear with them: re-tested with a valid 64x64 PNG, kimi-k2p6, kimi-k3 and qwen3p8-max all describe the image correctly, while glm-5p3 answers 'does not support image inputs'. gpt-oss-20b is listed but 404s and was therefore not added."
148
+ "date": "2026-09-28",
149
+ "result": "kimi-k2p6 is still listed on /models but a real chat call now returns 404 'not deployed', which failed the nightly model-liveness gate from 2026-09-26 and sent the matrix vision test down the text-only fallback. kimi-k3 answers a text call and a call carrying a PNG with 200, so it becomes the default; gpt-oss-120b, qwen3p8-max and glm-5p3 also answer text calls. Earlier (2026-08-30): deepseek-v4-pro was listed but hung past 120s, six ids returned 404 'not deployed', and glm-5p3 answers 'does not support image inputs'."
152
150
  },
153
151
  "addedInPR": "https://github.com/juspay/neurolink/pull/1587"
154
152
  }
@@ -222,9 +222,9 @@ export declare class GoogleVertexProvider extends BaseProvider {
222
222
  * clamped below the ceiling rather than passed through — an over-large
223
223
  * budget is a 400 from the API.
224
224
  *
225
- * `useFinalResultTool` (schema / structured-output mode) always pairs with
226
- * a forced `tool_choice:{type:"any"}` in the request built right after this
227
- * call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
225
+ * `forcesToolChoice` (schema mode on a model that accepts a forced choice)
226
+ * pairs with a forced `tool_choice:{type:"any"}` in the request built right
227
+ * after this call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
228
228
  * that forces tool use: a live 400, "Thinking may not be enabled when
229
229
  * tool_choice forces tool use." So `thinking` is omitted entirely in that
230
230
  * mode, preserving the prior (pre-thinking-fix) behaviour of silently
@@ -46,7 +46,7 @@ import { SpanKind, SpanStatusCode, context as otelContext, trace as otelTrace, }
46
46
  import { calculateCost } from "../../utils/pricing.js";
47
47
  import { transformToolExecutions } from "../../utils/transformationUtils.js";
48
48
  import { resolveToolExecutionRecords } from "../../core/toolExecutionRecorder.js";
49
- import { resolveSamplingParams } from "../../models/modelRegistry.js";
49
+ import { modelSupportsForcedToolChoice, resolveSamplingParams, } from "../../models/modelRegistry.js";
50
50
  import { sanitizeAnthropicMessagesForTrace } from "../../utils/anthropicTraceSanitizer.js";
51
51
  import { extractToolFailureText } from "../../utils/mcpErrorText.js";
52
52
  // Import proper types for multimodal message handling
@@ -707,6 +707,20 @@ async function collectVertexStreamChunks(stream, channel, hooks = {}) {
707
707
  ...(stepReasoningTokens ? { reasoningTokens: stepReasoningTokens } : {}),
708
708
  };
709
709
  }
710
+ /**
711
+ * The answer text of an unforced finalization reply. Text that leads into a
712
+ * further tool call is preamble, not an answer, so a reply still asking for a
713
+ * tool yields none.
714
+ */
715
+ function finalizationAnswerText(content) {
716
+ if (content.some((block) => block.type === "tool_use")) {
717
+ return "";
718
+ }
719
+ return content
720
+ .filter((block) => block.type === "text")
721
+ .map((b) => b.text)
722
+ .join("");
723
+ }
710
724
  export class GoogleVertexProvider extends BaseProvider {
711
725
  projectId;
712
726
  location;
@@ -2949,9 +2963,9 @@ export class GoogleVertexProvider extends BaseProvider {
2949
2963
  * clamped below the ceiling rather than passed through — an over-large
2950
2964
  * budget is a 400 from the API.
2951
2965
  *
2952
- * `useFinalResultTool` (schema / structured-output mode) always pairs with
2953
- * a forced `tool_choice:{type:"any"}` in the request built right after this
2954
- * call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
2966
+ * `forcesToolChoice` (schema mode on a model that accepts a forced choice)
2967
+ * pairs with a forced `tool_choice:{type:"any"}` in the request built right
2968
+ * after this call — Anthropic hard-rejects `thinking` combined with a `tool_choice`
2955
2969
  * that forces tool use: a live 400, "Thinking may not be enabled when
2956
2970
  * tool_choice forces tool use." So `thinking` is omitted entirely in that
2957
2971
  * mode, preserving the prior (pre-thinking-fix) behaviour of silently
@@ -2968,8 +2982,8 @@ export class GoogleVertexProvider extends BaseProvider {
2968
2982
  buildClaudeThinkingParam(
2969
2983
  // Structural, not TextGenerationOptions: the streaming caller passes
2970
2984
  // StreamOptions, and only these two fields are read.
2971
- options, maxTokens, useFinalResultTool) {
2972
- if (useFinalResultTool) {
2985
+ options, maxTokens, forcesToolChoice) {
2986
+ if (forcesToolChoice) {
2973
2987
  if (options.thinkingConfig?.enabled) {
2974
2988
  logger.debug("[GoogleVertex] Omitting thinking: schema mode forces tool_choice:any, which Anthropic rejects alongside thinking");
2975
2989
  }
@@ -3215,7 +3229,10 @@ export class GoogleVertexProvider extends BaseProvider {
3215
3229
  ...(options.topP !== undefined && { topP: options.topP }),
3216
3230
  }, "vertex.anthropic.stream");
3217
3231
  const streamMaxTokens = resolveClaudeMaxTokens(modelName, options.maxTokens);
3218
- const streamThinking = this.buildClaudeThinkingParam(options, streamMaxTokens, useFinalResultTool);
3232
+ // Claude 5.5 / 5.1 400 on a forced tool_choice; schema mode leaves them on
3233
+ // auto, steered by the final_result instruction already in the prompt.
3234
+ const forceFinalResult = useFinalResultTool && modelSupportsForcedToolChoice(modelName);
3235
+ const streamThinking = this.buildClaudeThinkingParam(options, streamMaxTokens, forceFinalResult);
3219
3236
  const requestParams = {
3220
3237
  model: modelName,
3221
3238
  // Default to the model's real output ceiling (e.g. 64K for Sonnet 4.x)
@@ -3226,7 +3243,7 @@ export class GoogleVertexProvider extends BaseProvider {
3226
3243
  ...(streamThinking && { thinking: streamThinking }),
3227
3244
  messages: messages,
3228
3245
  ...(tools && tools.length > 0 && { tools }),
3229
- ...(useFinalResultTool && { tool_choice: { type: "any" } }),
3246
+ ...(forceFinalResult && { tool_choice: { type: "any" } }),
3230
3247
  ...(systemPromptWithSchema && { system: systemPromptWithSchema }),
3231
3248
  ...(streamSampling.temperature !== undefined && {
3232
3249
  temperature: streamSampling.temperature,
@@ -3858,10 +3875,15 @@ export class GoogleVertexProvider extends BaseProvider {
3858
3875
  const finalizationStream = await client.messages.stream({
3859
3876
  ...requestParams,
3860
3877
  max_tokens: terminalMaxTokens,
3861
- tool_choice: {
3862
- type: "tool",
3863
- name: "final_result",
3864
- },
3878
+ // Resized, not inherited: terminalMaxTokens can sit below the
3879
+ // budget requestParams' thinking was sized for.
3880
+ thinking: this.buildClaudeThinkingParam(options, terminalMaxTokens, forceFinalResult),
3881
+ ...(forceFinalResult && {
3882
+ tool_choice: {
3883
+ type: "tool",
3884
+ name: "final_result",
3885
+ },
3886
+ }),
3865
3887
  ...(cachedFinal.system !== undefined && {
3866
3888
  system: cachedFinal.system,
3867
3889
  }),
@@ -3905,11 +3927,23 @@ export class GoogleVertexProvider extends BaseProvider {
3905
3927
  logger.debug("[GoogleVertex] Forced finalization returned structured output (stream)", { keys: Object.keys(forcedFinalResult.input) });
3906
3928
  }
3907
3929
  else {
3908
- const capMessage = hitContextLimit
3909
- ? buildContextCapMessage(externalToolCallCount)
3910
- : buildToolLoopCapMessage(maxSteps, externalToolCallCount);
3911
- channel.push({ content: capMessage });
3912
- aggregatedTurnText += capMessage;
3930
+ // Unforced (the model refuses a forced choice), the call may
3931
+ // answer in text instead; keep that answer over a cap notice.
3932
+ const finalizationText = forceFinalResult
3933
+ ? ""
3934
+ : finalizationAnswerText(response.content);
3935
+ if (finalizationText) {
3936
+ synthesizedFinalAnswer = true;
3937
+ channel.push({ content: finalizationText });
3938
+ aggregatedTurnText += finalizationText;
3939
+ }
3940
+ else {
3941
+ const capMessage = hitContextLimit
3942
+ ? buildContextCapMessage(externalToolCallCount)
3943
+ : buildToolLoopCapMessage(maxSteps, externalToolCallCount);
3944
+ channel.push({ content: capMessage });
3945
+ aggregatedTurnText += capMessage;
3946
+ }
3913
3947
  }
3914
3948
  }
3915
3949
  catch (error) {
@@ -4484,7 +4518,10 @@ export class GoogleVertexProvider extends BaseProvider {
4484
4518
  ...(options.topP !== undefined && { topP: options.topP }),
4485
4519
  }, "vertex.anthropic.generate");
4486
4520
  const generateMaxTokens = resolveClaudeMaxTokens(modelName, options.maxTokens);
4487
- const generateThinking = this.buildClaudeThinkingParam(options, generateMaxTokens, useFinalResultTool);
4521
+ // Claude 5.5 / 5.1 400 on a forced tool_choice; schema mode leaves them on
4522
+ // auto, steered by the final_result instruction already in the prompt.
4523
+ const forceFinalResult = useFinalResultTool && modelSupportsForcedToolChoice(modelName);
4524
+ const generateThinking = this.buildClaudeThinkingParam(options, generateMaxTokens, forceFinalResult);
4488
4525
  const requestParams = {
4489
4526
  model: modelName,
4490
4527
  // Default to the model's real output ceiling (see stream path note).
@@ -4492,7 +4529,7 @@ export class GoogleVertexProvider extends BaseProvider {
4492
4529
  ...(generateThinking && { thinking: generateThinking }),
4493
4530
  messages,
4494
4531
  ...(tools && tools.length > 0 && { tools }),
4495
- ...(useFinalResultTool && { tool_choice: { type: "any" } }),
4532
+ ...(forceFinalResult && { tool_choice: { type: "any" } }),
4496
4533
  ...(systemPromptWithSchema && { system: systemPromptWithSchema }),
4497
4534
  ...(generateSampling.temperature !== undefined && {
4498
4535
  temperature: generateSampling.temperature,
@@ -4901,10 +4938,15 @@ export class GoogleVertexProvider extends BaseProvider {
4901
4938
  const response = await withTimeout(client.messages.create({
4902
4939
  ...requestParams,
4903
4940
  max_tokens: terminalMaxTokens,
4904
- tool_choice: {
4905
- type: "tool",
4906
- name: "final_result",
4907
- },
4941
+ // Resized, not inherited: terminalMaxTokens can sit below the
4942
+ // budget requestParams' thinking was sized for.
4943
+ thinking: this.buildClaudeThinkingParam(options, terminalMaxTokens, forceFinalResult),
4944
+ ...(forceFinalResult && {
4945
+ tool_choice: {
4946
+ type: "tool",
4947
+ name: "final_result",
4948
+ },
4949
+ }),
4908
4950
  ...(cachedFinal.system !== undefined && {
4909
4951
  system: cachedFinal.system,
4910
4952
  }),
@@ -4928,9 +4970,20 @@ export class GoogleVertexProvider extends BaseProvider {
4928
4970
  logger.debug("[GoogleVertex] Forced finalization returned structured output (generate)", { keys: Object.keys(structuredOutput) });
4929
4971
  }
4930
4972
  else {
4931
- finalText = hitContextLimit
4932
- ? buildContextCapMessage(externalToolCallCount)
4933
- : buildToolLoopCapMessage(maxSteps, externalToolCallCount);
4973
+ // Unforced (the model refuses a forced choice), the call may
4974
+ // answer in text instead; keep that answer over a cap notice.
4975
+ const finalizationText = forceFinalResult
4976
+ ? ""
4977
+ : finalizationAnswerText(response.content);
4978
+ if (finalizationText) {
4979
+ finalText = finalizationText;
4980
+ synthesizedFinalAnswer = true;
4981
+ }
4982
+ else {
4983
+ finalText = hitContextLimit
4984
+ ? buildContextCapMessage(externalToolCallCount)
4985
+ : buildToolLoopCapMessage(maxSteps, externalToolCallCount);
4986
+ }
4934
4987
  }
4935
4988
  }
4936
4989
  catch (error) {
@@ -2,25 +2,7 @@ import { LayaModels } from "../constants/enums.js";
2
2
  import { logger } from "../utils/logger.js";
3
3
  import { redactUrlForError } from "../utils/logSanitize.js";
4
4
  import { getProviderModel } from "../utils/providerConfig.js";
5
- import { describeValidationErrors, isRecord, SystemOneDecisionProvider, } from "./systemOneDecision.js";
6
- /**
7
- * LiteLLM echoes credentials back in its error texts, in more than one
8
- * wording: a masked key plus its hash on a rejected key, the whole key when it
9
- * does not look like a LiteLLM key, a key hash on a rate limit. None of it
10
- * belongs in an error message or a log, whatever the wording, so the
11
- * configured key, anything shaped like a LiteLLM key and any long hex run are
12
- * all removed.
13
- */
14
- function redactCredentials(message, apiKey) {
15
- const withoutKey = apiKey
16
- ? message.split(apiKey).join("[redacted]")
17
- : message;
18
- return withoutKey
19
- .replace(/\.?\s*Received API Key\s*=[\s\S]*$/, "")
20
- .replace(/\bsk-[A-Za-z0-9._-]+/g, "[redacted]")
21
- .replace(/\b[0-9a-f]{32,}\b/gi, "[redacted]")
22
- .trim();
23
- }
5
+ import { describeValidationErrors, isRecord, redactCredentials, SystemOneDecisionProvider, } from "./systemOneDecision.js";
24
6
  /**
25
7
  * Normalise the two envelopes a Laya failure can arrive in.
26
8
  *
@@ -873,7 +873,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
873
873
  options.disableInternalFallback === true;
874
874
  // The native loop bypasses BaseProvider.executeGeneration, so the turn
875
875
  // budget has to be composed here or it stops existing for this provider.
876
- return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback);
876
+ return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback, options);
877
877
  }
878
878
  async executeNativeGenerate(options) {
879
879
  const startTime = Date.now();
@@ -1,7 +1,7 @@
1
1
  import type { AIProviderName } from "../constants/enums.js";
2
2
  import { BaseProvider } from "../core/baseProvider.js";
3
3
  import { ProviderError } from "../types/index.js";
4
- import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
4
+ import type { DecisionError, DecisionQuestion, DecisionRequest, DecisionResult, DecisionPreparedMedia, DecisionState, LanguageModel, StreamOptions, StreamResult, ValidationSchema } from "../types/index.js";
5
5
  /**
6
6
  * Generous enough for a cold start (measured at 2.0–2.7s after idle on Jev)
7
7
  * while still bounded. Every internal caller is fail-open, so this only bites
@@ -16,6 +16,15 @@ export declare const asNumber: (v: unknown) => number | undefined;
16
16
  * request back — possibly the whole state — so it is never surfaced.
17
17
  */
18
18
  export declare function describeValidationErrors(detail: readonly unknown[]): string;
19
+ /**
20
+ * A LiteLLM proxy echoes credentials back in its error texts, in more than one
21
+ * wording: a masked key plus its hash on a rejected key, the whole key when it
22
+ * does not look like a LiteLLM key, a key hash on a rate limit. None of it
23
+ * belongs in an error message or a log, whatever the wording, so the
24
+ * configured key, anything shaped like a LiteLLM key, any long hex run and any
25
+ * embedded `data:` URL are all removed.
26
+ */
27
+ export declare function redactCredentials(message: string, apiKey: string): string;
19
28
  /**
20
29
  * The shared half of every "System One" decision provider — a model that takes
21
30
  * one `state` plus typed questions and returns one typed answer each, with no
@@ -52,7 +61,7 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
52
61
  protected missingConfigMessage(): string | undefined;
53
62
  protected abstract decisionEndpoint(): string;
54
63
  protected abstract decisionHeaders(): Record<string, string>;
55
- protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string): Record<string, unknown>;
64
+ protected abstract buildDecisionBody(state: DecisionState, questions: Record<string, Record<string, unknown>>, model: string, media?: DecisionPreparedMedia): Record<string, unknown>;
56
65
  protected abstract parseDecisionError(status: number, payload: unknown, requestId: string | undefined): DecisionError;
57
66
  protected abstract readRequestId(headers: Headers): string | undefined;
58
67
  /**
@@ -87,6 +96,16 @@ export declare abstract class SystemOneDecisionProvider extends BaseProvider {
87
96
  * which returns null instead.
88
97
  */
89
98
  decide(request: DecisionRequest): Promise<DecisionResult>;
99
+ /**
100
+ * Turn the request's images and video into data URLs, or refuse it. Refused
101
+ * as a non-retryable `invalid_request`: resending the same media cannot help.
102
+ */
103
+ private prepareMedia;
104
+ /**
105
+ * The encoded body, not just the media, is what the server's size limit
106
+ * applies to.
107
+ */
108
+ private assertWithinRequestBytes;
90
109
  /**
91
110
  * Refuse a request the model cannot read in full. An encoder cuts the state
92
111
  * off past its window without saying so, so without this a decision would