Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 105 additions & 0 deletions src/api/providers/__tests__/openai.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -1415,6 +1501,25 @@ describe("OpenAiHandler", () => {
{ path: "/models/chat/completions" },
)
})

it("should exclude stream_options when O3 model uses Grok xAI base URL", async () => {
const handler = new OpenAiHandler({ ...o3Options, openAiBaseUrl: "https://api.x.ai/v1" })
const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello!" }])
await stream.next()

const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]
expect(lastCall[0]).not.toHaveProperty("stream_options")
})

it("should include stream_options when O3 model uses non-Grok URL containing 'x.ai' substring", async () => {
const handler = new OpenAiHandler({ ...o3Options, openAiBaseUrl: "https://box.ai/v1" })
const stream = handler.createMessage("You are a helpful assistant.", [{ role: "user", content: "Hello!" }])
await stream.next()

const lastCall = mockCreate.mock.calls[mockCreate.mock.calls.length - 1]
expect(lastCall[0]).toHaveProperty("stream_options")
expect(lastCall[0].stream_options).toEqual({ include_usage: true })
})
})
})

Expand Down
4 changes: 2 additions & 2 deletions src/api/providers/openai.ts
Original file line number Diff line number Diff line change
Expand Up @@ -510,19 +510,19 @@

protected _getUrlHost(baseUrl?: string): string {
try {
return new URL(baseUrl ?? "").host
return new URL(baseUrl ?? "").hostname
} catch (error) {
return ""
}
}

private _isGrokXAI(baseUrl?: string): boolean {
const urlHost = this._getUrlHost(baseUrl)
return urlHost.includes("x.ai")
return urlHost === "api.x.ai" || urlHost.endsWith(".x.ai")
Comment thread
BambinoSK marked this conversation as resolved.
}

protected _isAzureAiInference(baseUrl?: string): boolean {
return isAzureAiInferenceBaseUrl(baseUrl)

Check warning on line 525 in src/api/providers/openai.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/openai.ts:525: Survived StringLiteral mutant (replacement: "Stryker was here!"). See the job summary for the complete list and resolution guidance.
}

/**
Expand All @@ -530,7 +530,7 @@
* Note: max_tokens is deprecated in favor of max_completion_tokens as per OpenAI documentation
* O3 family models handle max_tokens separately in handleO3FamilyMessage
*/
protected addMaxTokensIfNeeded(

Check warning on line 533 in src/api/providers/openai.ts

View workflow job for this annotation

GitHub Actions / mutation-diff

Mutation test advisory

src/api/providers/openai.ts:533: Survived ConditionalExpression mutant (replacement: false). See the job summary for the complete list and resolution guidance.
requestOptions:
| OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
| OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming,
Expand Down
Loading