Merge pull request #6004 from gukoff/support-azure-foundry-endpoints
feat: Normalize modern Microsoft Foundry Responses API endpoints
This commit is contained in:
@@ -382,7 +382,7 @@ Built-in providers resolve these env vars (Node.js; in browsers pass `apiKey` ex
|
|||||||
|----------|------------------------|
|
|----------|------------------------|
|
||||||
| OpenAI | `OPENAI_API_KEY` |
|
| OpenAI | `OPENAI_API_KEY` |
|
||||||
| Ant Ling | `ANT_LING_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` |
|
| Anthropic | `ANTHROPIC_API_KEY` or `ANTHROPIC_OAUTH_TOKEN` |
|
||||||
| DeepSeek | `DEEPSEEK_API_KEY` |
|
| DeepSeek | `DEEPSEEK_API_KEY` |
|
||||||
| NVIDIA NIM | `NVIDIA_API_KEY` |
|
| NVIDIA NIM | `NVIDIA_API_KEY` |
|
||||||
|
|||||||
@@ -180,12 +180,20 @@ function normalizeAzureBaseUrl(baseUrl: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const isAzureHost =
|
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(/\/+$/, "");
|
const normalizedPath = url.pathname.replace(/\/+$/, "");
|
||||||
|
|
||||||
// Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK
|
// Ensure Azure hosts have /openai/v1 as base path so the AzureOpenAI SDK
|
||||||
// can append /deployments/<model>/... and ?api-version=v1 correctly.
|
// can append /deployments/<model>/... 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.pathname = "/openai/v1";
|
||||||
url.search = "";
|
url.search = "";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,6 +96,11 @@ describe("azure-openai-responses base URL normalization", () => {
|
|||||||
expect(baseURL).toBe("https://marc-quicktests-resource.cognitiveservices.azure.com/openai/v1");
|
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 () => {
|
it("normalizes Azure OpenAI root endpoints to /openai/v1", async () => {
|
||||||
const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com");
|
const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com");
|
||||||
expect(baseURL).toBe("https://my-resource.openai.azure.com/openai/v1");
|
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");
|
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 () => {
|
it("preserves explicit non-Azure proxy paths", async () => {
|
||||||
const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1");
|
const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1");
|
||||||
expect(baseURL).toBe("https://my-proxy.example.com/v1");
|
expect(baseURL).toBe("https://my-proxy.example.com/v1");
|
||||||
|
|||||||
@@ -156,8 +156,9 @@ OAuth credentials are also stored here after `/login` and managed automatically.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
export AZURE_OPENAI_API_KEY=...
|
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.cognitiveservices.azure.com
|
||||||
|
# also supported: https://your-resource.openai.azure.com
|
||||||
# root endpoints are auto-normalized to /openai/v1
|
# root endpoints are auto-normalized to /openai/v1
|
||||||
# or use resource name instead of base URL
|
# or use resource name instead of base URL
|
||||||
export AZURE_OPENAI_RESOURCE_NAME=your-resource
|
export AZURE_OPENAI_RESOURCE_NAME=your-resource
|
||||||
|
|||||||
Reference in New Issue
Block a user