Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/funny-rivers-protect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
"ai-gateway-provider": patch
"workers-ai-provider": patch
---

Support Azure OpenAI v1 Responses API URLs when routing wrapped models through AI Gateway.

`createAzure(...).responses(...)` with `useDeploymentBasedUrls: false` generates `https://{resource}.openai.azure.com/openai/v1/responses?...` URLs, which the shared provider matcher did not recognize, so wrapped Azure responses models failed with `provider "azure.responses" is currently not supported`. These URLs now route through the `azure-openai` gateway provider as `{resource}/openai/responses...` (Azure's non-deployment Responses route). Deployment-based Azure routing is unchanged, and the match is deliberately scoped to `responses` — other v1 paths (chat, embeddings) have no non-deployment Azure route to map to. Applies to both `ai-gateway-provider`'s wrapped-model routing and `workers-ai-provider`'s `createGatewayProvider` URL detection, which share the matcher.
14 changes: 14 additions & 0 deletions packages/ai-gateway-provider/test/endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@ const testCases = [
name: "azure-openai",
url: "https://myresource.openai.azure.com/openai/deployments/mydeployment/chat/completions?api-version=2024-02-15-preview",
},
{
expected: "myresource/openai/responses?api-version=2024-02-15-preview",
name: "azure-openai",
url: "https://myresource.openai.azure.com/openai/v1/responses?api-version=2024-02-15-preview",
},
{
expected: "v1/chat/completions",
name: "openrouter",
Expand All @@ -73,6 +78,15 @@ describe("ProvidersConfigs endpoint parsing", () => {
expect(result).toBe(testCase.expected);
});
}

// Azure's v1 surface is only routable for the Responses API (the sole path
// with a non-deployment Azure route); other v1 paths must stay unmatched
// rather than route to an endpoint that 404s at Azure.
it("does not match Azure v1 paths other than responses", () => {
const url =
"https://myresource.openai.azure.com/openai/v1/chat/completions?api-version=2024-02-15-preview";
expect(providers.find((p) => p.regex.test(url))).toBeUndefined();
});
});

describe("Provider auth header selection", () => {
Expand Down
41 changes: 41 additions & 0 deletions packages/ai-gateway-provider/test/request-shaping.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { http, HttpResponse } from "msw";
import { setupServer } from "msw/node";
import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest";
import { CF_TEMP_TOKEN } from "../src/auth";
import { createAzure } from "../src/providers/azure";
import { createOpenAI } from "../src/providers/openai";
import { createAiGateway } from "../src";

Expand Down Expand Up @@ -113,6 +114,46 @@ describe("REST request shaping", () => {
expect(captured.headers["cf-aig-byok-alias"]).toBe("alias-1");
expect(captured.headers["cf-aig-zdr"]).toBe("true");
});

it("routes Azure responses API v1 URLs through the azure-openai provider entry", async () => {
server.use(
http.post(GATEWAY_URL, async ({ request }) => {
captured.headers = Object.fromEntries(request.headers);
captured.body = await request.json();
return HttpResponse.json(openAiResponse());
}),
);

const aigateway = createAiGateway({
accountId: TEST_ACCOUNT_ID,
apiKey: TEST_API_KEY,
gateway: TEST_GATEWAY,
});
const azure = createAzure({
resourceName: "myresource",
apiKey: "azure-api-key",
apiVersion: "2024-02-15-preview",
useDeploymentBasedUrls: false,
});

await generateText({
model: aigateway(azure.responses("gpt-5.1")),
prompt: "hi",
maxRetries: 0,
});

const body = captured.body as Array<{
provider: string;
endpoint: string;
query: { model?: string };
}>;
expect(body).toHaveLength(1);
expect(body[0]?.provider).toBe("azure-openai");
expect(body[0]?.endpoint).toBe(
"myresource/openai/responses?api-version=2024-02-15-preview",
);
expect(body[0]?.query.model).toBe("gpt-5.1");
});
});

describe("Binding request shaping", () => {
Expand Down
25 changes: 19 additions & 6 deletions packages/gateway-core/src/gateway-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,16 +134,29 @@ function bedrockTransform(url: string): string {
return `bedrock-runtime/${region}/${rest}`;
}

// Azure's URL carries the resource + deployment, so it needs a bespoke transform
// (mirrors ai-gateway-provider). Only used for bring-your-own-provider detection.
// Azure OpenAI supports both deployment-based URLs
// (`/openai/deployments/{deployment}/...`) and the v1 surface
// (`/openai/v1/...`). AI Gateway expects the former as
// `{resource}/{deployment}/{rest}`. On the v1 surface only the Responses API is
// routable: Azure exposes a non-deployment `/openai/responses` route, so the
// gateway endpoint is `{resource}/openai/responses...`. Other v1 paths (chat,
// embeddings) have no such non-deployment route and stay unmatched rather than
// being routed to an endpoint that 404s at Azure.
// Only used for bring-your-own-provider detection.
const AZURE_HOST =
/^https:\/\/(?<resource>[^.]+)\.openai\.azure\.com\/openai\/deployments\/(?<deployment>[^/]+)\/(?<rest>.*)$/;
/^https:\/\/(?<resource>[^.]+)\.openai\.azure\.com\/openai\/(?:(?:deployments\/(?<deployment>[^/]+)\/(?<rest>.*))|(?:v1\/(?<v1rest>responses.*)))$/;
function azureTransform(url: string): string {
const m = url.match(AZURE_HOST);
if (!m?.groups) return url;
const { resource, deployment, rest } = m.groups;
if (!resource || !deployment || !rest) return url;
return `${resource}/${deployment}/${rest}`;
const { resource, deployment, rest, v1rest } = m.groups;
if (!resource) return url;
if (deployment && rest) {
return `${resource}/${deployment}/${rest}`;
}
if (v1rest) {
return `${resource}/openai/${v1rest}`;
}
return url;
}

/**
Expand Down