import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { getModel } from "../src/models.js"; import { streamAzureOpenAIResponses } from "../src/providers/azure-openai-responses.js"; import type { Context } from "../src/types.js"; interface CapturedAzureClientOptions { apiKey: string; apiVersion: string; dangerouslyAllowBrowser: boolean; defaultHeaders?: Record; baseURL: string; } const azureMock = vi.hoisted(() => ({ constructorCalls: [] as CapturedAzureClientOptions[], })); vi.mock("openai", () => { class AzureOpenAI { responses = { create: () => { throw new Error("mock create"); }, }; constructor(config: CapturedAzureClientOptions) { azureMock.constructorCalls.push(config); } } return { AzureOpenAI }; }); const context: Context = { messages: [{ role: "user", content: "hello", timestamp: Date.now() }], }; const originalAzureOpenAIBaseUrl = process.env.AZURE_OPENAI_BASE_URL; const originalAzureOpenAIResourceName = process.env.AZURE_OPENAI_RESOURCE_NAME; const originalAzureOpenAIApiVersion = process.env.AZURE_OPENAI_API_VERSION; const originalAzureOpenAIApiKey = process.env.AZURE_OPENAI_API_KEY; beforeEach(() => { azureMock.constructorCalls.length = 0; delete process.env.AZURE_OPENAI_BASE_URL; delete process.env.AZURE_OPENAI_RESOURCE_NAME; delete process.env.AZURE_OPENAI_API_VERSION; delete process.env.AZURE_OPENAI_API_KEY; }); afterEach(() => { if (originalAzureOpenAIBaseUrl === undefined) { delete process.env.AZURE_OPENAI_BASE_URL; } else { process.env.AZURE_OPENAI_BASE_URL = originalAzureOpenAIBaseUrl; } if (originalAzureOpenAIResourceName === undefined) { delete process.env.AZURE_OPENAI_RESOURCE_NAME; } else { process.env.AZURE_OPENAI_RESOURCE_NAME = originalAzureOpenAIResourceName; } if (originalAzureOpenAIApiVersion === undefined) { delete process.env.AZURE_OPENAI_API_VERSION; } else { process.env.AZURE_OPENAI_API_VERSION = originalAzureOpenAIApiVersion; } if (originalAzureOpenAIApiKey === undefined) { delete process.env.AZURE_OPENAI_API_KEY; } else { process.env.AZURE_OPENAI_API_KEY = originalAzureOpenAIApiKey; } }); async function captureClientBaseUrl(baseUrl: string): Promise { process.env.AZURE_OPENAI_BASE_URL = baseUrl; const model = getModel("azure-openai-responses", "gpt-4o-mini"); await streamAzureOpenAIResponses(model, context, { apiKey: "test-api-key" }).result(); expect(azureMock.constructorCalls).toHaveLength(1); return azureMock.constructorCalls[0].baseURL; } describe("azure-openai-responses base URL normalization", () => { it("normalizes Cognitive Services root endpoints to /openai/v1", async () => { const baseURL = await captureClientBaseUrl("https://marc-quicktests-resource.cognitiveservices.azure.com"); expect(baseURL).toBe("https://marc-quicktests-resource.cognitiveservices.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"); }); it("normalizes /openai to /openai/v1", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.cognitiveservices.azure.com/openai"); expect(baseURL).toBe("https://my-resource.cognitiveservices.azure.com/openai/v1"); }); it("preserves /openai/v1 endpoints", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.cognitiveservices.azure.com/openai/v1"); expect(baseURL).toBe("https://my-resource.cognitiveservices.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"); }); it("strips query params when normalizing Azure host URLs", async () => { const baseURL = await captureClientBaseUrl("https://my-resource.openai.azure.com/openai?api-version=2024-12-01"); expect(baseURL).toBe("https://my-resource.openai.azure.com/openai/v1"); }); it("preserves query params on non-Azure proxy URLs", async () => { const baseURL = await captureClientBaseUrl("https://my-proxy.example.com/v1?custom=true"); expect(baseURL).toBe("https://my-proxy.example.com/v1?custom=true"); }); it("throws on invalid URLs", async () => { process.env.AZURE_OPENAI_BASE_URL = "not-a-url"; const model = getModel("azure-openai-responses", "gpt-4o-mini"); const result = await streamAzureOpenAIResponses(model, context, { apiKey: "test-api-key" }).result(); expect(result.stopReason).toBe("error"); expect(result.errorMessage).toContain("Invalid Azure OpenAI base URL"); }); it("builds correct default URL from AZURE_OPENAI_RESOURCE_NAME", async () => { process.env.AZURE_OPENAI_RESOURCE_NAME = "my-resource"; const model = getModel("azure-openai-responses", "gpt-4o-mini"); await streamAzureOpenAIResponses(model, context, { apiKey: "test-api-key" }).result(); expect(azureMock.constructorCalls).toHaveLength(1); expect(azureMock.constructorCalls[0].baseURL).toBe("https://my-resource.openai.azure.com/openai/v1"); }); });