From ed5e848f3e5fffeaba7c0e4046e08928da50849c Mon Sep 17 00:00:00 2001 From: Eric Wright Date: Sun, 22 Jun 2025 18:01:13 -0700 Subject: [PATCH] add: responses api support for azure (#1321) - Use Responses API for Azure provider endpoints - Added a unit test to catch regression on the change from `/chat/completions` to `/responses` - Updated the default AOAI api version from `2025-03-01-preview` to `2025-04-01-preview` to avoid user/400 errors due to missing summary support in the March API version. - Changes have been tested locally on AOAI endpoints --- README.md | 2 +- codex-cli/src/utils/agent/agent-loop.ts | 6 +- codex-cli/src/utils/config.ts | 2 +- .../agent-azure-responses-endpoint.test.ts | 107 ++++++++++++++++++ 4 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 codex-cli/tests/agent-azure-responses-endpoint.test.ts diff --git a/README.md b/README.md index 24f362f7..d06a0dff 100644 --- a/README.md +++ b/README.md @@ -469,7 +469,7 @@ export OPENAI_API_KEY="your-api-key-here" # Azure OpenAI export AZURE_OPENAI_API_KEY="your-azure-api-key-here" -export AZURE_OPENAI_API_VERSION="2025-03-01-preview" (Optional) +export AZURE_OPENAI_API_VERSION="2025-04-01-preview" (Optional) # OpenRouter export OPENROUTER_API_KEY="your-openrouter-key-here" diff --git a/codex-cli/src/utils/agent/agent-loop.ts b/codex-cli/src/utils/agent/agent-loop.ts index cc57239b..8a5adbeb 100644 --- a/codex-cli/src/utils/agent/agent-loop.ts +++ b/codex-cli/src/utils/agent/agent-loop.ts @@ -800,7 +800,8 @@ export class AgentLoop { const responseCall = !this.config.provider || - this.config.provider?.toLowerCase() === "openai" + this.config.provider?.toLowerCase() === "openai" || + this.config.provider?.toLowerCase() === "azure" ? (params: ResponseCreateParams) => this.oai.responses.create(params) : (params: ResponseCreateParams) => @@ -1188,7 +1189,8 @@ export class AgentLoop { const responseCall = !this.config.provider || - this.config.provider?.toLowerCase() === "openai" + this.config.provider?.toLowerCase() === "openai" || + this.config.provider?.toLowerCase() === "azure" ? (params: ResponseCreateParams) => this.oai.responses.create(params) : (params: ResponseCreateParams) => diff --git a/codex-cli/src/utils/config.ts b/codex-cli/src/utils/config.ts index 51761bf6..3fafdb44 100644 --- a/codex-cli/src/utils/config.ts +++ b/codex-cli/src/utils/config.ts @@ -69,7 +69,7 @@ export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || ""; export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || ""; export const AZURE_OPENAI_API_VERSION = - process.env["AZURE_OPENAI_API_VERSION"] || "2025-03-01-preview"; + process.env["AZURE_OPENAI_API_VERSION"] || "2025-04-01-preview"; export const DEFAULT_REASONING_EFFORT = "high"; export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || ""; diff --git a/codex-cli/tests/agent-azure-responses-endpoint.test.ts b/codex-cli/tests/agent-azure-responses-endpoint.test.ts new file mode 100644 index 00000000..aecf5871 --- /dev/null +++ b/codex-cli/tests/agent-azure-responses-endpoint.test.ts @@ -0,0 +1,107 @@ +/** + * tests/agent-azure-responses-endpoint.test.ts + * + * Verifies that AgentLoop calls the `/responses` endpoint when provider is set to Azure. + */ + +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// Fake stream that yields a completed response event +class FakeStream { + async *[Symbol.asyncIterator]() { + yield { + type: "response.completed", + response: { id: "azure_resp", status: "completed", output: [] }, + } as any; + } +} + +let lastCreateParams: any = null; + +vi.mock("openai", () => { + class FakeDefaultClient { + public responses = { + create: async (params: any) => { + lastCreateParams = params; + return new FakeStream(); + }, + }; + } + class FakeAzureClient { + public responses = { + create: async (params: any) => { + lastCreateParams = params; + return new FakeStream(); + }, + }; + } + class APIConnectionTimeoutError extends Error {} + return { + __esModule: true, + default: FakeDefaultClient, + AzureOpenAI: FakeAzureClient, + APIConnectionTimeoutError, + }; +}); + +// Stub approvals to bypass command approval logic +vi.mock("../src/approvals.js", () => ({ + __esModule: true, + alwaysApprovedCommands: new Set(), + canAutoApprove: () => ({ type: "auto-approve", runInSandbox: false }), + isSafeCommand: () => null, +})); + +// Stub format-command to avoid formatting side effects +vi.mock("../src/format-command.js", () => ({ + __esModule: true, + formatCommandForDisplay: (cmd: Array) => cmd.join(" "), +})); + +// Stub internal logging to keep output clean +vi.mock("../src/utils/agent/log.js", () => ({ + __esModule: true, + log: () => {}, + isLoggingEnabled: () => false, +})); + +import { AgentLoop } from "../src/utils/agent/agent-loop.js"; + +describe("AgentLoop Azure provider responses endpoint", () => { + beforeEach(() => { + lastCreateParams = null; + }); + + it("calls the /responses endpoint when provider is azure", async () => { + const cfg: any = { + model: "test-model", + provider: "azure", + instructions: "", + disableResponseStorage: false, + notify: false, + }; + const loop = new AgentLoop({ + additionalWritableRoots: [], + model: cfg.model, + config: cfg, + instructions: cfg.instructions, + approvalPolicy: { mode: "suggest" } as any, + onItem: () => {}, + onLoading: () => {}, + getCommandConfirmation: async () => ({ review: "yes" }) as any, + onLastResponseId: () => {}, + }); + + await loop.run([ + { + type: "message", + role: "user", + content: [{ type: "input_text", text: "hello" }], + }, + ]); + + expect(lastCreateParams).not.toBeNull(); + expect(lastCreateParams.model).toBe(cfg.model); + expect(Array.isArray(lastCreateParams.input)).toBe(true); + }); +});