@juspay/neurolink 9.67.1 → 9.67.2

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.
@@ -1,4 +1,3 @@
1
- import { createParser } from "eventsource-parser";
2
1
  import { BaseProvider } from "../core/baseProvider.js";
3
2
  import { DEFAULT_MAX_STEPS } from "../core/constants.js";
4
3
  import { streamAnalyticsCollector } from "../core/streamAnalytics.js";
@@ -7,11 +6,11 @@ import { AuthenticationError, InvalidModelError, NetworkError, ProviderError, Ra
7
6
  import { logger } from "../utils/logger.js";
8
7
  import { NoOutputGeneratedError } from "../utils/generationErrors.js";
9
8
  import { buildNoOutputSentinel, stampNoOutputSpan, } from "../utils/noOutputSentinel.js";
10
- import { convertZodToJsonSchema } from "../utils/schemaConversion.js";
11
9
  import { composeAbortSignals, createTimeoutController, mergeAbortSignals, TimeoutError, } from "../utils/timeout.js";
12
10
  import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
13
11
  import { resolveToolChoice } from "../utils/toolChoice.js";
14
12
  import { transformToolExecutions } from "../utils/transformationUtils.js";
13
+ import { buildAPIError, buildBody, buildToolsForOpenAI, createChunkQueue, createDeferredAnalytics, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
15
14
  const FALLBACK_OPENAI_COMPATIBLE_MODEL = "gpt-3.5-turbo";
16
15
  const getOpenAICompatibleConfig = () => {
17
16
  const baseURL = process.env.OPENAI_COMPATIBLE_BASE_URL;
@@ -32,468 +31,12 @@ const getDefaultOpenAICompatibleModel = () => {
32
31
  // =============================================================================
33
32
  // Direct HTTP client for OpenAI chat-completions.
34
33
  //
35
- // Replaces both @ai-sdk/openai (the OpenAI wrapper) and streamText (the
36
- // orchestration). Tool execution, multi-step looping, and SSE parsing are
37
- // all inlined below. Nothing in this module imports from "ai" or
38
- // "@ai-sdk/provider" — the openai-compatible path is a clean cut.
34
+ // Wire-format converters, SSE parser, request builder, and error builder all
35
+ // live in ./openaiChatCompletionsClient.ts so providers that share the OpenAI
36
+ // chat-completions shape (litellm, etc.) can reuse them without duplication.
37
+ // Nothing in this module imports from "ai" or "@ai-sdk/provider" — the
38
+ // openai-compatible path is a clean cut.
39
39
  // =============================================================================
40
- const stripTrailingSlash = (s) => s.replace(/\/+$/, "");
41
- const messageBuilderToOpenAI = (messages) => {
42
- const out = [];
43
- for (const msg of messages) {
44
- switch (msg.role) {
45
- case "system":
46
- out.push({
47
- role: "system",
48
- content: typeof msg.content === "string"
49
- ? msg.content
50
- : safeStringify(msg.content),
51
- });
52
- break;
53
- case "user":
54
- out.push({
55
- role: "user",
56
- content: convertContentForOpenAI(msg.content),
57
- });
58
- break;
59
- case "assistant": {
60
- const parts = Array.isArray(msg.content) ? msg.content : [msg.content];
61
- const text = [];
62
- const toolCalls = [];
63
- for (const part of parts) {
64
- if (part && typeof part === "object") {
65
- const p = part;
66
- if (p.type === "text") {
67
- text.push({
68
- type: "text",
69
- text: part.text ?? "",
70
- });
71
- }
72
- else if (p.type === "tool-call") {
73
- const tc = part;
74
- toolCalls.push({
75
- id: tc.toolCallId ?? "",
76
- type: "function",
77
- function: {
78
- name: tc.toolName ?? "",
79
- arguments: stringifyToolInput(tc.input),
80
- },
81
- });
82
- }
83
- }
84
- else if (typeof part === "string") {
85
- text.push({ type: "text", text: part });
86
- }
87
- }
88
- const flat = text.length === 0
89
- ? null
90
- : text.length === 1 && text[0].type === "text"
91
- ? text[0].text
92
- : text;
93
- out.push({
94
- role: "assistant",
95
- content: flat,
96
- ...(toolCalls.length > 0 ? { tool_calls: toolCalls } : {}),
97
- });
98
- break;
99
- }
100
- case "tool": {
101
- // V3 tool messages carry `{ toolCallId, output }` per content[] entry,
102
- // not at the top-level. Emit one OpenAI `role: "tool"` message per
103
- // tool-result part so the model can correlate by tool_call_id.
104
- if (Array.isArray(msg.content)) {
105
- for (const part of msg.content) {
106
- if (!part || typeof part !== "object") {
107
- continue;
108
- }
109
- const p = part;
110
- if (p.type === "tool-result") {
111
- out.push({
112
- role: "tool",
113
- tool_call_id: p.toolCallId ?? "",
114
- content: stringifyToolOutput(p.output),
115
- });
116
- }
117
- }
118
- }
119
- else if (typeof msg.content === "string") {
120
- // Legacy / flat-string callers (not V3): forward as-is.
121
- out.push({
122
- role: "tool",
123
- tool_call_id: msg.toolCallId ?? "",
124
- content: msg.content,
125
- });
126
- }
127
- break;
128
- }
129
- }
130
- }
131
- return out;
132
- };
133
- const convertContentForOpenAI = (content) => {
134
- if (typeof content === "string") {
135
- return content;
136
- }
137
- if (!Array.isArray(content)) {
138
- return safeStringify(content);
139
- }
140
- const out = [];
141
- for (const part of content) {
142
- if (typeof part === "string") {
143
- out.push({ type: "text", text: part });
144
- continue;
145
- }
146
- if (!part || typeof part !== "object") {
147
- continue;
148
- }
149
- const p = part;
150
- if (p.type === "text") {
151
- out.push({
152
- type: "text",
153
- text: part.text ?? "",
154
- });
155
- }
156
- else if (p.type === "image" || p.type === "image_url") {
157
- const data = part.image ??
158
- part.data ??
159
- part.url;
160
- const url = imageDataToURL(data);
161
- if (url) {
162
- out.push({ type: "image_url", image_url: { url } });
163
- }
164
- }
165
- }
166
- if (out.length === 1 && out[0].type === "text") {
167
- return out[0].text;
168
- }
169
- return out;
170
- };
171
- const imageDataToURL = (data) => {
172
- if (typeof data === "string") {
173
- if (data.startsWith("data:") || /^https?:\/\//i.test(data)) {
174
- return data;
175
- }
176
- return `data:image/png;base64,${data}`;
177
- }
178
- if (data instanceof URL) {
179
- return data.toString();
180
- }
181
- if (data instanceof Uint8Array) {
182
- return `data:image/png;base64,${Buffer.from(data).toString("base64")}`;
183
- }
184
- return undefined;
185
- };
186
- const stringifyToolInput = (input) => {
187
- if (typeof input === "string") {
188
- return input;
189
- }
190
- try {
191
- return JSON.stringify(input ?? {});
192
- }
193
- catch {
194
- return "{}";
195
- }
196
- };
197
- const safeStringify = (value) => {
198
- try {
199
- return JSON.stringify(value ?? "");
200
- }
201
- catch {
202
- return String(value ?? "");
203
- }
204
- };
205
- // V3 tool-result `output` is a tagged union ({type:"text"|"json"|...}).
206
- // Serialize each variant the way an OpenAI-compatible endpoint expects
207
- // to read it as the `content` of a `role: "tool"` message.
208
- const stringifyToolOutput = (output) => {
209
- if (output === null || output === undefined) {
210
- return "";
211
- }
212
- if (typeof output === "string") {
213
- return output;
214
- }
215
- if (typeof output !== "object") {
216
- return String(output);
217
- }
218
- const o = output;
219
- switch (o.type) {
220
- case "text":
221
- return typeof o.value === "string" ? o.value : safeStringify(o.value);
222
- case "json":
223
- return safeStringify(o.value);
224
- case "execution-denied":
225
- return `Tool execution denied${o.reason ? `: ${o.reason}` : ""}`;
226
- case "error-text":
227
- return typeof o.value === "string" ? o.value : safeStringify(o.value);
228
- case "error-json":
229
- return safeStringify(o.value);
230
- case "content":
231
- if (Array.isArray(o.value)) {
232
- return o.value
233
- .map((p) => {
234
- if (p &&
235
- typeof p === "object" &&
236
- p.type === "text") {
237
- return String(p.text ?? "");
238
- }
239
- return "";
240
- })
241
- .filter((s) => s.length > 0)
242
- .join("\n");
243
- }
244
- return "";
245
- default:
246
- // Plain output object (not a V3 tagged union) — just stringify.
247
- return safeStringify(output);
248
- }
249
- };
250
- const buildToolsForOpenAI = (tools) => {
251
- const entries = Object.entries(tools);
252
- if (entries.length === 0) {
253
- return undefined;
254
- }
255
- const out = [];
256
- for (const [name, tool] of entries) {
257
- const t = tool;
258
- const rawSchema = t.inputSchema ?? t.parameters;
259
- // tool.inputSchema may be a Zod schema, an AI SDK jsonSchema() wrapper,
260
- // or plain JSON Schema — convertZodToJsonSchema normalizes all three.
261
- // Sending raw Zod internals (with `_def`) gets rejected by most
262
- // OpenAI-compatible endpoints.
263
- const parameters = rawSchema
264
- ? convertZodToJsonSchema(rawSchema)
265
- : { type: "object", properties: {} };
266
- out.push({
267
- type: "function",
268
- function: {
269
- name,
270
- ...(t.description ? { description: t.description } : {}),
271
- parameters,
272
- },
273
- });
274
- }
275
- return out;
276
- };
277
- // V3 → OpenAI conversion helpers used by the non-streaming `doGenerate`
278
- // path that BaseProvider's `generate()` still drives via the AI SDK's
279
- // `generateText`. The streaming path doesn't need these — it consumes
280
- // NeuroLink-shaped options directly.
281
- const v3ToolsToOpenAI = (tools) => {
282
- if (!tools || tools.length === 0) {
283
- return undefined;
284
- }
285
- const out = [];
286
- for (const t of tools) {
287
- if (t.type === "function") {
288
- out.push({
289
- type: "function",
290
- function: {
291
- name: t.name,
292
- ...(t.description ? { description: t.description } : {}),
293
- parameters: t.inputSchema,
294
- ...(t.strict !== undefined ? { strict: t.strict } : {}),
295
- },
296
- });
297
- }
298
- // provider-defined V3 tools are silently dropped here — they have no
299
- // OpenAI chat-completions equivalent.
300
- }
301
- return out.length > 0 ? out : undefined;
302
- };
303
- const v3ToolChoiceToOpenAI = (choice) => {
304
- switch (choice.type) {
305
- case "auto":
306
- case "none":
307
- case "required":
308
- return choice.type;
309
- case "tool":
310
- return { type: "function", function: { name: choice.toolName } };
311
- }
312
- };
313
- const v3ResponseFormatToOpenAI = (rf) => {
314
- if (rf.type === "text") {
315
- return { type: "text" };
316
- }
317
- if (!rf.schema) {
318
- return { type: "json_object" };
319
- }
320
- return {
321
- type: "json_schema",
322
- json_schema: {
323
- name: rf.name ?? "response",
324
- schema: rf.schema,
325
- ...(rf.description ? { description: rf.description } : {}),
326
- strict: true,
327
- },
328
- };
329
- };
330
- const mapNeuroLinkToolChoice = (choice) => {
331
- if (!choice) {
332
- return undefined;
333
- }
334
- if (choice === "auto" || choice === "none" || choice === "required") {
335
- return choice;
336
- }
337
- if (typeof choice === "object" && choice !== null) {
338
- const c = choice;
339
- if (c.type === "tool" && c.toolName) {
340
- return { type: "function", function: { name: c.toolName } };
341
- }
342
- }
343
- return undefined;
344
- };
345
- const buildBody = (args) => {
346
- const { modelId, messages, options, tools, toolChoice, streaming, responseFormat, } = args;
347
- const body = {
348
- model: modelId,
349
- messages,
350
- ...(streaming ? { stream: true } : {}),
351
- ...(streaming ? { stream_options: { include_usage: true } } : {}),
352
- };
353
- if (options.maxTokens !== undefined && options.maxTokens !== null) {
354
- body.max_tokens = options.maxTokens;
355
- }
356
- if (options.temperature !== undefined && options.temperature !== null) {
357
- body.temperature = options.temperature;
358
- }
359
- if (options.topP !== undefined && options.topP !== null) {
360
- body.top_p = options.topP;
361
- }
362
- if (options.presencePenalty !== undefined &&
363
- options.presencePenalty !== null) {
364
- body.presence_penalty = options.presencePenalty;
365
- }
366
- if (options.frequencyPenalty !== undefined &&
367
- options.frequencyPenalty !== null) {
368
- body.frequency_penalty = options.frequencyPenalty;
369
- }
370
- if (options.seed !== undefined && options.seed !== null) {
371
- body.seed = options.seed;
372
- }
373
- if (options.stopSequences && options.stopSequences.length > 0) {
374
- body.stop = options.stopSequences;
375
- }
376
- if (tools) {
377
- body.tools = tools;
378
- }
379
- if (toolChoice !== undefined) {
380
- body.tool_choice = toolChoice;
381
- }
382
- if (responseFormat) {
383
- body.response_format = responseFormat;
384
- }
385
- return body;
386
- };
387
- const parseSSEStream = async (body, onTextDelta) => {
388
- const result = {
389
- text: "",
390
- toolCalls: new Map(),
391
- finishReason: null,
392
- usage: undefined,
393
- };
394
- const decoder = new TextDecoder();
395
- let parseErr;
396
- const handleEvent = (msg) => {
397
- const data = msg.data;
398
- if (!data || data === "[DONE]") {
399
- return;
400
- }
401
- let chunk;
402
- try {
403
- chunk = JSON.parse(data);
404
- }
405
- catch (err) {
406
- parseErr = err instanceof Error ? err : new Error(String(err));
407
- return;
408
- }
409
- if (chunk.usage) {
410
- result.usage = chunk.usage;
411
- }
412
- const choice = chunk.choices?.[0];
413
- if (!choice) {
414
- return;
415
- }
416
- const delta = choice.delta;
417
- if (delta?.content) {
418
- result.text += delta.content;
419
- onTextDelta(delta.content);
420
- }
421
- if (delta?.tool_calls) {
422
- for (const tc of delta.tool_calls) {
423
- let state = result.toolCalls.get(tc.index);
424
- if (!state) {
425
- state = {
426
- id: tc.id ?? `call_${tc.index}_${Date.now()}`,
427
- name: tc.function?.name ?? "",
428
- argsBuffered: "",
429
- };
430
- result.toolCalls.set(tc.index, state);
431
- }
432
- else if (tc.id) {
433
- state.id = tc.id;
434
- }
435
- if (tc.function?.name) {
436
- state.name = tc.function.name;
437
- }
438
- if (tc.function?.arguments) {
439
- state.argsBuffered += tc.function.arguments;
440
- }
441
- }
442
- }
443
- if (choice.finish_reason) {
444
- result.finishReason = choice.finish_reason;
445
- }
446
- };
447
- const parser = createParser({ onEvent: handleEvent });
448
- const reader = body.getReader();
449
- try {
450
- for (;;) {
451
- const { done, value } = await reader.read();
452
- if (done) {
453
- break;
454
- }
455
- parser.feed(decoder.decode(value, { stream: true }));
456
- }
457
- parser.feed(decoder.decode());
458
- }
459
- finally {
460
- reader.releaseLock();
461
- }
462
- if (parseErr) {
463
- throw parseErr;
464
- }
465
- return result;
466
- };
467
- const buildAPIError = async (url, body, res) => {
468
- let bodyText;
469
- let parsed;
470
- try {
471
- bodyText = await res.text();
472
- parsed = bodyText
473
- ? JSON.parse(bodyText)
474
- : undefined;
475
- }
476
- catch {
477
- parsed = undefined;
478
- }
479
- const msg = parsed?.error?.message ??
480
- `OpenAI-compatible request failed with status ${res.status}`;
481
- const err = new Error(msg);
482
- err.statusCode = res.status;
483
- err.url = url;
484
- // Redacted summary only — never attach raw prompts, tool definitions, or
485
- // tool arguments to the thrown error. Anything serialized by upstream
486
- // logging would leak them otherwise.
487
- err.requestBody = {
488
- model: body.model,
489
- stream: body.stream === true,
490
- tool_count: body.tools?.length ?? 0,
491
- };
492
- if (bodyText !== undefined) {
493
- err.responseBody = bodyText;
494
- }
495
- return err;
496
- };
497
40
  // =============================================================================
498
41
  // Provider
499
42
  // =============================================================================
@@ -1186,57 +729,4 @@ export class OpenAICompatibleProvider extends BaseProvider {
1186
729
  ];
1187
730
  }
1188
731
  }
1189
- // Deferred-promise pair for `usage` and `finishReason` so the analytics
1190
- // collector resolves with the actual aggregated values after the multi-step
1191
- // loop ends, not the zeros they had at result-construction time.
1192
- const createDeferredAnalytics = () => {
1193
- let resolveUsage = () => { };
1194
- const usagePromise = new Promise((r) => {
1195
- resolveUsage = r;
1196
- });
1197
- let resolveFinish = () => { };
1198
- const finishPromise = new Promise((r) => {
1199
- resolveFinish = r;
1200
- });
1201
- return { usagePromise, finishPromise, resolveUsage, resolveFinish };
1202
- };
1203
- // Single-producer / single-consumer chunk queue. The streaming loop pushes
1204
- // `{content}` deltas as they arrive from SSE and a final `{done:true}` when
1205
- // it finishes; the consumer's AsyncIterable pulls from `nextChunk()`.
1206
- const createChunkQueue = () => {
1207
- const chunkQueue = [];
1208
- let pendingResolve;
1209
- const pushChunk = (c) => {
1210
- if (pendingResolve) {
1211
- const r = pendingResolve;
1212
- pendingResolve = undefined;
1213
- r(c);
1214
- }
1215
- else {
1216
- chunkQueue.push(c);
1217
- }
1218
- };
1219
- const nextChunk = () => new Promise((resolve) => {
1220
- if (chunkQueue.length > 0) {
1221
- resolve(chunkQueue.shift());
1222
- }
1223
- else {
1224
- pendingResolve = resolve;
1225
- }
1226
- });
1227
- return { pushChunk, nextChunk };
1228
- };
1229
- const mergeUsage = (a, b) => {
1230
- if (!a) {
1231
- return b;
1232
- }
1233
- if (!b) {
1234
- return a;
1235
- }
1236
- return {
1237
- prompt_tokens: (a.prompt_tokens ?? 0) + (b.prompt_tokens ?? 0),
1238
- completion_tokens: (a.completion_tokens ?? 0) + (b.completion_tokens ?? 0),
1239
- total_tokens: (a.total_tokens ?? 0) + (b.total_tokens ?? 0),
1240
- };
1241
- };
1242
732
  //# sourceMappingURL=openaiCompatible.js.map
@@ -903,6 +903,8 @@ export type AnthropicVertexSettings = {
903
903
  projectId: string;
904
904
  /** Google Cloud region for Anthropic models (e.g., 'us-east5') */
905
905
  region: string;
906
+ /** SDK request timeout in milliseconds */
907
+ timeout?: number;
906
908
  };
907
909
  /**
908
910
  * OpenAI-compatible models endpoint response structure
@@ -366,12 +366,13 @@ const createVertexSettings = async (region) => {
366
366
  return baseSettings;
367
367
  };
368
368
  // Create Anthropic-specific Vertex settings for native @anthropic-ai/vertex-sdk
369
- const createVertexAnthropicSettings = async (region) => {
369
+ const createVertexAnthropicSettings = async (region, timeoutMs) => {
370
370
  const location = region || getVertexLocation();
371
371
  const project = getVertexProjectId();
372
372
  return {
373
373
  projectId: project,
374
374
  region: location,
375
+ ...(timeoutMs !== undefined && { timeout: timeoutMs }),
375
376
  };
376
377
  };
377
378
  // Helper function to determine if a model is an Anthropic model
@@ -2032,9 +2033,9 @@ export class GoogleVertexProvider extends BaseProvider {
2032
2033
  /**
2033
2034
  * Create native AnthropicVertex client for Claude models
2034
2035
  */
2035
- async createAnthropicVertexClient() {
2036
+ async createAnthropicVertexClient(timeoutMs) {
2036
2037
  const mod = await getAnthropicVertexModule();
2037
- const settings = await createVertexAnthropicSettings(this.location);
2038
+ const settings = await createVertexAnthropicSettings(this.location, timeoutMs);
2038
2039
  return new mod.AnthropicVertex(settings);
2039
2040
  }
2040
2041
  /**
@@ -2042,9 +2043,10 @@ export class GoogleVertexProvider extends BaseProvider {
2042
2043
  * This bypasses @ai-sdk/google-vertex completely and uses Anthropic's native SDK
2043
2044
  */
2044
2045
  async executeNativeAnthropicStream(options) {
2045
- const client = await this.createAnthropicVertexClient();
2046
2046
  const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
2047
2047
  const startTime = Date.now();
2048
+ const streamTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
2049
+ const client = await this.createAnthropicVertexClient(streamTimeoutMs);
2048
2050
  logger.debug("[GoogleVertex] Using native @anthropic-ai/vertex-sdk for Claude stream", {
2049
2051
  model: modelName,
2050
2052
  project: this.projectId,
@@ -2336,7 +2338,6 @@ export class GoogleVertexProvider extends BaseProvider {
2336
2338
  // abort the stream after the configured timeout so a stalled
2337
2339
  // Vertex/Anthropic endpoint can't hang forever. options.timeout wins
2338
2340
  // if set; otherwise 5 min — generous for tool-heavy turns.
2339
- const streamTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
2340
2341
  const streamTimeoutHandle = setTimeout(() => {
2341
2342
  logger.warn(`[GoogleVertex] Anthropic stream exceeded ${streamTimeoutMs}ms — aborting`);
2342
2343
  abortHandler();
@@ -2560,9 +2561,10 @@ export class GoogleVertexProvider extends BaseProvider {
2560
2561
  * Execute generate using native @anthropic-ai/vertex-sdk for Claude models on Vertex AI
2561
2562
  */
2562
2563
  async executeNativeAnthropicGenerate(options) {
2563
- const client = await this.createAnthropicVertexClient();
2564
2564
  const modelName = options.model || this.modelName || "claude-sonnet-4-5@20250929";
2565
2565
  const startTime = Date.now();
2566
+ const generateTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
2567
+ const client = await this.createAnthropicVertexClient(generateTimeoutMs);
2566
2568
  logger.debug("[GoogleVertex] Using native @anthropic-ai/vertex-sdk for Claude generate", {
2567
2569
  model: modelName,
2568
2570
  project: this.projectId,
@@ -2826,7 +2828,6 @@ export class GoogleVertexProvider extends BaseProvider {
2826
2828
  // Bound the SDK wait so a stalled Vertex/Anthropic call can't hang
2827
2829
  // generate forever. options.timeout wins if set, otherwise default
2828
2830
  // to 5 min — generous for tool-heavy turns.
2829
- const generateTimeoutMs = parseTimeout(options.timeout) ?? 300_000;
2830
2831
  const response = await withTimeout(client.messages.create({
2831
2832
  ...requestParams,
2832
2833
  messages: currentMessages,