@juspay/neurolink 12.12.0 → 12.12.1

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.
@@ -13,6 +13,7 @@
13
13
  * share this: the v3 result shape (`content` parts, `finishReason`, `usage`) is
14
14
  * the same across Anthropic, the OpenAI-compatible family and SageMaker.
15
15
  */
16
+ import { logger } from "../utils/logger.js";
16
17
  import { guardToolExecutor } from "./toolExecutionGuards.js";
17
18
  /**
18
19
  * Narrow a model handle to the delegating shape this loop drives.
@@ -104,10 +105,19 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
104
105
  let steps = 0;
105
106
  // One bounded recovery re-ask per turn; see the empty-tool-calls branch.
106
107
  let reasked = false;
108
+ // True only while the NEXT step is the recovery re-ask. `reasked` stays set
109
+ // for the rest of the turn, so it cannot distinguish "this step is the
110
+ // re-ask" from "the re-ask already happened" — and degrading a later,
111
+ // unrelated failure would swallow a real error.
112
+ let reaskPending = false;
113
+ // The turn as it stood before the re-ask. The re-ask is a bonus request on
114
+ // top of a call that already produced a result, so if it fails the honest
115
+ // answer is that result — not a thrown turn.
116
+ let preReask;
107
117
  const hasTools = Boolean(args.tools && args.tools.length > 0);
108
118
  for (let step = 0; step < args.maxSteps; step++) {
109
119
  steps = step + 1;
110
- const res = await args.runStep(() => args.doGenerate({
120
+ const runThisStep = () => args.runStep(() => args.doGenerate({
111
121
  prompt: args.conversation,
112
122
  ...(args.tools && args.tools.length > 0 ? { tools: args.tools } : {}),
113
123
  // The v3 call option is an OBJECT — `{ type: "none" }`. Passing the
@@ -120,7 +130,9 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
120
130
  : args.toolChoice !== undefined
121
131
  ? { toolChoice: args.toolChoice }
122
132
  : {}),
123
- ...(args.responseFormat ? { responseFormat: args.responseFormat } : {}),
133
+ ...(args.responseFormat
134
+ ? { responseFormat: args.responseFormat }
135
+ : {}),
124
136
  ...(args.providerOptions
125
137
  ? { providerOptions: args.providerOptions }
126
138
  : {}),
@@ -132,6 +144,30 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
132
144
  : {}),
133
145
  ...(args.abortSignal ? { abortSignal: args.abortSignal } : {}),
134
146
  }));
147
+ let res;
148
+ try {
149
+ res = await runThisStep();
150
+ }
151
+ catch (stepError) {
152
+ // Ported from GenerationHandler.recoverEmptyToolCallsFinish's catch: a
153
+ // failed re-ask hands back the turn that already succeeded rather than
154
+ // turning a degraded turn into a thrown one. The native port made the
155
+ // re-ask a `continue`, so the failure surfaced from the NEXT step's
156
+ // doGenerate and propagated instead.
157
+ if (reaskPending && preReask) {
158
+ logger.warn("toolChoice: none re-ask failed; returning the original result", {
159
+ error: stepError instanceof Error
160
+ ? stepError.message
161
+ : String(stepError),
162
+ });
163
+ text = preReask.text;
164
+ finishReason = preReask.finishReason;
165
+ rawFinishReason = preReask.rawFinishReason;
166
+ break;
167
+ }
168
+ throw stepError;
169
+ }
170
+ reaskPending = false;
135
171
  const parts = asParts(res.content);
136
172
  // Each step REPLACES the text rather than appending: the final step's
137
173
  // answer is the turn's answer, matching what generateText reported.
@@ -177,6 +213,8 @@ export async function runNativeGenerateLoop(args, toolExecutionSummaries) {
177
213
  step + 1 < args.maxSteps;
178
214
  if (emptyToolCallsFinish) {
179
215
  reasked = true;
216
+ reaskPending = true;
217
+ preReask = { text, finishReason, rawFinishReason };
180
218
  args.conversation.push({ role: "assistant", content: parts });
181
219
  continue;
182
220
  }
@@ -20,9 +20,6 @@
20
20
  import type { AIProviderName } from "../constants/enums.js";
21
21
  import { BaseProvider } from "../core/baseProvider.js";
22
22
  import type { LanguageModel, OpenAICompatBuildBodyArgs, OpenAICompatChatMessage, OpenAICompatChatRequest, OpenAICompatChatTool, OpenAICompatResponseFormat, OpenAICompatStreamLifecycleListeners, Schema, EnhancedGenerateResult, TextGenerationOptions, ValidationSchema, StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
23
- /**
24
- * Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
25
- */
26
23
  export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider {
27
24
  protected config: {
28
25
  baseURL: string;
@@ -36,6 +36,7 @@ import { resolveRequestKind } from "../core/resolveRequestKind.js";
36
36
  import { appendJsonSchemaInstruction, hasNativeDoGenerate, runNativeGenerateLoop, } from "../core/nativeGenerateLoop.js";
37
37
  import { resolveToolExecutionRecords } from "../core/toolExecutionRecorder.js";
38
38
  import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
39
+ import { coerceJsonToSchema, schemaAccepts } from "../utils/json/coerce.js";
39
40
  import { resolveToolChoice } from "../utils/toolChoice.js";
40
41
  import { transformToolExecutions } from "../utils/transformationUtils.js";
41
42
  import { withProviderRetry } from "../utils/providerRetry.js";
@@ -52,6 +53,19 @@ const WINDOW_FIT_MARGIN_TOKENS = 512;
52
53
  /**
53
54
  * Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
54
55
  */
56
+ /**
57
+ * Did the model's text yield an object the caller's schema accepts?
58
+ *
59
+ * This is the trigger for the prompt-side structured-output fallback. It asks
60
+ * the question the ai-package's structured-output parser used to ask by
61
+ * throwing: did the native `response_format` attempt actually produce the
62
+ * object. A schema we cannot validate with accepts everything, so an unknown
63
+ * schema never forces a pointless second request.
64
+ */
65
+ const yieldsSchemaValidObject = (text, schema) => {
66
+ const coerced = coerceJsonToSchema(text, schema);
67
+ return coerced !== null && schemaAccepts(schema, coerced.structuredData);
68
+ };
55
69
  export class OpenAIChatCompletionsProvider extends BaseProvider {
56
70
  config;
57
71
  resolvedModel;
@@ -855,14 +869,42 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
855
869
  logger.warn(`[${this.providerName}] provider rejected response_format — retrying with the schema in the system prompt`, { provider: this.providerName, model: modelId });
856
870
  loop = await runLoop(appendJsonSchemaInstruction(conversation, responseFormat.schema), undefined);
857
871
  }
872
+ // The vendor can also IGNORE `response_format` and answer in prose without
873
+ // erroring at all — GMI Cloud's MiniMax endpoint does exactly that, and it
874
+ // is the case the fallback was written for. On the ai-package path the
875
+ // structured-output parser threw on the unparseable answer, so the catch
876
+ // above was reached; the native loop has no such parser, so the silent
877
+ // case sailed through and handed the caller prose. Same recovery, keyed on
878
+ // the result rather than on an exception.
879
+ if (responseFormat !== undefined &&
880
+ options.schema !== undefined &&
881
+ !yieldsSchemaValidObject(loop.text, options.schema)) {
882
+ logger.warn(`[${this.providerName}] response_format did not yield a schema-valid object — retrying with the schema in the system prompt`, { provider: this.providerName, model: modelId });
883
+ loop = await runLoop(appendJsonSchemaInstruction(conversation, responseFormat.schema), undefined);
884
+ }
858
885
  const { text, finishReason, toolsUsed } = loop;
859
886
  const inputTokens = loop.inputTokens;
860
887
  const outputTokens = loop.outputTokens;
888
+ // stopReason / stepsUsed parity with the other native loops (Vertex
889
+ // Gemini / Claude / Bedrock) and with the ai-package path this replaced.
890
+ // Without them a consumer cannot tell a completed turn from one the step
891
+ // cap truncated: the turn that ends on a `tool-calls` finish with the
892
+ // budget spent is exactly the case the caller configured `maxSteps` to
893
+ // bound, and reporting it as a plain completion hides that.
894
+ const stepsUsed = loop.steps;
895
+ const stopReason = stepsUsed >= (options.maxSteps || DEFAULT_MAX_STEPS) &&
896
+ finishReason === "tool-calls"
897
+ ? "step-cap"
898
+ : finishReason === "error"
899
+ ? "provider-error"
900
+ : "completed";
861
901
  const enhanced = {
862
902
  content: text,
863
903
  provider: this.providerName,
864
904
  model: modelId,
865
905
  finishReason,
906
+ stopReason,
907
+ stepsUsed,
866
908
  usage: {
867
909
  input: inputTokens,
868
910
  output: outputTokens,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@juspay/neurolink",
3
- "version": "12.12.0",
3
+ "version": "12.12.1",
4
4
  "packageManager": "pnpm@10.15.1",
5
5
  "description": "TypeScript AI SDK with 24+ LLM providers behind one consistent API. MCP-native (connect any MCP server), voice TTS/STT/realtime, RAG, agents, memory, context compaction. OpenAI · Anthropic · Gemini · Bedrock · Azure · Ollama · DeepSeek · NVIDIA NIM and more.",
6
6
  "author": {