UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

230 lines (229 loc) • 11.2 kB
import { APIVersions } from "../constants/enums.js"; import { AuthenticationError } from "../types/index.js"; import { logger } from "../utils/logger.js"; import { classifyProviderError, DEFAULT_ERROR_RULES, } from "../utils/errorClassifier.js"; import { createAzureAPIKeyConfig, createAzureEndpointConfig, validateApiKey, } from "../utils/providerConfig.js"; import { transformParamsForLogging } from "../utils/transformationUtils.js"; import { OpenAIChatCompletionsProvider } from "./openaiChatCompletionsBase.js"; import { requiresMaxCompletionTokens } from "./openaiChatCompletionsClient.js"; /** * Azure OpenAI Provider — direct HTTP, no AI SDK. * * Supports both classic Azure OpenAI Service endpoints * ("*.openai.azure.com", "*.cognitiveservices.azure.com") and the newer * Azure AI Foundry endpoints ("*.services.ai.azure.com"). * * All request/stream/tool-loop orchestration lives in * `OpenAIChatCompletionsProvider`; this class overrides the URL builder and * auth headers to accommodate Azure's deployment-based routing and * `api-key` header (rather than Bearer tokens). * * @see https://learn.microsoft.com/azure/cognitive-services/openai/ */ export class AzureOpenAIProvider extends OpenAIChatCompletionsProvider { // Azure-specific routing state resolved once in the constructor. azureDeployment; azureApiVersion; // Parsed from the endpoint — mutually exclusive: either resourceName (for // classic hosts) or foundryBaseURL (for AI Foundry) is non-empty. azureResourceOrigin; azureDeploymentPathPrefix; // Explicit `max_completion_tokens` capability. `undefined` ⇒ fall back to the // deployment-name heuristic in adjustRequestBody(). useMaxCompletionTokensOverride; constructor(modelName, sdk, _region, credentials) { const apiKey = credentials?.apiKey || process.env.AZURE_OPENAI_API_KEY || ""; // ----------------------------------------------------------------------- // Parse the AZURE_OPENAI_ENDPOINT environment variable (or credentials) // into the pieces needed to build deployment-based chat completions URLs. // // Two supported endpoint formats: // // 1. Classic Azure OpenAI / Cognitive Services: // https://<resource>.openai.azure.com // https://<resource>.cognitiveservices.azure.com // The @ai-sdk/azure tradition was to pass the bare resource subdomain // and let the SDK reconstruct the full URL. We instead keep the full // origin and emit the standard deployment path from it: // {origin}/openai/deployments/{deployment}/chat/completions // // 2. Azure AI Foundry: // https://<host>.services.ai.azure.com[/openai] // The host has no resource-name subdomain convention. The operator // may or may not include the "/openai" path prefix; we normalise that. // Final URL pattern: // {origin}{normalisedPath}/deployments/{deployment}/chat/completions // // In both cases we pass the "resource origin" (scheme+host) as `baseURL` // to super so that `getAvailableModels()` can still build a models URL // from it if needed; `getChatCompletionsURL()` builds the real path. // ----------------------------------------------------------------------- const endpoint = process.env.AZURE_OPENAI_ENDPOINT || ""; let endpointUrl; if (endpoint) { try { endpointUrl = new URL(endpoint.includes("://") ? endpoint : `https://${endpoint}`); } catch { endpointUrl = undefined; } } const endpointHost = endpointUrl?.hostname ?? ""; // Strip trailing slashes from the pathname; treat "/" as empty. const endpointPath = endpointUrl?.pathname && endpointUrl.pathname !== "/" ? endpointUrl.pathname.replace(/\/+$/, "") : ""; // Classic hosts encode the resource name as a subdomain. const isClassicAzureHost = /\.(openai|cognitiveservices)\.azure\.com$/.test(endpointHost); // For classic hosts the deployment URL path always starts with "/openai". // For Foundry hosts we reuse whatever the operator supplied, appending // "/openai" only when the path doesn't already end with it (or a // versioned variant like "/openai/v1"). let deploymentPathPrefix; if (isClassicAzureHost) { deploymentPathPrefix = "/openai"; } else { const hasOpenAIPathSuffix = /\/openai(?:\/v\d+)?$/.test(endpointPath); deploymentPathPrefix = hasOpenAIPathSuffix ? endpointPath : `${endpointPath}/openai`; } const resourceOrigin = endpointUrl?.origin ?? ""; const deployment = credentials?.deploymentName || modelName || process.env.AZURE_OPENAI_MODEL || process.env.AZURE_OPENAI_DEPLOYMENT || process.env.AZURE_OPENAI_DEPLOYMENT_ID || "gpt-4o"; const apiVersion = credentials?.apiVersion || process.env.AZURE_API_VERSION || APIVersions.AZURE_LATEST; // Deployment names are user-defined, so a model-name heuristic can't // reliably tell whether the backing model needs max_completion_tokens. // Prefer an explicit signal (credentials or env); fall back to the // heuristic only when unset. const envMaxCompletion = process.env.AZURE_OPENAI_USE_MAX_COMPLETION_TOKENS; const maxCompletionOverride = credentials?.useMaxCompletionTokens ?? (envMaxCompletion === undefined ? undefined : /^(1|true|yes)$/i.test(envMaxCompletion)); // Validate required credentials before committing. if (!apiKey) { validateApiKey(createAzureAPIKeyConfig()); } if (!resourceOrigin) { validateApiKey(createAzureEndpointConfig()); } // Pass the resource origin as baseURL so the base class can construct // auxiliary URLs (e.g. /models) from it when needed. super("azure", modelName, sdk, { baseURL: resourceOrigin, apiKey, }); this.azureDeployment = deployment; this.azureApiVersion = apiVersion; this.azureResourceOrigin = resourceOrigin; this.azureDeploymentPathPrefix = deploymentPathPrefix; this.useMaxCompletionTokensOverride = maxCompletionOverride; if (logger.shouldLog("debug")) { logger.debug("Azure OpenAI Provider initialized", transformParamsForLogging({ deployment: this.azureDeployment, resourceOrigin: this.azureResourceOrigin, deploymentPathPrefix: this.azureDeploymentPathPrefix, apiVersion: this.azureApiVersion, provider: "azure", })); } } // =========================================================================== // Abstract-hook implementations // =========================================================================== /** * Azure OpenAI natively supports `response_format: json_schema` together * with tool calling in one request, so structured output stays * wire-enforced mid-loop instead of deferring to post-hoc coercion. */ suppressResponseFormatWithTools() { return false; } getProviderName() { return "azure"; } /** * The "default model" for Azure is the deployment name — it's the * identifier callers pass to select a deployment. */ getDefaultModel() { return this.azureDeployment; } formatProviderError(error) { // RULING 1 fix: previously only a raw "401" substring was special-cased — // everything else (429, 404, network errors, 5xx) fell through to one // generic ProviderError. The 401 branch below is preserved byte-for-byte // (message text + the raw-substring match style); DEFAULT_ERROR_RULES now // gives 429/404/network/5xx their correct classifications instead of the // old undifferentiated catch-all. const rules = [ { match: (ctx) => /401/.test(ctx.message), errorClass: AuthenticationError, message: "Invalid Azure OpenAI API key or endpoint.", }, ...DEFAULT_ERROR_RULES, ]; return classifyProviderError(error, rules, this.providerName, this.modelName); } // =========================================================================== // New overridable hooks (provided by the base-enhancement branch) // =========================================================================== /** * Builds the full Azure deployment chat completions URL. * * Pattern: * {resourceOrigin}{deploymentPathPrefix}/deployments/{deployment}/chat/completions?api-version={apiVersion} * * Examples: * Classic: https://myresource.openai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview * Foundry: https://myhost.services.ai.azure.com/openai/deployments/gpt-4o/chat/completions?api-version=2025-04-01-preview */ getChatCompletionsURL(modelId) { // modelId is the deployment name when it has been resolved; fall back to // the stored deployment when the base passes a generic placeholder. const deployment = modelId || this.azureDeployment; const prefix = this.azureDeploymentPathPrefix.replace(/\/+$/, ""); return (`${this.azureResourceOrigin}${prefix}/deployments/${deployment}` + `/chat/completions?api-version=${this.azureApiVersion}`); } /** * Azure uses `api-key` rather than the standard `Authorization: Bearer` * header expected by OpenAI-compatible endpoints. */ getAuthHeaders() { return { "api-key": this.config.apiKey }; } /** * Newer Azure deployments (o-series, gpt-5+) reject `max_tokens` and require * `max_completion_tokens`. The `@ai-sdk/openai` path this migration replaced * renamed the field automatically; replicate that here. * * Capability is taken from the explicit `useMaxCompletionTokens` override * (credentials / `AZURE_OPENAI_USE_MAX_COMPLETION_TOKENS`) when set — Azure * deployment names are user-defined, so a `chat-prod` gpt-5 deployment can't * be detected from the name. When unset, fall back to a best-effort * model-name heuristic for the common case where the deployment echoes the * model (e.g. `gpt-5.4`). */ adjustRequestBody(body, modelId) { const needsMaxCompletion = this.useMaxCompletionTokensOverride ?? requiresMaxCompletionTokens(modelId); if (body.max_tokens !== undefined && needsMaxCompletion) { return { ...body, max_completion_tokens: body.max_tokens, max_tokens: undefined, }; } return body; } }