@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.
@@ -1,30 +1,64 @@
1
- import { createAzure } from "@ai-sdk/azure";
2
1
  import { APIVersions } from "../constants/enums.js";
3
- import { BaseProvider } from "../core/baseProvider.js";
4
- import { DEFAULT_MAX_STEPS } from "../core/constants.js";
5
- import { createProxyFetch } from "../proxy/proxyFetch.js";
6
- import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
7
2
  import { AuthenticationError, NetworkError, ProviderError, } from "../types/index.js";
8
3
  import { logger } from "../utils/logger.js";
9
4
  import { createAzureAPIKeyConfig, createAzureEndpointConfig, validateApiKey, } from "../utils/providerConfig.js";
10
- import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
11
- import { resolveToolChoice } from "../utils/toolChoice.js";
12
- import { stepCountIs } from "../utils/tool.js";
13
- import { streamText } from "../utils/generation.js";
14
- export class AzureOpenAIProvider extends BaseProvider {
15
- apiKey;
16
- resourceName;
17
- deployment;
18
- apiVersion;
19
- azureProvider;
5
+ import { TimeoutError } from "../utils/timeout.js";
6
+ import { transformParamsForLogging } from "../utils/transformationUtils.js";
7
+ import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js";
8
+ import { requiresMaxCompletionTokens } from "./openaiChatCompletionsClient.js";
9
+ /**
10
+ * Azure OpenAI Provider — direct HTTP, no AI SDK.
11
+ *
12
+ * Supports both classic Azure OpenAI Service endpoints
13
+ * ("*.openai.azure.com", "*.cognitiveservices.azure.com") and the newer
14
+ * Azure AI Foundry endpoints ("*.services.ai.azure.com").
15
+ *
16
+ * All request/stream/tool-loop orchestration lives in
17
+ * `OpenAIChatCompletionsProvider`; this class overrides the URL builder and
18
+ * auth headers to accommodate Azure's deployment-based routing and
19
+ * `api-key` header (rather than Bearer tokens).
20
+ *
21
+ * @see https://learn.microsoft.com/azure/cognitive-services/openai/
22
+ */
23
+ export class AzureOpenAIProvider extends OpenAIChatCompletionsProvider {
24
+ // Azure-specific routing state resolved once in the constructor.
25
+ azureDeployment;
26
+ azureApiVersion;
27
+ // Parsed from the endpoint — mutually exclusive: either resourceName (for
28
+ // classic hosts) or foundryBaseURL (for AI Foundry) is non-empty.
29
+ azureResourceOrigin;
30
+ azureDeploymentPathPrefix;
31
+ // Explicit `max_completion_tokens` capability. `undefined` ⇒ fall back to the
32
+ // deployment-name heuristic in adjustRequestBody().
33
+ useMaxCompletionTokensOverride;
20
34
  constructor(modelName, sdk, _region, credentials) {
21
- super(modelName, "azure", sdk);
22
- this.apiKey = credentials?.apiKey || process.env.AZURE_OPENAI_API_KEY || "";
35
+ const apiKey = credentials?.apiKey || process.env.AZURE_OPENAI_API_KEY || "";
36
+ // -----------------------------------------------------------------------
37
+ // Parse the AZURE_OPENAI_ENDPOINT environment variable (or credentials)
38
+ // into the pieces needed to build deployment-based chat completions URLs.
39
+ //
40
+ // Two supported endpoint formats:
41
+ //
42
+ // 1. Classic Azure OpenAI / Cognitive Services:
43
+ // https://<resource>.openai.azure.com
44
+ // https://<resource>.cognitiveservices.azure.com
45
+ // The @ai-sdk/azure tradition was to pass the bare resource subdomain
46
+ // and let the SDK reconstruct the full URL. We instead keep the full
47
+ // origin and emit the standard deployment path from it:
48
+ // {origin}/openai/deployments/{deployment}/chat/completions
49
+ //
50
+ // 2. Azure AI Foundry:
51
+ // https://<host>.services.ai.azure.com[/openai]
52
+ // The host has no resource-name subdomain convention. The operator
53
+ // may or may not include the "/openai" path prefix; we normalise that.
54
+ // Final URL pattern:
55
+ // {origin}{normalisedPath}/deployments/{deployment}/chat/completions
56
+ //
57
+ // In both cases we pass the "resource origin" (scheme+host) as `baseURL`
58
+ // to super so that `getAvailableModels()` can still build a models URL
59
+ // from it if needed; `getChatCompletionsURL()` builds the real path.
60
+ // -----------------------------------------------------------------------
23
61
  const endpoint = process.env.AZURE_OPENAI_ENDPOINT || "";
24
- // Use URL parsing instead of string-replace so endpoints that already
25
- // carry a path segment (e.g. "https://<host>/openai" — a valid Azure AI
26
- // Foundry shape) don't end up duplicating it as "<host>/openai/openai".
27
- // Tolerate missing scheme by prefixing https:// before parsing.
28
62
  let endpointUrl;
29
63
  if (endpoint) {
30
64
  try {
@@ -35,95 +69,85 @@ export class AzureOpenAIProvider extends BaseProvider {
35
69
  }
36
70
  }
37
71
  const endpointHost = endpointUrl?.hostname ?? "";
72
+ // Strip trailing slashes from the pathname; treat "/" as empty.
38
73
  const endpointPath = endpointUrl?.pathname && endpointUrl.pathname !== "/"
39
74
  ? endpointUrl.pathname.replace(/\/+$/, "")
40
75
  : "";
41
- // Classic Azure OpenAI ("*.openai.azure.com") and Cognitive Services
42
- // ("*.cognitiveservices.azure.com") endpoints encode the resource name as
43
- // a subdomain that @ai-sdk/azure expects to receive verbatim. The newer
44
- // Azure AI Foundry endpoint format ("*.services.ai.azure.com") does not
45
- // round-trip through that subdomain rewrite, so passing the resource name
46
- // would yield e.g. "<host>.services.ai.azure.com.openai.azure.com". For
47
- // those hosts we hand the full URL back via baseURL instead.
76
+ // Classic hosts encode the resource name as a subdomain.
48
77
  const isClassicAzureHost = /\.(openai|cognitiveservices)\.azure\.com$/.test(endpointHost);
49
- const envResourceName = isClassicAzureHost
50
- ? endpointHost
51
- .replace(".openai.azure.com", "")
52
- .replace(".cognitiveservices.azure.com", "")
53
- : "";
54
- this.resourceName = credentials?.resourceName || envResourceName;
55
- // For Azure AI Foundry the SDK still routes to the OpenAI-compatible API
56
- // (deployments/{deployment}/chat/completions); the `/openai` path suffix
57
- // mirrors what the SDK derives in classic mode
58
- // (`https://${resource}.openai.azure.com/openai`). Reuse the path the
59
- // operator already supplied if it already terminates in `/openai` *or*
60
- // a versioned form like `/openai/v1`; otherwise append `/openai`. Never
61
- // duplicate.
62
- const hasOpenAIPathSuffix = /\/openai(?:\/v\d+)?$/.test(endpointPath);
63
- const baseURLForFoundry = !this.resourceName && endpointUrl
64
- ? `${endpointUrl.origin}${hasOpenAIPathSuffix ? endpointPath : `${endpointPath}/openai`}`
65
- : undefined;
66
- this.deployment =
67
- credentials?.deploymentName ||
68
- modelName ||
69
- process.env.AZURE_OPENAI_MODEL ||
70
- process.env.AZURE_OPENAI_DEPLOYMENT ||
71
- process.env.AZURE_OPENAI_DEPLOYMENT_ID ||
72
- "gpt-4o";
73
- this.apiVersion =
74
- credentials?.apiVersion ||
75
- process.env.AZURE_API_VERSION ||
76
- APIVersions.AZURE_LATEST;
77
- // Configuration validation - now using consolidated utility
78
- if (!this.apiKey) {
78
+ // For classic hosts the deployment URL path always starts with "/openai".
79
+ // For Foundry hosts we reuse whatever the operator supplied, appending
80
+ // "/openai" only when the path doesn't already end with it (or a
81
+ // versioned variant like "/openai/v1").
82
+ let deploymentPathPrefix;
83
+ if (isClassicAzureHost) {
84
+ deploymentPathPrefix = "/openai";
85
+ }
86
+ else {
87
+ const hasOpenAIPathSuffix = /\/openai(?:\/v\d+)?$/.test(endpointPath);
88
+ deploymentPathPrefix = hasOpenAIPathSuffix
89
+ ? endpointPath
90
+ : `${endpointPath}/openai`;
91
+ }
92
+ const resourceOrigin = endpointUrl?.origin ?? "";
93
+ const deployment = credentials?.deploymentName ||
94
+ modelName ||
95
+ process.env.AZURE_OPENAI_MODEL ||
96
+ process.env.AZURE_OPENAI_DEPLOYMENT ||
97
+ process.env.AZURE_OPENAI_DEPLOYMENT_ID ||
98
+ "gpt-4o";
99
+ const apiVersion = credentials?.apiVersion ||
100
+ process.env.AZURE_API_VERSION ||
101
+ APIVersions.AZURE_LATEST;
102
+ // Deployment names are user-defined, so a model-name heuristic can't
103
+ // reliably tell whether the backing model needs max_completion_tokens.
104
+ // Prefer an explicit signal (credentials or env); fall back to the
105
+ // heuristic only when unset.
106
+ const envMaxCompletion = process.env.AZURE_OPENAI_USE_MAX_COMPLETION_TOKENS;
107
+ const maxCompletionOverride = credentials?.useMaxCompletionTokens ??
108
+ (envMaxCompletion === undefined
109
+ ? undefined
110
+ : /^(1|true|yes)$/i.test(envMaxCompletion));
111
+ // Validate required credentials before committing.
112
+ if (!apiKey) {
79
113
  validateApiKey(createAzureAPIKeyConfig());
80
114
  }
81
- if (!this.resourceName && !baseURLForFoundry) {
115
+ if (!resourceOrigin) {
82
116
  validateApiKey(createAzureEndpointConfig());
83
117
  }
84
- // Create the Azure provider instance with proxy support.
85
- // For classic *.openai.azure.com / *.cognitiveservices.azure.com hosts we
86
- // pass `resourceName`, which @ai-sdk/azure rewrites into the canonical
87
- // subdomain. For Azure AI Foundry hosts ("*.services.ai.azure.com") we
88
- // pass the full URL via `baseURL` so no rewrite happens.
89
- // useDeploymentBasedUrls is required because @ai-sdk/azure v3+ defaults to
90
- // the /v1/ URL format, but most Azure deployments still require the legacy
91
- // /deployments/{deployment}/ URL pattern.
92
- this.azureProvider = baseURLForFoundry
93
- ? createAzure({
94
- baseURL: baseURLForFoundry,
95
- apiKey: this.apiKey,
96
- apiVersion: this.apiVersion,
97
- useDeploymentBasedUrls: true,
98
- fetch: createProxyFetch(),
99
- })
100
- : createAzure({
101
- resourceName: this.resourceName,
102
- apiKey: this.apiKey,
103
- apiVersion: this.apiVersion,
104
- useDeploymentBasedUrls: true,
105
- fetch: createProxyFetch(),
106
- });
107
- logger.debug("Azure Vercel Provider initialized", {
108
- deployment: this.deployment,
109
- resourceName: this.resourceName,
110
- provider: "azure-vercel",
118
+ // Pass the resource origin as baseURL so the base class can construct
119
+ // auxiliary URLs (e.g. /models) from it when needed.
120
+ super("azure", modelName, sdk, {
121
+ baseURL: resourceOrigin,
122
+ apiKey,
111
123
  });
124
+ this.azureDeployment = deployment;
125
+ this.azureApiVersion = apiVersion;
126
+ this.azureResourceOrigin = resourceOrigin;
127
+ this.azureDeploymentPathPrefix = deploymentPathPrefix;
128
+ this.useMaxCompletionTokensOverride = maxCompletionOverride;
129
+ if (logger.shouldLog("debug")) {
130
+ logger.debug("Azure OpenAI Provider initialized", transformParamsForLogging({
131
+ deployment: this.azureDeployment,
132
+ resourceOrigin: this.azureResourceOrigin,
133
+ deploymentPathPrefix: this.azureDeploymentPathPrefix,
134
+ apiVersion: this.azureApiVersion,
135
+ provider: "azure",
136
+ }));
137
+ }
112
138
  }
139
+ // ===========================================================================
140
+ // Abstract-hook implementations
141
+ // ===========================================================================
113
142
  getProviderName() {
114
143
  return "azure";
115
144
  }
116
- getDefaultModel() {
117
- return this.deployment;
118
- }
119
145
  /**
120
- * Returns the Vercel AI SDK model instance for Azure OpenAI.
121
- * Uses .chat() explicitly because @ai-sdk/azure v3+ defaults the bare
122
- * provider() call to the Responses API, which many Azure deployments
123
- * do not support yet.
146
+ * The "default model" for Azure is the deployment name — it's the
147
+ * identifier callers pass to select a deployment.
124
148
  */
125
- getAISDKModel() {
126
- return this.azureProvider.chat(this.deployment);
149
+ getDefaultModel() {
150
+ return this.azureDeployment;
127
151
  }
128
152
  formatProviderError(error) {
129
153
  if (error instanceof TimeoutError) {
@@ -140,80 +164,56 @@ export class AzureOpenAIProvider extends BaseProvider {
140
164
  : "Unknown error";
141
165
  return new ProviderError(`Azure OpenAI error: ${message}`, "azure");
142
166
  }
143
- // executeGenerate removed - BaseProvider handles all generation with tools
144
- async executeStream(options, _analysisSchema) {
145
- const timeout = this.getTimeout(options);
146
- const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
147
- try {
148
- // Get tools - options.tools is pre-merged by BaseProvider.stream()
149
- const shouldUseTools = !options.disableTools && this.supportsTools();
150
- const tools = shouldUseTools
151
- ? options.tools || (await this.getAllTools())
152
- : {};
153
- logger.debug("Azure Stream - Tool Loading Debug", {
154
- shouldUseTools,
155
- toolCount: Object.keys(tools).length,
156
- toolNames: Object.keys(tools).slice(0, 10),
157
- disableTools: options.disableTools,
158
- supportsTools: this.supportsTools(),
159
- });
160
- // Build message array from options with multimodal support
161
- // Using protected helper from BaseProvider to eliminate code duplication
162
- const messages = await this.buildMessagesForStream(options);
163
- const model = await this.getAISDKModelWithMiddleware(options);
164
- // Reviewer follow-up: capture upstream provider errors via onError
165
- // so the post-stream NoOutput sentinel carries the real cause.
166
- let capturedProviderError;
167
- const stream = await streamText({
168
- model,
169
- messages: messages,
170
- ...(options.maxTokens !== null && options.maxTokens !== undefined
171
- ? { maxOutputTokens: options.maxTokens }
172
- : {}),
173
- ...(options.temperature !== null && options.temperature !== undefined
174
- ? { temperature: options.temperature }
175
- : {}),
176
- tools,
177
- toolChoice: resolveToolChoice(options, tools, shouldUseTools),
178
- stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
179
- abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
180
- experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
181
- experimental_repairToolCall: this.getToolCallRepairFn(options),
182
- onError: (event) => {
183
- capturedProviderError = event.error;
184
- logger.error("AzureOpenAI: Stream error", {
185
- error: event.error instanceof Error
186
- ? event.error.message
187
- : String(event.error),
188
- });
189
- },
190
- onStepFinish: (event) => {
191
- emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), event.toolResults);
192
- this.handleToolExecutionStorage([...event.toolCalls], [...event.toolResults], options, new Date()).catch((error) => {
193
- logger.warn("[AzureOpenaiProvider] Failed to store tool executions", {
194
- provider: this.providerName,
195
- error: error instanceof Error ? error.message : String(error),
196
- });
197
- });
198
- },
199
- });
200
- timeoutController?.cleanup();
201
- // Transform string stream to content object stream using BaseProvider method
202
- const transformedStream = this.createTextStream(stream, () => capturedProviderError);
167
+ // ===========================================================================
168
+ // New overridable hooks (provided by the base-enhancement branch)
169
+ // ===========================================================================
170
+ /**
171
+ * Builds the full Azure deployment chat completions URL.
172
+ *
173
+ * Pattern:
174
+ * {resourceOrigin}{deploymentPathPrefix}/deployments/{deployment}/chat/completions?api-version={apiVersion}
175
+ *
176
+ * Examples:
177
+ * Classic: https://myresource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview
178
+ * Foundry: https://myhost.services.ai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview
179
+ */
180
+ getChatCompletionsURL(modelId) {
181
+ // modelId is the deployment name when it has been resolved; fall back to
182
+ // the stored deployment when the base passes a generic placeholder.
183
+ const deployment = modelId || this.azureDeployment;
184
+ const prefix = this.azureDeploymentPathPrefix.replace(/\/+$/, "");
185
+ return (`${this.azureResourceOrigin}${prefix}/deployments/${deployment}` +
186
+ `/chat/completions?api-version=${this.azureApiVersion}`);
187
+ }
188
+ /**
189
+ * Azure uses `api-key` rather than the standard `Authorization: Bearer`
190
+ * header expected by OpenAI-compatible endpoints.
191
+ */
192
+ getAuthHeaders() {
193
+ return { "api-key": this.config.apiKey };
194
+ }
195
+ /**
196
+ * Newer Azure deployments (o-series, gpt-5+) reject `max_tokens` and require
197
+ * `max_completion_tokens`. The `@ai-sdk/openai` path this migration replaced
198
+ * renamed the field automatically; replicate that here.
199
+ *
200
+ * Capability is taken from the explicit `useMaxCompletionTokens` override
201
+ * (credentials / `AZURE_OPENAI_USE_MAX_COMPLETION_TOKENS`) when set — Azure
202
+ * deployment names are user-defined, so a `chat-prod` gpt-5 deployment can't
203
+ * be detected from the name. When unset, fall back to a best-effort
204
+ * model-name heuristic for the common case where the deployment echoes the
205
+ * model (e.g. `gpt-5.4`).
206
+ */
207
+ adjustRequestBody(body, modelId) {
208
+ const needsMaxCompletion = this.useMaxCompletionTokensOverride ??
209
+ requiresMaxCompletionTokens(modelId);
210
+ if (body.max_tokens !== undefined && needsMaxCompletion) {
203
211
  return {
204
- stream: transformedStream,
205
- provider: "azure",
206
- model: this.deployment,
207
- metadata: {
208
- streamId: `azure-${Date.now()}`,
209
- startTime: Date.now(),
210
- },
212
+ ...body,
213
+ max_completion_tokens: body.max_tokens,
214
+ max_tokens: undefined,
211
215
  };
212
216
  }
213
- catch (error) {
214
- timeoutController?.cleanup();
215
- throw this.handleProviderError(error);
216
- }
217
+ return body;
217
218
  }
218
219
  }
219
- export default AzureOpenAIProvider;
@@ -19,7 +19,7 @@
19
19
  */
20
20
  import type { AIProviderName } from "../constants/enums.js";
21
21
  import { BaseProvider } from "../core/baseProvider.js";
22
- import type { LanguageModel, OpenAICompatBuildBodyArgs, OpenAICompatStreamLifecycleListeners, Schema, StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
22
+ import type { LanguageModel, OpenAICompatBuildBodyArgs, OpenAICompatChatRequest, OpenAICompatResponseFormat, OpenAICompatStreamLifecycleListeners, Schema, StreamOptions, StreamResult, ZodUnknownSchema } from "../types/index.js";
23
23
  /**
24
24
  * Abstract HTTP+SSE provider for OpenAI chat-completions-shaped endpoints.
25
25
  */
@@ -52,6 +52,24 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
52
52
  * (e.g. LiteLLM's Gemini 2.5 maxTokens skip).
53
53
  */
54
54
  protected adjustBuildBodyOptions(_modelId: string, opts: OpenAICompatBuildBodyArgs["options"]): OpenAICompatBuildBodyArgs["options"];
55
+ /**
56
+ * Hook to adjust the OpenAI `response_format` after it's converted from the
57
+ * V3 responseFormat (non-streaming `doGenerate` path). Default identity.
58
+ * Override for providers that don't support a given format type — e.g.
59
+ * DeepSeek rejects `response_format: { type: "json_schema" }` ("This
60
+ * response_format type is unavailable now"); the `@ai-sdk/openai-compatible`
61
+ * path this replaced declared `supportsStructuredOutputs: false`, which
62
+ * downgraded `json_schema` to `json_object`. Subclasses replicate that here.
63
+ */
64
+ protected adjustResponseFormat(rf: OpenAICompatResponseFormat | undefined, _modelId: string): OpenAICompatResponseFormat | undefined;
65
+ /**
66
+ * Hook to adjust the fully-built wire request body before it is sent, on
67
+ * both the streaming and non-streaming paths. Default identity. Override for
68
+ * provider/model quirks that can't be expressed through buildBody options —
69
+ * e.g. Azure's newer reasoning deployments (o-series, gpt-5+) reject
70
+ * `max_tokens` and require `max_completion_tokens`.
71
+ */
72
+ protected adjustRequestBody(body: OpenAICompatChatRequest, _modelId: string): OpenAICompatChatRequest;
55
73
  /**
56
74
  * Hook called once at the start of every `executeStream` invocation.
57
75
  * Return lifecycle listeners (onUsage / onFinish) to receive deferred
@@ -66,6 +84,18 @@ export declare abstract class OpenAIChatCompletionsProvider extends BaseProvider
66
84
  * `getDefaultModel()` will never hit this branch anyway.
67
85
  */
68
86
  protected shouldAutoDiscoverModel(): boolean;
87
+ /**
88
+ * Builds the chat-completions request URL for a model. Default is
89
+ * `${baseURL}/chat/completions`. Override for providers with a different
90
+ * routing scheme (e.g. Azure's deployment-based path + api-version query).
91
+ */
92
+ protected getChatCompletionsURL(_modelId: string): string;
93
+ /**
94
+ * Auth headers merged into every request. Default is a Bearer token.
95
+ * Override for providers that authenticate differently (e.g. Azure, which
96
+ * uses an `api-key` header instead of `Authorization: Bearer`).
97
+ */
98
+ protected getAuthHeaders(): Record<string, string>;
69
99
  supportsTools(): boolean;
70
100
  /**
71
101
  * Returns a minimal V3-shaped model used by BaseProvider's `generate()`
@@ -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>;