Skip to content

Commit 77e422f

Browse files
BambinoSKedelauna
andauthored
fix: _isGrokXAI() false-positive substring match breaks token usage for domains containing "x.ai" (#1484)
* 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. * Address CodeRabbit review: use URL.hostname, bracket notation, remove changeset * test: add O3+Grok stream_options coverage for handleO3FamilyMessage --------- Co-authored-by: Elliott de Launay <edelauna@gmail.com>
1 parent 10b45ab commit 77e422f

2 files changed

Lines changed: 107 additions & 2 deletions

File tree

‎src/api/providers/__tests__/openai.spec.ts‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,92 @@ describe("OpenAiHandler", () => {
12521252
})
12531253
})
12541254

1255+
describe("Grok xAI false-positive prevention", () => {
1256+
it("should NOT detect as Grok xAI when host contains 'x.ai' as a substring but is not x.ai (e.g. box.ai)", () => {
1257+
const nonGrokOptions = {
1258+
...mockOptions,
1259+
openAiBaseUrl: "https://box.ai/v1",
1260+
openAiModelId: "gpt-4o",
1261+
}
1262+
const handler = new OpenAiHandler(nonGrokOptions)
1263+
expect(handler["_isGrokXAI"](nonGrokOptions.openAiBaseUrl)).toBe(false)
1264+
})
1265+
1266+
it("should NOT detect as Grok xAI for other domains containing 'x.ai' substring (e.g. fox.ai, max.ai)", () => {
1267+
const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://fox.ai/v1" })
1268+
expect(handler["_isGrokXAI"]("https://fox.ai/v1")).toBe(false)
1269+
expect(handler["_isGrokXAI"]("https://max.ai/v1")).toBe(false)
1270+
})
1271+
1272+
it("should detect as Grok xAI for api.x.ai", () => {
1273+
const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://api.x.ai/v1" })
1274+
expect(handler["_isGrokXAI"]("https://api.x.ai/v1")).toBe(true)
1275+
})
1276+
1277+
it("should detect as Grok xAI for subdomains of x.ai (e.g. custom.x.ai)", () => {
1278+
const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://custom.x.ai/v1" })
1279+
expect(handler["_isGrokXAI"]("https://custom.x.ai/v1")).toBe(true)
1280+
})
1281+
1282+
it("should detect as Grok xAI when api.x.ai uses a non-default port", () => {
1283+
const handler = new OpenAiHandler({ ...mockOptions, openAiBaseUrl: "https://api.x.ai:8443/v1" })
1284+
expect(handler["_isGrokXAI"]("https://api.x.ai:8443/v1")).toBe(true)
1285+
})
1286+
1287+
it("should exclude stream_options when streaming with api.x.ai on a non-default port", async () => {
1288+
const portOptions = {
1289+
...mockOptions,
1290+
openAiBaseUrl: "https://api.x.ai:8443/v1",
1291+
openAiModelId: "grok-1",
1292+
}
1293+
const handler = new OpenAiHandler(portOptions)
1294+
const systemPrompt = "You are a helpful assistant."
1295+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1296+
1297+
const stream = handler.createMessage(systemPrompt, messages)
1298+
await stream.next()
1299+
1300+
expect(mockCreate).toHaveBeenCalledWith(
1301+
expect.objectContaining({
1302+
model: portOptions.openAiModelId,
1303+
stream: true,
1304+
}),
1305+
{},
1306+
)
1307+
1308+
const mockCalls = mockCreate.mock.calls
1309+
const lastCall = mockCalls[mockCalls.length - 1]
1310+
expect(lastCall[0]).not.toHaveProperty("stream_options")
1311+
})
1312+
1313+
it("should include stream_options when using a non-Grok provider whose URL contains 'x.ai' substring", async () => {
1314+
const nonGrokOptions = {
1315+
...mockOptions,
1316+
openAiBaseUrl: "https://box.ai/v1",
1317+
openAiModelId: "gpt-4o",
1318+
}
1319+
const handler = new OpenAiHandler(nonGrokOptions)
1320+
const systemPrompt = "You are a helpful assistant."
1321+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Hello!" }]
1322+
1323+
const stream = handler.createMessage(systemPrompt, messages)
1324+
await stream.next()
1325+
1326+
expect(mockCreate).toHaveBeenCalledWith(
1327+
expect.objectContaining({
1328+
model: nonGrokOptions.openAiModelId,
1329+
stream: true,
1330+
}),
1331+
{},
1332+
)
1333+
1334+
const mockCalls = mockCreate.mock.calls
1335+
const lastCall = mockCalls[mockCalls.length - 1]
1336+
expect(lastCall[0]).toHaveProperty("stream_options")
1337+
expect(lastCall[0].stream_options).toEqual({ include_usage: true })
1338+
})
1339+
})
1340+
12551341
describe("O3 Family Models", () => {
12561342
const o3Options = {
12571343
...mockOptions,
@@ -1630,6 +1716,25 @@ describe("OpenAiHandler", () => {
16301716
{ path: "/models/chat/completions" },
16311717
)
16321718
})
1719+
1720+
it("should exclude stream_options when O3 model uses Grok xAI base URL", async () => {
1721+
const handler = new OpenAiHandler({ ...o3Options, openAiBaseUrl: "https://api.x.ai/v1" })
1722+
const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello!" }])
1723+
await stream.next()
1724+
1725+
const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]
1726+
expect(lastCall[0]).not.toHaveProperty("stream_options")
1727+
})
1728+
1729+
it("should include stream_options when O3 model uses non-Grok URL containing 'x.ai' substring", async () => {
1730+
const handler = new OpenAiHandler({ ...o3Options, openAiBaseUrl: "https://box.ai/v1" })
1731+
const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello!" }])
1732+
await stream.next()
1733+
1734+
const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]
1735+
expect(lastCall[0]).toHaveProperty("stream_options")
1736+
expect(lastCall[0].stream_options).toEqual({ include_usage: true })
1737+
})
16331738
})
16341739
})
16351740

‎src/api/providers/openai.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -522,15 +522,15 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
522522

523523
protected _getUrlHost(baseUrl?: string): string {
524524
try {
525-
return new URL(baseUrl ?? "").host
525+
return new URL(baseUrl ?? "").hostname
526526
} catch (error) {
527527
return ""
528528
}
529529
}
530530

531531
private _isGrokXAI(baseUrl?: string): boolean {
532532
const urlHost = this._getUrlHost(baseUrl)
533-
return urlHost.includes("x.ai")
533+
return urlHost === "api.x.ai" || urlHost.endsWith(".x.ai")
534534
}
535535

536536
protected _isAzureAiInference(baseUrl?: string): boolean {

0 commit comments

Comments
 (0)