diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 38550533a5..ac4788c83e 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -1052,6 +1052,92 @@ describe("OpenAiHandler", () => { }) }) + describe("Grok xAI false-positive prevention", () => { + it("should NOT detect as Grok xAI when host contains 'x.ai' as a substring but is not x.ai (e.g. box.ai)", () => { + const nonGrokOptions = { + ...mockOptions, + openAiBaseUrl: "https://box.ai/v1", + openAiModelId: "gpt-4o", + } + const handler = new OpenAiHandler(nonGrokOptions) + expect(handler["_isGrokXAI"](nonGrokOptions.openAiBaseUrl)).toBe(false) + }) + + it("should NOT detect as Grok xAI for other domains containing 'x.ai' substring (e.g. fox.ai, max.ai)", () => { + const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://fox.ai/v1" }) + expect(handler["_isGrokXAI"]("https://fox.ai/v1")).toBe(false) + expect(handler["_isGrokXAI"]("https://max.ai/v1")).toBe(false) + }) + + it("should detect as Grok xAI for api.x.ai", () => { + const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://api.x.ai/v1" }) + expect(handler["_isGrokXAI"]("https://api.x.ai/v1")).toBe(true) + }) + + it("should detect as Grok xAI for subdomains of x.ai (e.g. custom.x.ai)", () => { + const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://custom.x.ai/v1" }) + expect(handler["_isGrokXAI"]("https://custom.x.ai/v1")).toBe(true) + }) + + it("should detect as Grok xAI when api.x.ai uses a non-default port", () => { + const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://api.x.ai:8443/v1" }) + expect(handler["_isGrokXAI"]("https://api.x.ai:8443/v1")).toBe(true) + }) + + it("should exclude stream_options when streaming with api.x.ai on a non-default port", async () => { + const portOptions = { + ...mockOptions, + openAiBaseUrl: "https://api.x.ai:8443/v1", + openAiModelId: "grok-1", + } + const handler = new OpenAiHandler(portOptions) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }] + + const stream = handler.createMessage(systemPrompt, messages) + await stream.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: portOptions.openAiModelId, + stream: true, + }), + {}, + ) + + const mockCalls = mockCreate.mock.calls + const lastCall = mockCalls[mockCalls.length - 1] + expect(lastCall[0]).not.toHaveProperty("stream_options") + }) + + it("should include stream_options when using a non-Grok provider whose URL contains 'x.ai' substring", async () => { + const nonGrokOptions = { + ...mockOptions, + openAiBaseUrl: "https://box.ai/v1", + openAiModelId: "gpt-4o", + } + const handler = new OpenAiHandler(nonGrokOptions) + const systemPrompt = "You are a helpful assistant." + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }] + + const stream = handler.createMessage(systemPrompt, messages) + await stream.next() + + expect(mockCreate).toHaveBeenCalledWith( + expect.objectContaining({ + model: nonGrokOptions.openAiModelId, + stream: true, + }), + {}, + ) + + const mockCalls = mockCreate.mock.calls + const lastCall = mockCalls[mockCalls.length - 1] + expect(lastCall[0]).toHaveProperty("stream_options") + expect(lastCall[0].stream_options).toEqual({ include_usage: true }) + }) + }) + describe("O3 Family Models", () => { const o3Options = { ...mockOptions, diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 5588dd37d6..a2c3f95242 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -510,7 +510,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl protected _getUrlHost(baseUrl?: string): string { try { - return new URL(baseUrl ?? "").host + return new URL(baseUrl ?? "").hostname } catch (error) { return "" } @@ -518,7 +518,7 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl private _isGrokXAI(baseUrl?: string): boolean { const urlHost = this._getUrlHost(baseUrl) - return urlHost.includes("x.ai") + return urlHost === "api.x.ai" || urlHost.endsWith(".x.ai") } protected _isAzureAiInference(baseUrl?: string): boolean {