@juspay/neurolink 12.37.4 → 12.37.5

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.
@@ -262,12 +262,21 @@ export declare abstract class BaseProvider implements AIProvider {
262
262
  * and a retired default model stops degrading to the next live one in the
263
263
  * catalog — which is exactly what the provider contract gate checks.
264
264
  */
265
- protected runGenerateWithModelFallback(attempt: () => Promise<EnhancedGenerateResult | null>, callerOwnsFallback: boolean): Promise<EnhancedGenerateResult | null>;
265
+ protected runGenerateWithModelFallback(attempt: () => Promise<EnhancedGenerateResult | null>, callerOwnsFallback: boolean, options?: TextGenerationOptions): Promise<EnhancedGenerateResult | null>;
266
266
  /**
267
267
  * Model ids to try when this provider rejects its current model as invalid.
268
268
  * Empty by default; catalog-driven providers return their `fallbacks`.
269
269
  */
270
270
  protected getModelFallbacks(): string[];
271
+ /**
272
+ * The fallbacks an invalid-model retry may try for this request. A catalog
273
+ * orders `fallbacks` for text, so a request carrying an image keeps only the
274
+ * vision-capable ones: a text-only candidate fails the vision guard, and
275
+ * that error names the fallback instead of the retired model the caller
276
+ * asked for. With no vision-capable candidate left, the caller gets the
277
+ * original invalid-model error.
278
+ */
279
+ protected modelFallbacksFor(options: TextGenerationOptions | StreamOptions, requestedModel: string): string[];
271
280
  /**
272
281
  * Alias for generate method - implements AIProvider interface
273
282
  */
@@ -46,6 +46,7 @@ import { StreamHandler } from "./modules/StreamHandler.js";
46
46
  import { TelemetryHandler } from "./modules/TelemetryHandler.js";
47
47
  import { ToolsManager } from "./modules/ToolsManager.js";
48
48
  import { Utilities } from "./modules/Utilities.js";
49
+ import { ProviderImageAdapter } from "../adapters/providerImageAdapter.js";
49
50
  import { generateOnceNative } from "../utils/nativeSingleShot.js";
50
51
  import { validateExecutionControl } from "../utils/parameterValidation.js";
51
52
  import { extractTokenUsage } from "../utils/tokenUtils.js";
@@ -107,6 +108,18 @@ function isProviderErrorClassified(error) {
107
108
  }
108
109
  return error[PROVIDER_ERROR_CLASSIFIED] === true;
109
110
  }
111
+ function requestCarriesImages(options) {
112
+ const input = options.input;
113
+ if (!input) {
114
+ return false;
115
+ }
116
+ if ((input.images?.length ?? 0) > 0) {
117
+ return true;
118
+ }
119
+ return ("content" in input &&
120
+ Array.isArray(input.content) &&
121
+ input.content.some((part) => part.type === "image"));
122
+ }
110
123
  export class BaseProvider {
111
124
  // Not `readonly` because providers that auto-discover the model from a
112
125
  // /v1/models endpoint (lm-studio, llamacpp) need to update modelName after
@@ -523,9 +536,7 @@ export class BaseProvider {
523
536
  throw error;
524
537
  }
525
538
  const requestedModel = provider.modelName;
526
- const candidates = provider
527
- .getModelFallbacks()
528
- .filter((model) => model !== requestedModel);
539
+ const candidates = provider.modelFallbacksFor(options, requestedModel);
529
540
  for (const candidate of candidates) {
530
541
  logger.warn(`[${provider.providerName}] model "${requestedModel}" was rejected as invalid — retrying stream with fallback "${candidate}". This provider's catalog entry is stale; run "pnpm run check:models".`);
531
542
  provider.refreshHandlersForModel(candidate);
@@ -671,7 +682,7 @@ export class BaseProvider {
671
682
  return undefined;
672
683
  }
673
684
  const requestedModel = this.modelName;
674
- const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
685
+ const candidates = this.modelFallbacksFor(options, requestedModel);
675
686
  for (const candidate of candidates) {
676
687
  logger.warn(`[${this.providerName}] model "${requestedModel}" was rejected as invalid — retrying stream with fallback "${candidate}". This provider's catalog entry is stale; run "pnpm run check:models".`);
677
688
  this.refreshHandlersForModel(candidate);
@@ -1493,7 +1504,7 @@ export class BaseProvider {
1493
1504
  // or a router that retries on its own) pass the flag on both paths;
1494
1505
  // TextGenerationOptions declares it, so a plain read is enough here.
1495
1506
  const callerOwnsFallback = options.disableInternalFallback === true;
1496
- return await this.runGenerateWithModelFallback(attempt, callerOwnsFallback);
1507
+ return await this.runGenerateWithModelFallback(attempt, callerOwnsFallback, options);
1497
1508
  }
1498
1509
  /**
1499
1510
  * Models the catalog names are the models a vendor served when the entry was
@@ -1520,7 +1531,7 @@ export class BaseProvider {
1520
1531
  * and a retired default model stops degrading to the next live one in the
1521
1532
  * catalog — which is exactly what the provider contract gate checks.
1522
1533
  */
1523
- async runGenerateWithModelFallback(attempt, callerOwnsFallback) {
1534
+ async runGenerateWithModelFallback(attempt, callerOwnsFallback, options) {
1524
1535
  const requestedModel = this.modelName;
1525
1536
  try {
1526
1537
  return await attempt();
@@ -1529,7 +1540,9 @@ export class BaseProvider {
1529
1540
  if (callerOwnsFallback || !isInvalidModelError(error)) {
1530
1541
  throw error;
1531
1542
  }
1532
- const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
1543
+ const candidates = options
1544
+ ? this.modelFallbacksFor(options, requestedModel)
1545
+ : this.getModelFallbacks().filter((model) => model !== requestedModel);
1533
1546
  if (candidates.length === 0) {
1534
1547
  throw error;
1535
1548
  }
@@ -1563,6 +1576,21 @@ export class BaseProvider {
1563
1576
  getModelFallbacks() {
1564
1577
  return [];
1565
1578
  }
1579
+ /**
1580
+ * The fallbacks an invalid-model retry may try for this request. A catalog
1581
+ * orders `fallbacks` for text, so a request carrying an image keeps only the
1582
+ * vision-capable ones: a text-only candidate fails the vision guard, and
1583
+ * that error names the fallback instead of the retired model the caller
1584
+ * asked for. With no vision-capable candidate left, the caller gets the
1585
+ * original invalid-model error.
1586
+ */
1587
+ modelFallbacksFor(options, requestedModel) {
1588
+ const candidates = this.getModelFallbacks().filter((model) => model !== requestedModel);
1589
+ if (!requestCarriesImages(options)) {
1590
+ return candidates;
1591
+ }
1592
+ return candidates.filter((model) => ProviderImageAdapter.supportsVision(this.providerName, model));
1593
+ }
1566
1594
  /**
1567
1595
  * Alias for generate method - implements AIProvider interface
1568
1596
  */
@@ -38,7 +38,7 @@ export const anthropicManifest = {
38
38
  samplingParams: false, // matches SAMPLING_PARAM_REJECTING_FAMILIES /opus[-_.]?5(?![0-9])/i
39
39
  },
40
40
  "claude-sonnet-5-5": {
41
- aliases: ["sonnet-5.5"],
41
+ aliases: ["sonnet-5.5", "claude-sonnet"],
42
42
  displayName: "Claude Sonnet 5.5",
43
43
  contextWindow: 1_000_000,
44
44
  maxOutputTokens: 128_000,
@@ -66,7 +66,7 @@ export const anthropicManifest = {
66
66
  samplingParams: false, // matches SAMPLING_PARAM_REJECTING_FAMILIES /fable/i
67
67
  },
68
68
  "claude-sonnet-5": {
69
- aliases: ["sonnet-5", "claude-sonnet"],
69
+ aliases: ["sonnet-5"],
70
70
  displayName: "Claude Sonnet 5",
71
71
  contextWindow: 1_000_000,
72
72
  // Was 64_000; corrected to 128_000 to match the official spec
@@ -4,7 +4,7 @@
4
4
  * Part of Phase 4.1 - Models Command System
5
5
  */
6
6
  import { AIProviderName } from "../constants/enums.js";
7
- import type { JsonValue, ModelCapabilities, ModelInfo } from "../types/index.js";
7
+ import type { ClaudeDisabledThinkingReplacement, JsonValue, ModelCapabilities, ModelInfo } from "../types/index.js";
8
8
  /**
9
9
  * Public MODEL_REGISTRY: the manifest-derived entries merged over the
10
10
  * hand-authored LEGACY_MODEL_REGISTRY base. Spread order matters — manifest
@@ -90,6 +90,17 @@ export declare function resolveSamplingParams(provider: string, model: string |
90
90
  temperature?: number;
91
91
  topP?: number;
92
92
  };
93
+ /**
94
+ * Whether a Claude model accepts a forced `tool_choice` (`any` or a named
95
+ * `tool`). Unknown models default to accepting it.
96
+ */
97
+ export declare function modelSupportsForcedToolChoice(model: string | undefined): boolean;
98
+ /**
99
+ * What to send instead of `thinking: {type: "disabled"}` to a Claude model.
100
+ * `between_tools` is refused at `xhigh` and `max` effort, so those fall back
101
+ * to adaptive thinking (`omit`).
102
+ */
103
+ export declare function claudeDisabledThinkingReplacement(model: string | undefined, effort: string | undefined): ClaudeDisabledThinkingReplacement;
93
104
  /**
94
105
  * Get models by provider
95
106
  */
@@ -1168,14 +1168,14 @@ const LEGACY_MODEL_REGISTRY = {
1168
1168
  translation: 9,
1169
1169
  summarization: 9,
1170
1170
  },
1171
- aliases: ["sonnet-5", "claude-sonnet-latest"],
1171
+ aliases: ["sonnet-5"],
1172
1172
  deprecated: false,
1173
1173
  isLocal: false,
1174
1174
  releaseDate: "2026-06-30",
1175
1175
  category: "general",
1176
1176
  },
1177
- // "claude-sonnet-latest" stays on CLAUDE_SONNET_5 until someone decides to
1178
- // move it; this entry sits after that one so resolver ties still go to it.
1177
+ // Owns "claude-sonnet-latest", but sits after CLAUDE_SONNET_5 so resolver
1178
+ // score ties still go to Sonnet 5.
1179
1179
  [AnthropicModels.CLAUDE_SONNET_5_5]: {
1180
1180
  id: AnthropicModels.CLAUDE_SONNET_5_5,
1181
1181
  name: "Claude Sonnet 5.5",
@@ -1210,7 +1210,7 @@ const LEGACY_MODEL_REGISTRY = {
1210
1210
  translation: 9,
1211
1211
  summarization: 9,
1212
1212
  },
1213
- aliases: ["sonnet-5.5"],
1213
+ aliases: ["sonnet-5.5", "claude-sonnet-latest"],
1214
1214
  deprecated: false,
1215
1215
  isLocal: false,
1216
1216
  releaseDate: "2026-09-28",
@@ -1374,7 +1374,7 @@ const LEGACY_MODEL_REGISTRY = {
1374
1374
  translation: 8,
1375
1375
  summarization: 8,
1376
1376
  },
1377
- // "claude-sonnet-latest" now belongs to CLAUDE_SONNET_5 — see the note
1377
+ // "claude-sonnet-latest" now belongs to CLAUDE_SONNET_5_5 — see the note
1378
1378
  // on CLAUDE_OPUS_4_5's aliases.
1379
1379
  aliases: ["claude-4.5-sonnet", "sonnet-4.5"],
1380
1380
  deprecated: false,
@@ -2846,6 +2846,58 @@ export function resolveSamplingParams(provider, model, params, site) {
2846
2846
  }
2847
2847
  return {};
2848
2848
  }
2849
+ /**
2850
+ * Claude models that answer a forced `tool_choice` (`any` or `tool`) with a
2851
+ * 400: "tool_choice: type "tool" and "any" are not supported for this model."
2852
+ * Sonnet 5.5 and Opus 5.5 were probed live on Vertex; the Fable 5.1 docs state
2853
+ * the same, and Mythos 5.1 shares Fable 5.1's capabilities. The same families
2854
+ * also refuse `thinking: {type: "disabled"}`.
2855
+ */
2856
+ const FORCED_TOOL_CHOICE_REJECTING_FAMILIES = [
2857
+ /sonnet[-_.]?5[-_.]5(?![0-9])/i,
2858
+ /opus[-_.]?5[-_.]5(?![0-9])/i,
2859
+ /(?:fable|mythos)[-_.]?5[-_.]1(?![0-9])/i,
2860
+ ];
2861
+ /**
2862
+ * Of those, the ones that accept `thinking: {type: "between_tools"}` as their
2863
+ * lowest setting. The rest think adaptively, always on, and take no
2864
+ * `thinking` field in its place.
2865
+ */
2866
+ const BETWEEN_TOOLS_THINKING_FAMILIES = [
2867
+ /sonnet[-_.]?5[-_.]5(?![0-9])/i,
2868
+ ];
2869
+ function matchesClaudeFamily(families, model) {
2870
+ const normalizedModel = (model ?? "").toLowerCase();
2871
+ if (!normalizedModel) {
2872
+ return false;
2873
+ }
2874
+ const modelId = MODEL_ALIASES[normalizedModel] ?? normalizedModel;
2875
+ return families.some((family) => family.test(modelId) || family.test(normalizedModel));
2876
+ }
2877
+ /**
2878
+ * Whether a Claude model accepts a forced `tool_choice` (`any` or a named
2879
+ * `tool`). Unknown models default to accepting it.
2880
+ */
2881
+ export function modelSupportsForcedToolChoice(model) {
2882
+ return !matchesClaudeFamily(FORCED_TOOL_CHOICE_REJECTING_FAMILIES, model);
2883
+ }
2884
+ /**
2885
+ * What to send instead of `thinking: {type: "disabled"}` to a Claude model.
2886
+ * `between_tools` is refused at `xhigh` and `max` effort, so those fall back
2887
+ * to adaptive thinking (`omit`).
2888
+ */
2889
+ export function claudeDisabledThinkingReplacement(model, effort) {
2890
+ if (!matchesClaudeFamily(FORCED_TOOL_CHOICE_REJECTING_FAMILIES, model)) {
2891
+ return "keep";
2892
+ }
2893
+ const level = effort?.toLowerCase();
2894
+ if (matchesClaudeFamily(BETWEEN_TOOLS_THINKING_FAMILIES, model) &&
2895
+ level !== "xhigh" &&
2896
+ level !== "max") {
2897
+ return "between_tools";
2898
+ }
2899
+ return "omit";
2900
+ }
2849
2901
  /**
2850
2902
  * Get models by provider
2851
2903
  */
@@ -126,7 +126,7 @@ export class AmazonSageMakerProvider extends BaseProvider {
126
126
  options.disableInternalFallback === true;
127
127
  // The native loop bypasses BaseProvider.executeGeneration, so the turn
128
128
  // budget has to be composed here or it stops existing for this provider.
129
- return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback);
129
+ return this.runGenerateWithModelFallback(() => this.withTurnTimeout({ ...options, tools: mergedTools }, this.getDescriptorGenerateMs(), (timedOptions) => this.executeNativeGenerate(timedOptions)), callerOwnsFallback, options);
130
130
  }
131
131
  async executeNativeGenerate(options) {
132
132
  const startTime = Date.now();
@@ -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