diff --git a/.env.example b/.env.example index 29652fe46..965ea95c8 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,15 @@ TWILIO_ACCOUNT_SID=ACxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx TWILIO_AUTH_TOKEN=your_auth_token_here # Must be a WhatsApp-enabled Twilio number, prefixed with whatsapp: TWILIO_WHATSAPP_FROM=whatsapp:+17343367101 + +# Azure OpenAI Configuration +# Your Azure OpenAI API key +AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here +# Your Azure OpenAI resource name (e.g., "my-openai-resource") +AZURE_OPENAI_RESOURCE_NAME=your_resource_name_here +# Your Azure OpenAI deployment name (e.g., "gpt-5", "gpt-5-codex") +AZURE_OPENAI_DEPLOYMENT_NAME=your_deployment_name_here +# API version (optional, defaults to 2024-08-01-preview) +AZURE_OPENAI_API_VERSION=2024-08-01-preview +# Or provide the full endpoint URL directly (alternative to resource name) +AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com diff --git a/docker-compose.azure.yml b/docker-compose.azure.yml new file mode 100644 index 000000000..d0b0bfcfd --- /dev/null +++ b/docker-compose.azure.yml @@ -0,0 +1,100 @@ +# Docker Compose for Azure OpenAI deployment +# Usage: +# 1. Copy .env.example to .env and fill in your Azure OpenAI credentials +# 2. Build the image: docker compose -f docker-compose.azure.yml build +# 3. Run the gateway: docker compose -f docker-compose.azure.yml up -d moltbot-azure-gateway +# 4. Or run the CLI: docker compose -f docker-compose.azure.yml run --rm moltbot-azure-cli + +services: + moltbot-azure-gateway: + build: + context: . + dockerfile: Dockerfile + image: moltbot-azure:local + container_name: moltbot-azure-gateway + environment: + HOME: /home/node + TERM: xterm-256color + # Gateway authentication token (generate a secure token for production) + CLAWDBOT_GATEWAY_TOKEN: ${CLAWDBOT_GATEWAY_TOKEN:-your-secure-gateway-token} + # Azure OpenAI Configuration (required) + AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY:?Azure OpenAI API key is required} + AZURE_OPENAI_RESOURCE_NAME: ${AZURE_OPENAI_RESOURCE_NAME:?Azure OpenAI resource name is required} + AZURE_OPENAI_DEPLOYMENT_NAME: ${AZURE_OPENAI_DEPLOYMENT_NAME:?Azure OpenAI deployment name is required} + AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-2024-08-01-preview} + # Optional: provide full endpoint URL instead of resource name + AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} + volumes: + # Persist configuration and session data + - ${CLAWDBOT_CONFIG_DIR:-./data/config}:/home/node/.moltbot + - ${CLAWDBOT_WORKSPACE_DIR:-./data/workspace}:/home/node/clawd + ports: + # Gateway HTTP/WebSocket port + - "${CLAWDBOT_GATEWAY_PORT:-18789}:18789" + # Bridge port for channel connections + - "${CLAWDBOT_BRIDGE_PORT:-18790}:18790" + init: true + restart: unless-stopped + command: + [ + "node", + "dist/index.js", + "gateway", + "--bind", + "${CLAWDBOT_GATEWAY_BIND:-lan}", + "--port", + "${CLAWDBOT_GATEWAY_PORT:-18789}", + "--allow-unconfigured" + ] + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:18789/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 30s + + moltbot-azure-cli: + build: + context: . + dockerfile: Dockerfile + image: moltbot-azure:local + container_name: moltbot-azure-cli + environment: + HOME: /home/node + TERM: xterm-256color + BROWSER: echo + # Azure OpenAI Configuration (required) + AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY:?Azure OpenAI API key is required} + AZURE_OPENAI_RESOURCE_NAME: ${AZURE_OPENAI_RESOURCE_NAME:?Azure OpenAI resource name is required} + AZURE_OPENAI_DEPLOYMENT_NAME: ${AZURE_OPENAI_DEPLOYMENT_NAME:?Azure OpenAI deployment name is required} + AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-2024-08-01-preview} + AZURE_OPENAI_ENDPOINT: ${AZURE_OPENAI_ENDPOINT} + volumes: + - ${CLAWDBOT_CONFIG_DIR:-./data/config}:/home/node/.moltbot + - ${CLAWDBOT_WORKSPACE_DIR:-./data/workspace}:/home/node/clawd + stdin_open: true + tty: true + init: true + entrypoint: ["node", "dist/index.js"] + + # Minimal test service to verify Azure OpenAI connection + moltbot-azure-test: + build: + context: . + dockerfile: Dockerfile + image: moltbot-azure:local + environment: + HOME: /home/node + TERM: xterm-256color + AZURE_OPENAI_API_KEY: ${AZURE_OPENAI_API_KEY:?Azure OpenAI API key is required} + AZURE_OPENAI_RESOURCE_NAME: ${AZURE_OPENAI_RESOURCE_NAME:?Azure OpenAI resource name is required} + AZURE_OPENAI_DEPLOYMENT_NAME: ${AZURE_OPENAI_DEPLOYMENT_NAME:?Azure OpenAI deployment name is required} + AZURE_OPENAI_API_VERSION: ${AZURE_OPENAI_API_VERSION:-2024-08-01-preview} + command: ["node", "dist/index.js", "models", "list"] + profiles: + - test + +# Named volumes for persistent storage (optional, use bind mounts above for easier access) +volumes: + moltbot-config: + moltbot-workspace: diff --git a/docs/providers/azure-openai.md b/docs/providers/azure-openai.md new file mode 100644 index 000000000..d6d6c3cc7 --- /dev/null +++ b/docs/providers/azure-openai.md @@ -0,0 +1,150 @@ +# Azure OpenAI Provider + +Configure Moltbot to use Azure OpenAI Service as a model provider. + +## Overview + +Azure OpenAI exposes OpenAI models deployed in your Azure subscription (for example `gpt-5` or `gpt-5-codex`) behind Azure endpoints and authentication. + +## Prerequisites + +1. An Azure subscription +2. An Azure OpenAI resource created in Azure Portal +3. At least one model deployment (e.g., `gpt-5` or `gpt-5-codex`) +4. API key from the Azure OpenAI resource + +## Configuration + +### Environment Variables + +Set the following environment variables to configure Azure OpenAI: + +| Variable | Required | Description | +|----------|----------|-------------| +| `AZURE_OPENAI_API_KEY` | Yes | Your Azure OpenAI API key | +| `AZURE_OPENAI_RESOURCE_NAME` | Yes* | Azure OpenAI resource name (e.g., `my-openai-resource`) | +| `AZURE_OPENAI_DEPLOYMENT_NAME` | Yes | Model deployment name (e.g., `gpt-5`) | +| `AZURE_OPENAI_API_VERSION` | No | API version (defaults to `2024-08-01-preview`) | +| `AZURE_OPENAI_ENDPOINT` | No | Full endpoint URL (alternative to resource name) | + +\* Either `AZURE_OPENAI_RESOURCE_NAME` or `AZURE_OPENAI_ENDPOINT` is required. + +### Example .env Configuration + +```bash +# Azure OpenAI Configuration +AZURE_OPENAI_API_KEY=your_azure_openai_api_key_here +AZURE_OPENAI_RESOURCE_NAME=my-openai-resource +AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5 +AZURE_OPENAI_API_VERSION=2024-08-01-preview + +# Alternative: provide full endpoint URL +# AZURE_OPENAI_ENDPOINT=https://my-openai-resource.openai.azure.com +``` + +## Docker Deployment + +A dedicated Docker Compose file (`docker-compose.azure.yml`) is provided for Azure OpenAI deployments. + +### Quick Start + +1. Copy `.env.example` to `.env` and fill in your Azure OpenAI credentials: + +```bash +cp .env.example .env +# Edit .env with your Azure OpenAI configuration +``` + +2. Build the Docker image: + +```bash +docker compose -f docker-compose.azure.yml build +``` + +3. Run the gateway: + +```bash +docker compose -f docker-compose.azure.yml up -d moltbot-azure-gateway +``` + +4. Or run the CLI interactively: + +```bash +docker compose -f docker-compose.azure.yml run --rm moltbot-azure-cli +``` + +### Services + +The `docker-compose.azure.yml` includes a gateway service, an interactive CLI service, and an optional test profile. + +### Testing Connection + +Verify your Azure OpenAI connection: + +```bash +docker compose -f docker-compose.azure.yml --profile test run --rm moltbot-azure-test +``` + +## API Endpoint Format + +Azure OpenAI uses a different URL format than OpenAI: + +``` +https://{resourceName}.openai.azure.com/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion} +``` + +The provider automatically handles: +- Building the correct endpoint URL from resource and deployment names +- Adding the `api-version` query parameter via a global fetch wrapper +- Using the `api-key` header instead of Bearer token authentication + +## Supported Models + +Moltbot treats Azure deployments as model IDs. Use your deployment name as `AZURE_OPENAI_DEPLOYMENT_NAME`. + +Recommended (newer) deployment names to use in docs/examples: + +- `gpt-5` +- `gpt-5-mini` +- `gpt-5-nano` +- `gpt-5-codex` + +## Troubleshooting + +### Common Issues + +**401 Unauthorized** +- Verify your `AZURE_OPENAI_API_KEY` is correct +- Check that the API key has access to the specified deployment + +**404 Not Found** +- Verify `AZURE_OPENAI_RESOURCE_NAME` matches your Azure resource +- Verify `AZURE_OPENAI_DEPLOYMENT_NAME` matches an existing deployment +- Check that the deployment is in a "Succeeded" state in Azure Portal + +**API Version Errors** +- Try updating `AZURE_OPENAI_API_VERSION` to a supported version +- Check [Azure OpenAI API versions](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) for the latest + +### Verifying Configuration + +List available models to verify your configuration: + +```bash +moltbot models list +``` + +The Azure OpenAI deployment should appear as `azure-openai/{deployment-name}`. + +## Security Considerations + +- Store API keys securely using environment variables or a secrets manager +- Consider using Azure Managed Identity for production deployments +- Review Azure OpenAI content filtering policies for your use case +- Ensure your Azure resource has appropriate network access controls + +## Related Documentation + +- [Azure OpenAI Service Documentation](https://learn.microsoft.com/en-us/azure/ai-services/openai/) +- [Azure OpenAI API Reference](https://learn.microsoft.com/en-us/azure/ai-services/openai/reference) +- [Moltbot Configuration](/configuration) diff --git a/src/agents/azure-openai-provider.ts b/src/agents/azure-openai-provider.ts new file mode 100644 index 000000000..32f5f1000 --- /dev/null +++ b/src/agents/azure-openai-provider.ts @@ -0,0 +1,342 @@ +/** + * Azure OpenAI Provider Configuration + * + * This module provides support for Azure OpenAI Service, which uses a different + * API endpoint format and authentication mechanism compared to OpenAI's standard API. + * + * Azure OpenAI endpoint format: + * https://{resourceName}.openai.azure.com/openai/deployments/{deploymentName}/chat/completions?api-version={apiVersion} + */ + +import type { ModelDefinitionConfig } from "../config/types.models.js"; +import type { ProviderConfig } from "./models-config.providers.js"; + +// Store for Azure OpenAI configurations to enable fetch interception +const azureOpenAIConfigs = new Map(); + +/** + * Register an Azure OpenAI resource for fetch interception. + * This enables the global fetch wrapper to add api-version query parameters. + */ +export function registerAzureOpenAIResource(resourceName: string, apiVersion: string): void { + azureOpenAIConfigs.set(resourceName.toLowerCase(), { apiVersion }); +} + +/** + * Check if a URL is an Azure OpenAI endpoint and get its configuration. + */ +export function getAzureOpenAIConfig( + url: string, +): { resourceName: string; apiVersion: string } | null { + try { + const parsed = new URL(url); + const match = /^([^.]+)\.openai\.azure\.com$/.exec(parsed.hostname); + if (!match) return null; + const resourceName = match[1].toLowerCase(); + const config = azureOpenAIConfigs.get(resourceName); + if (!config) return null; + return { resourceName, apiVersion: config.apiVersion }; + } catch { + return null; + } +} + +// Track if the fetch wrapper has been installed +let fetchWrapperInstalled = false; +let originalFetch: typeof fetch | null = null; + +/** + * Install a global fetch wrapper that adds api-version query parameter + * to Azure OpenAI requests. + */ +export function installAzureOpenAIFetchWrapper(): void { + if (fetchWrapperInstalled) return; + + originalFetch = globalThis.fetch; + fetchWrapperInstalled = true; + + globalThis.fetch = async function azureOpenAIFetchWrapper( + input: RequestInfo | URL, + init?: RequestInit, + ): Promise { + const url = + typeof input === "string" ? input : input instanceof URL ? input.toString() : input.url; + const azureConfig = getAzureOpenAIConfig(url); + + if (azureConfig) { + // Add api-version query parameter to Azure OpenAI requests + const parsedUrl = new URL(url); + if (!parsedUrl.searchParams.has("api-version")) { + parsedUrl.searchParams.set("api-version", azureConfig.apiVersion); + const newUrl = parsedUrl.toString(); + + // Recreate the request with the new URL + if (typeof input === "string") { + return originalFetch!(newUrl, init); + } else if (input instanceof URL) { + return originalFetch!(new URL(newUrl), init); + } else { + // Request object - create new request with modified URL + const newRequest = new Request(newUrl, { + method: input.method, + headers: input.headers, + body: init?.body ?? input.body, + mode: input.mode, + credentials: input.credentials, + cache: input.cache, + redirect: input.redirect, + referrer: input.referrer, + integrity: input.integrity, + signal: init?.signal ?? input.signal, + }); + return originalFetch!(newRequest); + } + } + } + + return originalFetch!(input, init); + }; +} + +/** + * Uninstall the Azure OpenAI fetch wrapper (for testing). + */ +export function uninstallAzureOpenAIFetchWrapper(): void { + if (!fetchWrapperInstalled || !originalFetch) return; + globalThis.fetch = originalFetch; + fetchWrapperInstalled = false; + originalFetch = null; +} + +// Azure OpenAI default configuration +const AZURE_OPENAI_DEFAULT_API_VERSION = "2024-08-01-preview"; +const AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW = 128000; +const AZURE_OPENAI_DEFAULT_MAX_TOKENS = 4096; +const AZURE_OPENAI_DEFAULT_COST = { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, +}; + +export interface AzureOpenAIConfig { + /** Azure OpenAI resource name (the name of your Azure OpenAI resource) */ + resourceName: string; + /** Deployment name (the name of your model deployment) */ + deploymentName: string; + /** API version (defaults to 2024-08-01-preview) */ + apiVersion?: string; + /** Azure OpenAI API key */ + apiKey?: string; + /** Custom model display name */ + modelName?: string; + /** Whether this is a reasoning model (like o1) */ + reasoning?: boolean; + /** Supported input types */ + input?: Array<"text" | "image">; + /** Context window size */ + contextWindow?: number; + /** Max output tokens */ + maxTokens?: number; +} + +/** + * Builds the Azure OpenAI base URL from resource name + */ +export function buildAzureOpenAIBaseUrl(resourceName: string): string { + return `https://${resourceName}.openai.azure.com`; +} + +/** + * Builds the full Azure OpenAI API endpoint URL + */ +export function buildAzureOpenAIEndpoint( + resourceName: string, + deploymentName: string, + apiVersion: string = AZURE_OPENAI_DEFAULT_API_VERSION, +): string { + return `${buildAzureOpenAIBaseUrl(resourceName)}/openai/deployments/${deploymentName}/chat/completions?api-version=${apiVersion}`; +} + +/** + * Creates a model definition for Azure OpenAI deployment + */ +export function buildAzureOpenAIModelDefinition(config: AzureOpenAIConfig): ModelDefinitionConfig { + const modelId = config.deploymentName; + const modelName = config.modelName ?? `Azure ${config.deploymentName}`; + + return { + id: modelId, + name: modelName, + reasoning: config.reasoning ?? false, + input: config.input ?? ["text"], + cost: AZURE_OPENAI_DEFAULT_COST, + contextWindow: config.contextWindow ?? AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: config.maxTokens ?? AZURE_OPENAI_DEFAULT_MAX_TOKENS, + }; +} + +/** + * Builds a provider configuration for Azure OpenAI + * + * Azure OpenAI uses a different URL format: + * https://{resource}.openai.azure.com/openai/deployments/{deployment}/chat/completions?api-version={version} + * + * We set the baseUrl to include the deployment path so the OpenAI SDK appends /chat/completions correctly. + * The api-version query parameter is added automatically via a global fetch wrapper. + */ +export function buildAzureOpenAIProvider(config: AzureOpenAIConfig): ProviderConfig { + const apiVersion = config.apiVersion ?? AZURE_OPENAI_DEFAULT_API_VERSION; + // Azure OpenAI requires the deployment in the URL path + // Format: https://{resource}.openai.azure.com/openai/deployments/{deployment} + // The SDK will append /chat/completions to this + const baseUrl = `https://${config.resourceName}.openai.azure.com/openai/deployments/${config.deploymentName}`; + + // Register the resource for fetch interception to add api-version query param + registerAzureOpenAIResource(config.resourceName, apiVersion); + // Install the global fetch wrapper + installAzureOpenAIFetchWrapper(); + + return { + baseUrl, + api: "openai-completions", + apiKey: config.apiKey, + // Azure OpenAI uses api-key header instead of Bearer token + authHeader: false, + headers: { + "api-key": config.apiKey ?? "", + }, + // Azure requires api-version as query parameter - store in provider config + azureResourceName: config.resourceName, + azureApiVersion: apiVersion, + models: [buildAzureOpenAIModelDefinition(config)], + }; +} + +/** + * Builds a provider configuration for Azure OpenAI with multiple deployments + * + * Note: Each deployment gets its own baseUrl with the deployment name in the path. + * For multiple deployments, we use the first deployment in the baseUrl. + * The api-version query parameter is added automatically via a global fetch wrapper. + */ +export function buildAzureOpenAIProviderMultiDeployment(params: { + resourceName: string; + apiKey?: string; + apiVersion?: string; + deployments: Array<{ + name: string; + displayName?: string; + reasoning?: boolean; + input?: Array<"text" | "image">; + contextWindow?: number; + maxTokens?: number; + }>; +}): ProviderConfig { + const apiVersion = params.apiVersion ?? AZURE_OPENAI_DEFAULT_API_VERSION; + const firstDeployment = params.deployments[0]?.name ?? "default"; + const baseUrl = `https://${params.resourceName}.openai.azure.com/openai/deployments/${firstDeployment}`; + + // Register the resource for fetch interception to add api-version query param + registerAzureOpenAIResource(params.resourceName, apiVersion); + // Install the global fetch wrapper + installAzureOpenAIFetchWrapper(); + + const models: ModelDefinitionConfig[] = params.deployments.map((deployment) => ({ + id: deployment.name, + name: deployment.displayName ?? `Azure ${deployment.name}`, + reasoning: deployment.reasoning ?? false, + input: deployment.input ?? ["text"], + cost: AZURE_OPENAI_DEFAULT_COST, + contextWindow: deployment.contextWindow ?? AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: deployment.maxTokens ?? AZURE_OPENAI_DEFAULT_MAX_TOKENS, + })); + + return { + baseUrl, + api: "openai-completions", + apiKey: params.apiKey, + authHeader: false, + headers: { + "api-key": params.apiKey ?? "", + }, + azureResourceName: params.resourceName, + azureApiVersion: apiVersion, + models, + }; +} + +/** + * Environment variable names for Azure OpenAI configuration + */ +export const AZURE_OPENAI_ENV = { + API_KEY: "AZURE_OPENAI_API_KEY", + RESOURCE_NAME: "AZURE_OPENAI_RESOURCE_NAME", + DEPLOYMENT_NAME: "AZURE_OPENAI_DEPLOYMENT_NAME", + API_VERSION: "AZURE_OPENAI_API_VERSION", + ENDPOINT: "AZURE_OPENAI_ENDPOINT", +} as const; + +/** + * Resolves Azure OpenAI configuration from environment variables + */ +export function resolveAzureOpenAIConfigFromEnv( + env: NodeJS.ProcessEnv = process.env, +): AzureOpenAIConfig | null { + const apiKey = env[AZURE_OPENAI_ENV.API_KEY]?.trim(); + const resourceName = env[AZURE_OPENAI_ENV.RESOURCE_NAME]?.trim(); + const deploymentName = env[AZURE_OPENAI_ENV.DEPLOYMENT_NAME]?.trim(); + const apiVersion = env[AZURE_OPENAI_ENV.API_VERSION]?.trim(); + + // If endpoint is provided directly, parse resource name from it + const endpoint = env[AZURE_OPENAI_ENV.ENDPOINT]?.trim(); + let resolvedResourceName = resourceName; + if (!resolvedResourceName && endpoint) { + const match = /https:\/\/([^.]+)\.openai\.azure\.com/.exec(endpoint); + if (match) { + resolvedResourceName = match[1]; + } + } + + if (!apiKey || !resolvedResourceName || !deploymentName) { + return null; + } + + return { + resourceName: resolvedResourceName, + deploymentName, + apiVersion: apiVersion || AZURE_OPENAI_DEFAULT_API_VERSION, + apiKey, + }; +} + +/** + * Default export for common models available on Azure OpenAI + * These are the most commonly deployed models + */ +export const AZURE_OPENAI_COMMON_MODELS = { + "gpt-5": { + reasoning: false, + input: ["text", "image"] as Array<"text" | "image">, + contextWindow: AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: AZURE_OPENAI_DEFAULT_MAX_TOKENS, + }, + "gpt-5-mini": { + reasoning: false, + input: ["text", "image"] as Array<"text" | "image">, + contextWindow: AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: AZURE_OPENAI_DEFAULT_MAX_TOKENS, + }, + "gpt-5-nano": { + reasoning: false, + input: ["text", "image"] as Array<"text" | "image">, + contextWindow: AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: AZURE_OPENAI_DEFAULT_MAX_TOKENS, + }, + "gpt-5-codex": { + reasoning: false, + input: ["text"] as Array<"text" | "image">, + contextWindow: AZURE_OPENAI_DEFAULT_CONTEXT_WINDOW, + maxTokens: AZURE_OPENAI_DEFAULT_MAX_TOKENS, + }, +} as const; diff --git a/src/agents/model-auth.ts b/src/agents/model-auth.ts index 1445b53f7..4a4bbac01 100644 --- a/src/agents/model-auth.ts +++ b/src/agents/model-auth.ts @@ -286,6 +286,7 @@ export function resolveEnvApiKey(provider: string): EnvApiKeyResult | null { venice: "VENICE_API_KEY", mistral: "MISTRAL_API_KEY", opencode: "OPENCODE_API_KEY", + "azure-openai": "AZURE_OPENAI_API_KEY", }; const envVar = envMap[normalized]; if (!envVar) return null; diff --git a/src/agents/models-config.providers.ts b/src/agents/models-config.providers.ts index 0cd034c82..dadea6960 100644 --- a/src/agents/models-config.providers.ts +++ b/src/agents/models-config.providers.ts @@ -13,6 +13,11 @@ import { SYNTHETIC_MODEL_CATALOG, } from "./synthetic-models.js"; import { discoverVeniceModels, VENICE_BASE_URL } from "./venice-models.js"; +import { + buildAzureOpenAIProvider, + resolveAzureOpenAIConfigFromEnv, + AZURE_OPENAI_ENV, +} from "./azure-openai-provider.js"; type ModelsConfig = NonNullable; export type ProviderConfig = NonNullable[string]; @@ -454,6 +459,30 @@ export async function resolveImplicitProviders(params: { providers.ollama = { ...(await buildOllamaProvider()), apiKey: ollamaKey }; } + // Azure OpenAI provider - auto-discover from environment variables + const azureOpenAIConfig = resolveAzureOpenAIConfigFromEnv(); + if (azureOpenAIConfig) { + providers["azure-openai"] = buildAzureOpenAIProvider(azureOpenAIConfig); + } else { + // Check for API key in auth profiles + const azureOpenAIKey = + resolveEnvApiKeyVarName("azure-openai") ?? + resolveApiKeyFromProfiles({ provider: "azure-openai", store: authStore }); + if (azureOpenAIKey) { + // If we have an API key but not full config, check for resource/deployment in env + const resourceName = process.env[AZURE_OPENAI_ENV.RESOURCE_NAME]?.trim(); + const deploymentName = process.env[AZURE_OPENAI_ENV.DEPLOYMENT_NAME]?.trim(); + if (resourceName && deploymentName) { + providers["azure-openai"] = buildAzureOpenAIProvider({ + resourceName, + deploymentName, + apiKey: azureOpenAIKey, + apiVersion: process.env[AZURE_OPENAI_ENV.API_VERSION]?.trim(), + }); + } + } + } + return providers; } diff --git a/src/config/types.models.ts b/src/config/types.models.ts index 11b6c64cb..c7201812a 100644 --- a/src/config/types.models.ts +++ b/src/config/types.models.ts @@ -41,6 +41,10 @@ export type ModelProviderConfig = { headers?: Record; authHeader?: boolean; models: ModelDefinitionConfig[]; + /** Azure OpenAI specific: resource name */ + azureResourceName?: string; + /** Azure OpenAI specific: API version */ + azureApiVersion?: string; }; export type BedrockDiscoveryConfig = {