From e3dcb244f81e3c699a9f5dab96dec0d792c1a51c Mon Sep 17 00:00:00 2001 From: Konstantin Gukov Date: Tue, 23 Jun 2026 15:38:43 +0200 Subject: [PATCH] Support modern Microsoft Foundry Responses API endpoints: - support *.ai.azure.com endpoints - normalize away the /responses postfix in the query path which by default is added on Microsoft Foundry UI --- packages/ai/README.md | 2 +- packages/ai/src/api/azure-openai-responses.ts | 12 ++++++++++-- packages/ai/test/azure-openai-base-url.test.ts | 10 ++++++++++ packages/coding-agent/docs/providers.md | 3 ++- 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/packages/ai/README.md b/packages/ai/README.md index f81f61900..b058bdad7 100644 --- a/packages/ai/README.md +++ b/packages/ai/README.md @@ -368,7 +368,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex |----------|------------------------| | OpenAI | `OPENAI_API_KEY` | | Ant Ling | `ANT_LING_API_KEY` | -| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.openai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com` and `*.cognitiveservices.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | +| Azure OpenAI | `AZURE_OPENAI_API_KEY` + `AZURE_OPENAI_BASE_URL` (e.g. `https://{resource}.ai.azure.com`) or `AZURE_OPENAI_RESOURCE_NAME`. Supports `*.openai.azure.com`, `*.cognitiveservices.azure.com` and `*.ai.azure.com`; root endpoints auto-normalize to `/openai/v1`. Optional: `AZURE_OPENAI_API_VERSION` (default `v1`), `AZURE_OPENAI_DEPLOYMENT_NAME_MAP`. | | Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` | | DeepSeek | `DEEPSEEK_API_KEY` | | NVIDIA NIM | `NVIDIA_API_KEY` | diff --git a/packages/ai/src/api/azure-openai-responses.ts b/packages/ai/src/api/azure-openai-responses.ts index 8150a82be..137c43c91 100644 --- a/packages/ai/src/api/azure-openai-responses.ts +++ b/packages/ai/src/api/azure-openai-responses.ts @@ -180,12 +180,20 @@ function normalizeAzureBaseUrl(baseUrl: string): string { } const isAzureHost = - url.hostname.endsWith(".openai.azure.com") || url.hostname.endsWith(".cognitiveservices.azure.com"); + url.hostname.endsWith(".openai.azure.com") || + url.hostname.endsWith(".cognitiveservices.azure.com") || + url.hostname.endsWith(".ai.azure.com"); const normalizedPath = url.pathname.replace(/\/+$/, ""); // Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK // can append /deployments//... and ?api-version=v1 correctly. - if (isAzureHost && (normalizedPath === "" || normalizedPath === "/" || normalizedPath === "/openai")) { + if ( + isAzureHost && + (normalizedPath === "" || + normalizedPath === "/" || + normalizedPath === "/openai" || + normalizedPath === "/openai/v1/responses") + ) { url.pathname = "/openai/v1"; url.search = ""; } diff --git a/packages/ai/test/azure-openai-base-url.test.ts b/packages/ai/test/azure-openai-base-url.test.ts index e372168ba..5908ad382 100644 --- a/packages/ai/test/azure-openai-base-url.test.ts +++ b/packages/ai/test/azure-openai-base-url.test.ts @@ -96,6 +96,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://marc-quicktests-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes Microsoft Foundry root endpoints to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://marc-quicktests-resource.ai.azure.com"); + expect(baseURL).toBe("https://marc-quicktests-resource.ai.azure.com/openai/v1"); + }); + it("normalizes Azure OpenAI root endpoints to /openai/v1", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com"); expect(baseURL).toBe("https://my-resource.openai.azure.com/openai/v1"); @@ -111,6 +116,11 @@ describe("azure-openai-responses base URL normalization", () => { expect(baseURL).toBe("https://my-resource.cognitiveservices.azure.com/openai/v1"); }); + it("normalizes /openai/v1/responses to /openai/v1", async () => { + const baseURL = await captureClientBaseUrl("https://my-resource.services.ai.azure.com/openai/v1/responses"); + expect(baseURL).toBe("https://my-resource.services.ai.azure.com/openai/v1"); + }); + it("preserves explicit non-Azure proxy paths", async () => { const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1"); expect(baseURL).toBe("https://my-proxy.example.com/v1"); diff --git a/packages/coding-agent/docs/providers.md b/packages/coding-agent/docs/providers.md index e97eba5d2..46163abf4 100644 --- a/packages/coding-agent/docs/providers.md +++ b/packages/coding-agent/docs/providers.md @@ -156,8 +156,9 @@ OAuth credentials are also stored here after `/login` and managed automatically. ```bash export AZURE_OPENAI_API_KEY=... -export AZURE_OPENAI_BASE_URL=https://your-resource.openai.azure.com +export AZURE_OPENAI_BASE_URL=https://your-resource.ai.azure.com # also supported: https://your-resource.cognitiveservices.azure.com +# also supported: https://your-resource.openai.azure.com # root endpoints are auto-normalized to /openai/v1 # or use resource name instead of base URL export AZURE_OPENAI_RESOURCE_NAME=your-resource