@juspay/neurolink
Version:
Universal AI Development Platform with working MCP integration, multi-provider support, voice (TTS/STT/realtime), and professional CLI. 58+ external MCP servers discoverable, multimodal file processing, RAG pipelines. Build, test, and deploy AI applicatio
219 lines • 10.9 kB
JavaScript
import { createAzure } from "@ai-sdk/azure";
import { stepCountIs, streamText } from "ai";
import { APIVersions } from "../constants/enums.js";
import { BaseProvider } from "../core/baseProvider.js";
import { DEFAULT_MAX_STEPS } from "../core/constants.js";
import { createProxyFetch } from "../proxy/proxyFetch.js";
import { emitToolEndFromStepFinish } from "../utils/toolEndEmitter.js";
import { AuthenticationError, NetworkError, ProviderError, } from "../types/index.js";
import { logger } from "../utils/logger.js";
import { createAzureAPIKeyConfig, createAzureEndpointConfig, validateApiKey, } from "../utils/providerConfig.js";
import { composeAbortSignals, createTimeoutController, TimeoutError, } from "../utils/timeout.js";
import { resolveToolChoice } from "../utils/toolChoice.js";
export class AzureOpenAIProvider extends BaseProvider {
apiKey;
resourceName;
deployment;
apiVersion;
azureProvider;
constructor(modelName, sdk, _region, credentials) {
super(modelName, "azure", sdk);
this.apiKey = credentials?.apiKey || process.env.AZURE_OPENAI_API_KEY || "";
const endpoint = process.env.AZURE_OPENAI_ENDPOINT || "";
// Use URL parsing instead of string-replace so endpoints that already
// carry a path segment (e.g. "https://<host>/openai" — a valid Azure AI
// Foundry shape) don't end up duplicating it as "<host>/openai/openai".
// Tolerate missing scheme by prefixing https:// before parsing.
let endpointUrl;
if (endpoint) {
try {
endpointUrl = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`);
}
catch {
endpointUrl = undefined;
}
}
const endpointHost = endpointUrl?.hostname ?? "";
const endpointPath = endpointUrl?.pathname && endpointUrl.pathname !== "/"
? endpointUrl.pathname.replace(/\/+$/, "")
: "";
// Classic Azure OpenAI ("*.openai.azure.com") and Cognitive Services
// ("*.cognitiveservices.azure.com") endpoints encode the resource name as
// a subdomain that @ai-sdk/azure expects to receive verbatim. The newer
// Azure AI Foundry endpoint format ("*.services.ai.azure.com") does not
// round-trip through that subdomain rewrite, so passing the resource name
// would yield e.g. "<host>.services.ai.azure.com.openai.azure.com". For
// those hosts we hand the full URL back via baseURL instead.
const isClassicAzureHost = /\.(openai|cognitiveservices)\.azure\.com$/.test(endpointHost);
const envResourceName = isClassicAzureHost
? endpointHost
.replace(".openai.azure.com", "")
.replace(".cognitiveservices.azure.com", "")
: "";
this.resourceName = credentials?.resourceName || envResourceName;
// For Azure AI Foundry the SDK still routes to the OpenAI-compatible API
// (deployments/{deployment}/chat/completions); the `/openai` path suffix
// mirrors what the SDK derives in classic mode
// (`https://${resource}.openai.azure.com/openai`). Reuse the path the
// operator already supplied if it already terminates in `/openai` *or*
// a versioned form like `/openai/v1`; otherwise append `/openai`. Never
// duplicate.
const hasOpenAIPathSuffix = /\/openai(?:\/v\d+)?$/.test(endpointPath);
const baseURLForFoundry = !this.resourceName && endpointUrl
? `${endpointUrl.origin}${hasOpenAIPathSuffix ? endpointPath : `${endpointPath}/openai`}`
: undefined;
this.deployment =
credentials?.deploymentName ||
modelName ||
process.env.AZURE_OPENAI_MODEL ||
process.env.AZURE_OPENAI_DEPLOYMENT ||
process.env.AZURE_OPENAI_DEPLOYMENT_ID ||
"gpt-4o";
this.apiVersion =
credentials?.apiVersion ||
process.env.AZURE_API_VERSION ||
APIVersions.AZURE_LATEST;
// Configuration validation - now using consolidated utility
if (!this.apiKey) {
validateApiKey(createAzureAPIKeyConfig());
}
if (!this.resourceName && !baseURLForFoundry) {
validateApiKey(createAzureEndpointConfig());
}
// Create the Azure provider instance with proxy support.
// For classic *.openai.azure.com / *.cognitiveservices.azure.com hosts we
// pass `resourceName`, which @ai-sdk/azure rewrites into the canonical
// subdomain. For Azure AI Foundry hosts ("*.services.ai.azure.com") we
// pass the full URL via `baseURL` so no rewrite happens.
// useDeploymentBasedUrls is required because @ai-sdk/azure v3+ defaults to
// the /v1/ URL format, but most Azure deployments still require the legacy
// /deployments/{deployment}/ URL pattern.
this.azureProvider = baseURLForFoundry
? createAzure({
baseURL: baseURLForFoundry,
apiKey: this.apiKey,
apiVersion: this.apiVersion,
useDeploymentBasedUrls: true,
fetch: createProxyFetch(),
})
: createAzure({
resourceName: this.resourceName,
apiKey: this.apiKey,
apiVersion: this.apiVersion,
useDeploymentBasedUrls: true,
fetch: createProxyFetch(),
});
logger.debug("Azure Vercel Provider initialized", {
deployment: this.deployment,
resourceName: this.resourceName,
provider: "azure-vercel",
});
}
getProviderName() {
return "azure";
}
getDefaultModel() {
return this.deployment;
}
/**
* Returns the Vercel AI SDK model instance for Azure OpenAI.
* Uses .chat() explicitly because @ai-sdk/azure v3+ defaults the bare
* provider() call to the Responses API, which many Azure deployments
* do not support yet.
*/
getAISDKModel() {
return this.azureProvider.chat(this.deployment);
}
formatProviderError(error) {
if (error instanceof TimeoutError) {
return new NetworkError(`Request timed out: ${error.message}`, "azure");
}
const errorObj = error;
if (errorObj?.message &&
typeof errorObj.message === "string" &&
errorObj.message.includes("401")) {
return new AuthenticationError("Invalid Azure OpenAI API key or endpoint.", "azure");
}
const message = errorObj?.message && typeof errorObj.message === "string"
? errorObj.message
: "Unknown error";
return new ProviderError(`Azure OpenAI error: ${message}`, "azure");
}
// executeGenerate removed - BaseProvider handles all generation with tools
async executeStream(options, _analysisSchema) {
const timeout = this.getTimeout(options);
const timeoutController = createTimeoutController(timeout, this.providerName, "stream");
try {
// Get tools - options.tools is pre-merged by BaseProvider.stream()
const shouldUseTools = !options.disableTools && this.supportsTools();
const tools = shouldUseTools
? options.tools || (await this.getAllTools())
: {};
logger.debug("Azure Stream - Tool Loading Debug", {
shouldUseTools,
toolCount: Object.keys(tools).length,
toolNames: Object.keys(tools).slice(0, 10),
disableTools: options.disableTools,
supportsTools: this.supportsTools(),
});
// Build message array from options with multimodal support
// Using protected helper from BaseProvider to eliminate code duplication
const messages = await this.buildMessagesForStream(options);
const model = await this.getAISDKModelWithMiddleware(options);
// Reviewer follow-up: capture upstream provider errors via onError
// so the post-stream NoOutput sentinel carries the real cause.
let capturedProviderError;
const stream = await streamText({
model,
messages: messages,
...(options.maxTokens !== null && options.maxTokens !== undefined
? { maxOutputTokens: options.maxTokens }
: {}),
...(options.temperature !== null && options.temperature !== undefined
? { temperature: options.temperature }
: {}),
tools,
toolChoice: resolveToolChoice(options, tools, shouldUseTools),
stopWhen: stepCountIs(options.maxSteps || DEFAULT_MAX_STEPS),
abortSignal: composeAbortSignals(options.abortSignal, timeoutController?.controller.signal),
experimental_telemetry: this.telemetryHandler.getTelemetryConfig(options),
experimental_repairToolCall: this.getToolCallRepairFn(options),
onError: (event) => {
capturedProviderError = event.error;
logger.error("AzureOpenAI: Stream error", {
error: event.error instanceof Error
? event.error.message
: String(event.error),
});
},
onStepFinish: (event) => {
emitToolEndFromStepFinish(this.neurolink?.getEventEmitter(), event.toolResults);
this.handleToolExecutionStorage([...event.toolCalls], [...event.toolResults], options, new Date()).catch((error) => {
logger.warn("[AzureOpenaiProvider] Failed to store tool executions", {
provider: this.providerName,
error: error instanceof Error ? error.message : String(error),
});
});
},
});
timeoutController?.cleanup();
// Transform string stream to content object stream using BaseProvider method
const transformedStream = this.createTextStream(stream, () => capturedProviderError);
return {
stream: transformedStream,
provider: "azure",
model: this.deployment,
metadata: {
streamId: `azure-${Date.now()}`,
startTime: Date.now(),
},
};
}
catch (error) {
timeoutController?.cleanup();
throw this.handleProviderError(error);
}
}
}
export default AzureOpenAIProvider;
//# sourceMappingURL=azureOpenai.js.map