diff --git a/core/llm/index.ts b/core/llm/index.ts index 1af44b25614..b3d0515cebd 100644 --- a/core/llm/index.ts +++ b/core/llm/index.ts @@ -1021,9 +1021,18 @@ export abstract class BaseLLM implements ILLM { } private canUseOpenAIResponses(options: CompletionOptions): boolean { + const isOfficialOpenAIAPI = + !this.apiBase || + this.apiBase.trim() === "" || + this.apiBase.replace(/\/$/, "") === "https://api.openai.com/v1"; + + const shouldUseResponses = + this._llmOptions.useResponsesApi === true || + (this._llmOptions.useResponsesApi !== false && isOfficialOpenAIAPI); + return ( this.providerName === "openai" && - this._llmOptions.useResponsesApi !== false && + shouldUseResponses && typeof (this as any)._streamResponses === "function" && (this as any).isOSeriesOrGpt5PlusModel(options.model) ); diff --git a/core/llm/llms/OpenAI.test.ts b/core/llm/llms/OpenAI.test.ts index 943e9436903..4658cf7a8f6 100644 --- a/core/llm/llms/OpenAI.test.ts +++ b/core/llm/llms/OpenAI.test.ts @@ -39,4 +39,51 @@ describe("OpenAI", () => { expect(openai.isOSeriesOrGpt5PlusModel("ao31")).toBeFalsy(); expect(openai.isOSeriesOrGpt5PlusModel("1os")).toBeFalsy(); }); + + describe("canUseOpenAIResponses", () => { + test("should use responses on official OpenAI apiBase for o-series model", () => { + const openai = new OpenAI({ + model: "o3-mini", + apiKey: "test", + }); + expect((openai as any).canUseOpenAIResponses({ model: "o3-mini" })).toBe( + true, + ); + }); + + test("should not use responses on custom apiBase by default", () => { + const openai = new OpenAI({ + model: "o3-mini", + apiKey: "test", + apiBase: "https://custom.openai-proxy.com/v1/", + }); + expect((openai as any).canUseOpenAIResponses({ model: "o3-mini" })).toBe( + false, + ); + }); + + test("should allow forcing responses on custom apiBase when useResponsesApi is true", () => { + const openai = new OpenAI({ + model: "o3-mini", + apiKey: "test", + apiBase: "https://custom.openai-proxy.com/v1/", + useResponsesApi: true, + }); + expect((openai as any).canUseOpenAIResponses({ model: "o3-mini" })).toBe( + true, + ); + }); + + test("should disable responses when useResponsesApi is false", () => { + const openai = new OpenAI({ + model: "o3-mini", + apiKey: "test", + apiBase: "https://api.openai.com/v1", + useResponsesApi: false, + }); + expect((openai as any).canUseOpenAIResponses({ model: "o3-mini" })).toBe( + false, + ); + }); + }); }); diff --git a/packages/openai-adapters/src/apis/OpenAI.ts b/packages/openai-adapters/src/apis/OpenAI.ts index d0f8d30ca3a..2cec799b7dc 100644 --- a/packages/openai-adapters/src/apis/OpenAI.ts +++ b/packages/openai-adapters/src/apis/OpenAI.ts @@ -91,8 +91,17 @@ export class OpenAIApi implements BaseLlmApi { if (this.config.useResponsesApi === false) { return false; } - const isOfficialOpenAIAPI = this.apiBase === "https://api.openai.com/v1/"; - return isOfficialOpenAIAPI && isResponsesModel(model); + if (!isResponsesModel(model)) { + return false; + } + if (this.config.useResponsesApi === true) { + return true; + } + const isOfficialOpenAIAPI = + !this.apiBase || + this.apiBase.trim() === "" || + this.apiBase.replace(/\/$/, "") === "https://api.openai.com/v1"; + return isOfficialOpenAIAPI; } modifyCompletionBody< diff --git a/packages/openai-adapters/src/test/openai-adapter.vitest.ts b/packages/openai-adapters/src/test/openai-adapter.vitest.ts index 6abc49a3af4..ef72646fb38 100644 --- a/packages/openai-adapters/src/test/openai-adapter.vitest.ts +++ b/packages/openai-adapters/src/test/openai-adapter.vitest.ts @@ -1,4 +1,5 @@ -import { describe, vi } from "vitest"; +import { describe, expect, it, vi } from "vitest"; +import { OpenAIApi } from "../apis/OpenAI.js"; import { createAdapterTests } from "./adapter-test-utils.js"; // Mock the fetch package (not needed for OpenAI but required by the shared test utils) @@ -25,4 +26,49 @@ describe("OpenAI Adapter Tests", () => { accept: "application/json", }, }); + + describe("shouldUseResponsesEndpoint", () => { + it("should use responses endpoint on official apiBase for responses models", () => { + const api = new OpenAIApi({ + provider: "openai", + apiKey: "test", + apiBase: "https://api.openai.com/v1/", + }); + expect((api as any).shouldUseResponsesEndpoint("gpt-5")).toBe(true); + expect((api as any).shouldUseResponsesEndpoint("o3-mini")).toBe(true); + expect((api as any).shouldUseResponsesEndpoint("gpt-4o")).toBe(false); + }); + + it("should not use responses endpoint on custom apiBase by default", () => { + const api = new OpenAIApi({ + provider: "openai", + apiKey: "test", + apiBase: "https://custom-openai-proxy.com/v1/", + }); + expect((api as any).shouldUseResponsesEndpoint("gpt-5")).toBe(false); + expect((api as any).shouldUseResponsesEndpoint("o3-mini")).toBe(false); + }); + + it("should use responses endpoint on custom apiBase if useResponsesApi is true", () => { + const api = new OpenAIApi({ + provider: "openai", + apiKey: "test", + apiBase: "https://custom-openai-proxy.com/v1/", + useResponsesApi: true, + }); + expect((api as any).shouldUseResponsesEndpoint("gpt-5")).toBe(true); + expect((api as any).shouldUseResponsesEndpoint("o3-mini")).toBe(true); + expect((api as any).shouldUseResponsesEndpoint("gpt-4o")).toBe(false); + }); + + it("should not use responses endpoint when useResponsesApi is false", () => { + const api = new OpenAIApi({ + provider: "openai", + apiKey: "test", + apiBase: "https://api.openai.com/v1/", + useResponsesApi: false, + }); + expect((api as any).shouldUseResponsesEndpoint("gpt-5")).toBe(false); + }); + }); });