From 768e0f6f6af2d563683fc0a09c2e42578f76ab8d Mon Sep 17 00:00:00 2001 From: BambinoSK Date: Tue, 1 Sep 2026 10:54:35 +0200 Subject: [PATCH 1/2] fix: _isGrokXAI false-positive substring match breaks token usage for domains containing 'x.ai' Fixes #1483 The _isGrokXAI() method used urlHost.includes('x.ai') which matches any domain containing 'x.ai' as a substring (e.g. box.ai, fox.ai, max.ai). This false-positive causes stream_options:{include_usage:true} to be omitted, so the API never returns usage data and the token bar shows 0. Fix: Use exact host match (api.x.ai) or subdomain match (*.x.ai) instead of substring includes. Added tests for false-positive scenarios and valid x.ai domain detection. AI-assisted: developed with Zoo Code/GLM-5.2, reviewed and verified by the contributor. --- .changeset/fix-grok-xai-false-positive.md | 9 ++++ src/api/providers/__tests__/openai.spec.ts | 61 ++++++++++++++++++++++ src/api/providers/openai.ts | 2 +- 3 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 .changeset/fix-grok-xai-false-positive.md diff --git a/.changeset/fix-grok-xai-false-positive.md b/.changeset/fix-grok-xai-false-positive.md new file mode 100644 index 0000000000..1fa6849148 --- /dev/null +++ b/.changeset/fix-grok-xai-false-positive.md @@ -0,0 +1,9 @@ +--- +"zoo-code": patch +--- + +Fix `_isGrokXAI()` false-positive substring match that broke token usage for OpenAI-compatible providers whose domain contains "x.ai" as a substring (e.g. box.ai, fox.ai, max.ai). + +The `_isGrokXAI()` method in `src/api/providers/openai.ts` used `urlHost.includes("x.ai")` which is a substring match. Any domain containing "x.ai" anywhere in its host (e.g. `box.ai`, `fox.ai`, `max.ai`) was falsely identified as a Grok/xAI endpoint. This caused `stream_options: { include_usage: true }` to be omitted from API requests in both `createMessage()` and `handleO3FamilyMessage()`, so the API never returned usage data and the token bar showed 0 — a silent failure with no error message. + +Fixed by using exact host match (`api.x.ai`) or subdomain suffix check (`.x.ai`) instead of substring `includes()`. diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 38550533a5..43e3f72473 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -1052,6 +1052,67 @@ 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) + // @ts-expect-error - accessing private method for testing + 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" }) + // @ts-expect-error - accessing private method for testing + expect(handler._isGrokXAI("https://fox.ai/v1")).toBe(false) + + // @ts-expect-error - accessing private method for testing + 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" }) + // @ts-expect-error - accessing private method for testing + 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" }) + // @ts-expect-error - accessing private method for testing + expect(handler._isGrokXAI("https://custom.x.ai/v1")).toBe(true) + }) + + 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..4756facb7a 100644 --- a/src/api/providers/openai.ts +++ b/src/api/providers/openai.ts @@ -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 { From 5592ad7661b0e82bd719b9596b227511830e491c Mon Sep 17 00:00:00 2001 From: BambinoSK Date: Tue, 1 Sep 2026 13:20:04 +0200 Subject: [PATCH 2/2] Address CodeRabbit review: use URL.hostname, bracket notation, remove changeset --- .changeset/fix-grok-xai-false-positive.md | 9 ----- src/api/providers/__tests__/openai.spec.ts | 47 +++++++++++++++++----- src/api/providers/openai.ts | 2 +- 3 files changed, 37 insertions(+), 21 deletions(-) delete mode 100644 .changeset/fix-grok-xai-false-positive.md diff --git a/.changeset/fix-grok-xai-false-positive.md b/.changeset/fix-grok-xai-false-positive.md deleted file mode 100644 index 1fa6849148..0000000000 --- a/.changeset/fix-grok-xai-false-positive.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"zoo-code": patch ---- - -Fix `_isGrokXAI()` false-positive substring match that broke token usage for OpenAI-compatible providers whose domain contains "x.ai" as a substring (e.g. box.ai, fox.ai, max.ai). - -The `_isGrokXAI()` method in `src/api/providers/openai.ts` used `urlHost.includes("x.ai")` which is a substring match. Any domain containing "x.ai" anywhere in its host (e.g. `box.ai`, `fox.ai`, `max.ai`) was falsely identified as a Grok/xAI endpoint. This caused `stream_options: { include_usage: true }` to be omitted from API requests in both `createMessage()` and `handleO3FamilyMessage()`, so the API never returned usage data and the token bar showed 0 — a silent failure with no error message. - -Fixed by using exact host match (`api.x.ai`) or subdomain suffix check (`.x.ai`) instead of substring `includes()`. diff --git a/src/api/providers/__tests__/openai.spec.ts b/src/api/providers/__tests__/openai.spec.ts index 43e3f72473..ac4788c83e 100644 --- a/src/api/providers/__tests__/openai.spec.ts +++ b/src/api/providers/__tests__/openai.spec.ts @@ -1060,29 +1060,54 @@ describe("OpenAiHandler", () => { openAiModelId: "gpt-4o", } const handler = new OpenAiHandler(nonGrokOptions) - // @ts-expect-error - accessing private method for testing - expect(handler._isGrokXAI(nonGrokOptions.openAiBaseUrl)).toBe(false) + 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" }) - // @ts-expect-error - accessing private method for testing - expect(handler._isGrokXAI("https://fox.ai/v1")).toBe(false) - - // @ts-expect-error - accessing private method for testing - expect(handler._isGrokXAI("https://max.ai/v1")).toBe(false) + 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" }) - // @ts-expect-error - accessing private method for testing - expect(handler._isGrokXAI("https://api.x.ai/v1")).toBe(true) + 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" }) - // @ts-expect-error - accessing private method for testing - expect(handler._isGrokXAI("https://custom.x.ai/v1")).toBe(true) + 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 () => { diff --git a/src/api/providers/openai.ts b/src/api/providers/openai.ts index 4756facb7a..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 "" }