@juspay/neurolink 9.67.3 → 9.68.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.
@@ -28,7 +28,7 @@ import { composeAbortSignals, createTimeoutController, mergeAbortSignals, } from
28
28
  import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
29
29
  import { resolveToolChoice } from "../utils/toolChoice.js";
30
30
  import { transformToolExecutions } from "../utils/transformationUtils.js";
31
- import { buildAPIError, buildBody, buildToolsForOpenAI, createChunkQueue, createDeferredAnalytics, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
31
+ import { buildAPIError, buildBody, buildToolsForOpenAI, createChunkQueue, createDeferredAnalytics, ensureJsonWordInBody, mapNeuroLinkToolChoice, mergeUsage, messageBuilderToOpenAI, parseSSEStream, stringifyToolOutput, stripTrailingSlash, v3ResponseFormatToOpenAI, v3ToolChoiceToOpenAI, v3ToolsToOpenAI, } from "./openaiChatCompletionsClient.js";
32
32
  /**
33
33
  * Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
34
34
  */
@@ -64,6 +64,28 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
64
64
  adjustBuildBodyOptions(_modelId, opts) {
65
65
  return opts;
66
66
  }
67
+ /**
68
+ * Hook to adjust the OpenAI `response_format` after it's converted from the
69
+ * V3 responseFormat (non-streaming `doGenerate` path). Default identity.
70
+ * Override for providers that don't support a given format type — e.g.
71
+ * DeepSeek rejects `response_format: { type: "json_schema" }` ("This
72
+ * response_format type is unavailable now"); the `@ai-sdk/openai-compatible`
73
+ * path this replaced declared `supportsStructuredOutputs: false`, which
74
+ * downgraded `json_schema` to `json_object`. Subclasses replicate that here.
75
+ */
76
+ adjustResponseFormat(rf, _modelId) {
77
+ return rf;
78
+ }
79
+ /**
80
+ * Hook to adjust the fully-built wire request body before it is sent, on
81
+ * both the streaming and non-streaming paths. Default identity. Override for
82
+ * provider/model quirks that can't be expressed through buildBody options —
83
+ * e.g. Azure's newer reasoning deployments (o-series, gpt-5+) reject
84
+ * `max_tokens` and require `max_completion_tokens`.
85
+ */
86
+ adjustRequestBody(body, _modelId) {
87
+ return body;
88
+ }
67
89
  /**
68
90
  * Hook called once at the start of every `executeStream` invocation.
69
91
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred
@@ -82,6 +104,22 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
82
104
  shouldAutoDiscoverModel() {
83
105
  return true;
84
106
  }
107
+ /**
108
+ * Builds the chat-completions request URL for a model. Default is
109
+ * `${baseURL}/chat/completions`. Override for providers with a different
110
+ * routing scheme (e.g. Azure's deployment-based path + api-version query).
111
+ */
112
+ getChatCompletionsURL(_modelId) {
113
+ return `${stripTrailingSlash(this.config.baseURL)}/chat/completions`;
114
+ }
115
+ /**
116
+ * Auth headers merged into every request. Default is a Bearer token.
117
+ * Override for providers that authenticate differently (e.g. Azure, which
118
+ * uses an `api-key` header instead of `Authorization: Bearer`).
119
+ */
120
+ getAuthHeaders() {
121
+ return { Authorization: `Bearer ${this.config.apiKey}` };
122
+ }
85
123
  // ===========================================================================
86
124
  // Public/protected concrete methods (shared by all subclasses)
87
125
  // ===========================================================================
@@ -129,11 +167,13 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
129
167
  return fallback;
130
168
  }
131
169
  buildDelegatingModel(modelId) {
132
- const url = `${stripTrailingSlash(this.config.baseURL)}/chat/completions`;
170
+ const url = this.getChatCompletionsURL(modelId);
133
171
  const fetchImpl = createProxyFetch();
134
- const apiKey = this.config.apiKey;
172
+ const getAuthHeaders = this.getAuthHeaders.bind(this);
135
173
  const providerName = this.providerName;
136
174
  const adjustBuildBodyOptions = this.adjustBuildBodyOptions.bind(this);
175
+ const adjustResponseFormat = this.adjustResponseFormat.bind(this);
176
+ const adjustRequestBody = this.adjustRequestBody.bind(this);
137
177
  const getTimeoutForOptions = (opts) => this.getTimeout((opts ?? {}));
138
178
  return {
139
179
  specificationVersion: "v3",
@@ -141,10 +181,17 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
141
181
  modelId,
142
182
  supportedUrls: {},
143
183
  doGenerate: async (options) => {
144
- const messages = messageBuilderToOpenAI(options.prompt);
145
- const body = buildBody({
184
+ const baseMessages = messageBuilderToOpenAI(options.prompt);
185
+ const responseFormat = options.responseFormat
186
+ ? adjustResponseFormat(v3ResponseFormatToOpenAI(options.responseFormat), modelId)
187
+ : undefined;
188
+ // ensureJsonWordInBody runs LAST — on the body after adjustRequestBody —
189
+ // so the json_object word guard reflects whatever a subclass left on
190
+ // the wire (it may rewrite response_format/messages), not an
191
+ // intermediate state.
192
+ const body = ensureJsonWordInBody(adjustRequestBody(buildBody({
146
193
  modelId,
147
- messages,
194
+ messages: baseMessages,
148
195
  options: adjustBuildBodyOptions(modelId, {
149
196
  maxTokens: options.maxOutputTokens,
150
197
  temperature: options.temperature,
@@ -159,12 +206,8 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
159
206
  ? { toolChoice: v3ToolChoiceToOpenAI(options.toolChoice) }
160
207
  : {}),
161
208
  streaming: false,
162
- ...(options.responseFormat
163
- ? {
164
- responseFormat: v3ResponseFormatToOpenAI(options.responseFormat),
165
- }
166
- : {}),
167
- });
209
+ ...(responseFormat ? { responseFormat } : {}),
210
+ }), modelId));
168
211
  const timeoutController = createTimeoutController(getTimeoutForOptions(options), providerName, "generate");
169
212
  const composedSignal = composeAbortSignals(options.abortSignal, timeoutController?.controller.signal);
170
213
  let res;
@@ -173,7 +216,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
173
216
  method: "POST",
174
217
  headers: {
175
218
  "Content-Type": "application/json",
176
- Authorization: `Bearer ${apiKey}`,
219
+ ...getAuthHeaders(),
177
220
  },
178
221
  body: JSON.stringify(body),
179
222
  ...(composedSignal ? { signal: composedSignal } : {}),
@@ -282,7 +325,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
282
325
  timeoutController?.cleanup();
283
326
  throw setupErr;
284
327
  }
285
- const url = `${stripTrailingSlash(this.config.baseURL)}/chat/completions`;
328
+ const url = this.getChatCompletionsURL(modelId);
286
329
  const fetchImpl = createProxyFetch();
287
330
  const maxSteps = options.maxSteps || DEFAULT_MAX_STEPS;
288
331
  const emitter = this.neurolink?.getEventEmitter();
@@ -296,7 +339,6 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
296
339
  maxSteps,
297
340
  modelId,
298
341
  url,
299
- apiKey: this.config.apiKey,
300
342
  fetchImpl,
301
343
  abortSignal,
302
344
  options,
@@ -424,7 +466,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
424
466
  return result;
425
467
  }
426
468
  async runStreamLoop(args) {
427
- const { maxSteps, modelId, url, apiKey, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, emitter, toolsUsed, toolExecutionSummaries, pushChunk, resolveUsage, resolveFinish, } = args;
469
+ const { maxSteps, modelId, url, fetchImpl, abortSignal, options, conversation, openAITools, openAIToolChoice, toolsRecord, emitter, toolsUsed, toolExecutionSummaries, pushChunk, resolveUsage, resolveFinish, } = args;
428
470
  try {
429
471
  let stepFinish = null;
430
472
  let stepUsage;
@@ -432,7 +474,6 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
432
474
  const stepResult = await this.streamOneStep({
433
475
  modelId,
434
476
  url,
435
- apiKey,
436
477
  fetchImpl,
437
478
  abortSignal,
438
479
  options,
@@ -481,7 +522,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
481
522
  }
482
523
  }
483
524
  async streamOneStep(args) {
484
- const body = buildBody({
525
+ const body = ensureJsonWordInBody(this.adjustRequestBody(buildBody({
485
526
  modelId: args.modelId,
486
527
  messages: args.conversation,
487
528
  options: this.adjustBuildBodyOptions(args.modelId, args.options),
@@ -490,12 +531,12 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
490
531
  ? { toolChoice: args.openAIToolChoice }
491
532
  : {}),
492
533
  streaming: true,
493
- });
534
+ }), args.modelId));
494
535
  const res = await args.fetchImpl(args.url, {
495
536
  method: "POST",
496
537
  headers: {
497
538
  "Content-Type": "application/json",
498
- Authorization: `Bearer ${args.apiKey}`,
539
+ ...this.getAuthHeaders(),
499
540
  },
500
541
  body: JSON.stringify(body),
501
542
  ...(args.abortSignal ? { signal: args.abortSignal } : {}),
@@ -610,7 +651,7 @@ export class OpenAIChatCompletionsProvider extends BaseProvider {
610
651
  const t = setTimeout(() => controller.abort(), 5000);
611
652
  const response = await proxyFetch(modelsUrl, {
612
653
  headers: {
613
- Authorization: `Bearer ${this.config.apiKey}`,
654
+ ...this.getAuthHeaders(),
614
655
  "Content-Type": "application/json",
615
656
  },
616
657
  signal: controller.signal,
@@ -31,6 +31,9 @@ export declare const v3ResponseFormatToOpenAI: (rf: {
31
31
  description?: string;
32
32
  }) => OpenAICompatResponseFormat | undefined;
33
33
  export declare const mapNeuroLinkToolChoice: (choice: unknown) => OpenAICompatToolChoiceWire | undefined;
34
+ export declare const messagesContainJsonWord: (messages: ReadonlyArray<OpenAICompatChatMessage>) => boolean;
35
+ export declare const ensureJsonWordInBody: (body: OpenAICompatChatRequest) => OpenAICompatChatRequest;
36
+ export declare const requiresMaxCompletionTokens: (modelId: string) => boolean;
34
37
  export declare const buildBody: (args: OpenAICompatBuildBodyArgs) => OpenAICompatChatRequest;
35
38
  export declare const parseSSEStream: (body: ReadableStream<Uint8Array>, onTextDelta: (delta: string) => void) => Promise<OpenAICompatSSEResult>;
36
39
  export declare const buildAPIError: (url: string, body: OpenAICompatChatRequest, res: Response) => Promise<Error>;
@@ -318,6 +318,45 @@ export const mapNeuroLinkToolChoice = (choice) => {
318
318
  }
319
319
  return undefined;
320
320
  };
321
+ // OpenAI-compatible endpoints (OpenAI, DeepSeek, …) reject
322
+ // `response_format: { type: "json_object" }` unless the literal word "json"
323
+ // appears somewhere in the messages. The `@ai-sdk/openai-compatible` wrapper
324
+ // this client replaced injected that instruction for us; the native client
325
+ // must do the same or json_object requests 400.
326
+ export const messagesContainJsonWord = (messages) => messages.some((m) => {
327
+ const c = m.content;
328
+ if (typeof c === "string") {
329
+ return /\bjson\b/i.test(c);
330
+ }
331
+ if (Array.isArray(c)) {
332
+ return c.some((part) => typeof part?.text === "string" &&
333
+ /\bjson\b/i.test(part.text));
334
+ }
335
+ return false;
336
+ });
337
+ // Prepends a minimal JSON-instruction system message to the FINAL wire body
338
+ // when json_object mode is requested and its messages don't already mention
339
+ // "json". Operates on the post-`adjustRequestBody` body so the guard reflects
340
+ // whatever a subclass left on the wire (response_format/messages it may have
341
+ // rewritten), not an intermediate state. No-op otherwise.
342
+ export const ensureJsonWordInBody = (body) => body.response_format?.type === "json_object" &&
343
+ !messagesContainJsonWord(body.messages)
344
+ ? {
345
+ ...body,
346
+ messages: [
347
+ {
348
+ role: "system",
349
+ content: "Respond with valid JSON only — no prose, no markdown fencing.",
350
+ },
351
+ ...body.messages,
352
+ ],
353
+ }
354
+ : body;
355
+ // Reasoning-class OpenAI models (o-series, gpt-5+) reject `max_tokens` and
356
+ // require `max_completion_tokens`. The OpenAI + Azure providers use this to
357
+ // rename the field on the wire body; third-party OpenAI-compatible endpoints
358
+ // keep `max_tokens`, so it is opt-in per provider, never applied by default.
359
+ export const requiresMaxCompletionTokens = (modelId) => /^(o\d|gpt-5)/i.test(modelId.replace(/^.*\//, ""));
321
360
  export const buildBody = (args) => {
322
361
  const { modelId, messages, options, tools, toolChoice, streaming, responseFormat, } = args;
323
362
  const body = {
@@ -212,7 +212,6 @@ export type StreamLoopArgs = {
212
212
  maxSteps: number;
213
213
  modelId: string;
214
214
  url: string;
215
- apiKey: string;
216
215
  fetchImpl: typeof fetch;
217
216
  abortSignal: AbortSignal | undefined;
218
217
  options: StreamOptions;
@@ -125,6 +125,7 @@ export type NeurolinkCredentials = {
125
125
  resourceName?: string;
126
126
  deploymentName?: string;
127
127
  apiVersion?: string;
128
+ useMaxCompletionTokens?: boolean;
128
129
  };
129
130
  mistral?: {
130
131
  apiKey?: string;
@@ -1,24 +1,61 @@
1
1
  import { type AIProviderName } from "../constants/enums.js";
2
- import { BaseProvider } from "../core/baseProvider.js";
3
- import type { StreamOptions, StreamResult, NeurolinkCredentials } from "../types/index.js";
4
- import type { LanguageModel } from "../types/index.js";
5
- export declare class AzureOpenAIProvider extends BaseProvider {
6
- private apiKey;
7
- private resourceName;
8
- private deployment;
9
- private apiVersion;
10
- private azureProvider;
2
+ import type { NeurolinkCredentials, OpenAICompatChatRequest } from "../types/index.js";
3
+ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
4
+ /**
5
+ * Azure OpenAI Provider direct HTTP, no AI SDK.
6
+ *
7
+ * Supports both classic Azure OpenAI Service endpoints
8
+ * ("*.openai.azure.com", "*.cognitiveservices.azure.com") and the newer
9
+ * Azure AI Foundry endpoints ("*.services.ai.azure.com").
10
+ *
11
+ * All request/stream/tool-loop orchestration lives in
12
+ * `OpenAIChatCompletionsProvider`; this class overrides the URL builder and
13
+ * auth headers to accommodate Azure's deployment-based routing and
14
+ * `api-key` header (rather than Bearer tokens).
15
+ *
16
+ * @see https://learn.microsoft.com/azure/cognitive-services/openai/
17
+ */
18
+ export declare class AzureOpenAIProvider extends OpenAIChatCompletionsProvider {
19
+ protected readonly azureDeployment: string;
20
+ protected readonly azureApiVersion: string;
21
+ protected readonly azureResourceOrigin: string;
22
+ protected readonly azureDeploymentPathPrefix: string;
23
+ protected readonly useMaxCompletionTokensOverride?: boolean;
11
24
  constructor(modelName?: string, sdk?: unknown, _region?: string, credentials?: NeurolinkCredentials["azure"]);
12
- getProviderName(): AIProviderName;
13
- getDefaultModel(): string;
25
+ protected getProviderName(): AIProviderName;
14
26
  /**
15
- * Returns the Vercel AI SDK model instance for Azure OpenAI.
16
- * Uses .chat() explicitly because @ai-sdk/azure v3+ defaults the bare
17
- * provider() call to the Responses API, which many Azure deployments
18
- * do not support yet.
27
+ * The "default model" for Azure is the deployment name — it's the
28
+ * identifier callers pass to select a deployment.
19
29
  */
20
- getAISDKModel(): LanguageModel;
30
+ protected getDefaultModel(): string;
21
31
  protected formatProviderError(error: unknown): Error;
22
- protected executeStream(options: StreamOptions, _analysisSchema?: unknown): Promise<StreamResult>;
32
+ /**
33
+ * Builds the full Azure deployment chat completions URL.
34
+ *
35
+ * Pattern:
36
+ * {resourceOrigin}{deploymentPathPrefix}/deployments/{deployment}/chat/completions?api-version={apiVersion}
37
+ *
38
+ * Examples:
39
+ * Classic: https://myresource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview
40
+ * Foundry: https://myhost.services.ai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview
41
+ */
42
+ protected getChatCompletionsURL(modelId: string): string;
43
+ /**
44
+ * Azure uses `api-key` rather than the standard `Authorization: Bearer`
45
+ * header expected by OpenAI-compatible endpoints.
46
+ */
47
+ protected getAuthHeaders(): Record<string, string>;
48
+ /**
49
+ * Newer Azure deployments (o-series, gpt-5+) reject `max_tokens` and require
50
+ * `max_completion_tokens`. The `@ai-sdk/openai` path this migration replaced
51
+ * renamed the field automatically; replicate that here.
52
+ *
53
+ * Capability is taken from the explicit `useMaxCompletionTokens` override
54
+ * (credentials / `AZURE_OPENAI_USE_MAX_COMPLETION_TOKENS`) when set — Azure
55
+ * deployment names are user-defined, so a `chat-prod` gpt-5 deployment can't
56
+ * be detected from the name. When unset, fall back to a best-effort
57
+ * model-name heuristic for the common case where the deployment echoes the
58
+ * model (e.g. `gpt-5.4`).
59
+ */
60
+ protected adjustRequestBody(body: OpenAICompatChatRequest, modelId: string): OpenAICompatChatRequest;
23
61
  }
24
- export default AzureOpenAIProvider;