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
53 changes: 27 additions & 26 deletions src/api/providers/__tests__/openai-native-tools.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,27 @@ import OpenAI from "openai"

import { OpenAiHandler } from "../openai"

vi.mock("@ai-sdk/openai-compatible", () => ({
createOpenAICompatible: vi.fn(() => vi.fn((modelId: string) => ({ modelId, provider: "openai-compatible" }))),
}))

vi.mock("@ai-sdk/azure", () => ({
createAzure: vi.fn(() => ({
chat: vi.fn((modelId: string) => ({ modelId, provider: "azure.chat" })),
})),
}))

describe("OpenAiHandler native tools", () => {
it("includes tools in request when tools are provided via metadata (regression test)", async () => {
const mockCreate = vi.fn().mockImplementationOnce(() => ({
[Symbol.asyncIterator]: async function* () {
yield {
choices: [{ delta: { content: "Test response" } }],
}
},
}))
async function* mockFullStream() {
yield { type: "text-delta", text: "Test response" }
}

mockStreamText.mockReturnValueOnce({
fullStream: mockFullStream(),
usage: Promise.resolve({ inputTokens: 10, outputTokens: 5 }),
providerMetadata: Promise.resolve(undefined),
})

// Set openAiCustomModelInfo without any tool capability flags; tools should
// still be passed whenever metadata.tools is present.
Expand All @@ -26,16 +38,6 @@ describe("OpenAiHandler native tools", () => {
},
} as unknown as import("../../../shared/api").ApiHandlerOptions)

// Patch the OpenAI client call
const mockClient = {
chat: {
completions: {
create: mockCreate,
},
},
} as unknown as OpenAI
;(handler as unknown as { client: OpenAI }).client = mockClient

const tools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
Expand All @@ -53,17 +55,12 @@ describe("OpenAiHandler native tools", () => {
})
await stream.next()

expect(mockCreate).toHaveBeenCalledWith(
expect(mockStreamText).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.arrayContaining([
expect.objectContaining({
type: "function",
function: expect.objectContaining({ name: "test_tool" }),
}),
]),
parallel_tool_calls: true,
tools: expect.objectContaining({
test_tool: expect.anything(),
}),
}),
expect.anything(),
)
})
})
Expand Down Expand Up @@ -92,6 +89,10 @@ vi.mock("@ai-sdk/openai", () => ({
modelId: "gpt-4o",
provider: "openai.responses",
}))
;(provider as any).chat = vi.fn((modelId: string) => ({
modelId,
provider: "openai.chat",
}))
return provider
}),
}))
Expand Down
106 changes: 44 additions & 62 deletions src/api/providers/__tests__/openai-timeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,51 +3,34 @@
import { OpenAiHandler } from "../openai"
import { ApiHandlerOptions } from "../../../shared/api"

// Mock the timeout config utility
vitest.mock("../utils/timeout-config", () => ({
getApiRequestTimeout: vitest.fn(),
const mockCreateOpenAI = vi.hoisted(() => vi.fn())
const mockCreateOpenAICompatible = vi.hoisted(() => vi.fn())
const mockCreateAzure = vi.hoisted(() => vi.fn())

vi.mock("@ai-sdk/openai", () => ({
createOpenAI: mockCreateOpenAI.mockImplementation(() => ({
chat: vi.fn(() => ({ modelId: "test", provider: "openai.chat" })),
})),
}))

import { getApiRequestTimeout } from "../utils/timeout-config"

// Mock OpenAI and AzureOpenAI
const mockOpenAIConstructor = vitest.fn()
const mockAzureOpenAIConstructor = vitest.fn()

vitest.mock("openai", () => {
return {
__esModule: true,
default: vitest.fn().mockImplementation((config) => {
mockOpenAIConstructor(config)
return {
chat: {
completions: {
create: vitest.fn(),
},
},
}
}),
AzureOpenAI: vitest.fn().mockImplementation((config) => {
mockAzureOpenAIConstructor(config)
return {
chat: {
completions: {
create: vitest.fn(),
},
},
}
}),
}
})
vi.mock("@ai-sdk/openai-compatible", () => ({
createOpenAICompatible: mockCreateOpenAICompatible.mockImplementation(() =>
vi.fn((modelId: string) => ({ modelId, provider: "openai-compatible" })),
),
}))

vi.mock("@ai-sdk/azure", () => ({
createAzure: mockCreateAzure.mockImplementation(() => ({
chat: vi.fn((modelId: string) => ({ modelId, provider: "azure.chat" })),
})),
}))

describe("OpenAiHandler timeout configuration", () => {
describe("OpenAiHandler provider configuration", () => {
beforeEach(() => {
vitest.clearAllMocks()
vi.clearAllMocks()
})

it("should use default timeout for standard OpenAI", () => {
;(getApiRequestTimeout as any).mockReturnValue(600000)

it("should use createOpenAI for standard OpenAI endpoints", () => {
const options: ApiHandlerOptions = {
apiModelId: "gpt-4",
openAiModelId: "gpt-4",
Expand All @@ -56,19 +39,15 @@ describe("OpenAiHandler timeout configuration", () => {

new OpenAiHandler(options)

expect(getApiRequestTimeout).toHaveBeenCalled()
expect(mockOpenAIConstructor).toHaveBeenCalledWith(
expect(mockCreateOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "https://api.openai.com/v1",
apiKey: "test-key",
timeout: 600000, // 600 seconds in milliseconds
}),
)
})

it("should use custom timeout for OpenAI-compatible providers", () => {
;(getApiRequestTimeout as any).mockReturnValue(1800000) // 30 minutes

it("should use createOpenAI for custom OpenAI-compatible providers", () => {
const options: ApiHandlerOptions = {
apiModelId: "custom-model",
openAiModelId: "custom-model",
Expand All @@ -78,17 +57,14 @@ describe("OpenAiHandler timeout configuration", () => {

new OpenAiHandler(options)

expect(mockOpenAIConstructor).toHaveBeenCalledWith(
expect(mockCreateOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
baseURL: "http://localhost:8080/v1",
timeout: 1800000, // 1800 seconds in milliseconds
}),
)
})

it("should use timeout for Azure OpenAI", () => {
;(getApiRequestTimeout as any).mockReturnValue(900000) // 15 minutes

it("should use createAzure for Azure OpenAI", () => {
const options: ApiHandlerOptions = {
apiModelId: "gpt-4",
openAiModelId: "gpt-4",
Expand All @@ -99,16 +75,16 @@ describe("OpenAiHandler timeout configuration", () => {

new OpenAiHandler(options)

expect(mockAzureOpenAIConstructor).toHaveBeenCalledWith(
expect(mockCreateAzure).toHaveBeenCalledWith(
expect.objectContaining({
timeout: 900000, // 900 seconds in milliseconds
baseURL: "https://myinstance.openai.azure.com/openai",
apiKey: "test-key",
useDeploymentBasedUrls: true,
}),
)
})

it("should use timeout for Azure AI Inference", () => {
;(getApiRequestTimeout as any).mockReturnValue(1200000) // 20 minutes

it("should use createOpenAICompatible for Azure AI Inference", () => {
const options: ApiHandlerOptions = {
apiModelId: "deepseek",
openAiModelId: "deepseek",
Expand All @@ -118,26 +94,32 @@ describe("OpenAiHandler timeout configuration", () => {

new OpenAiHandler(options)

expect(mockOpenAIConstructor).toHaveBeenCalledWith(
expect(mockCreateOpenAICompatible).toHaveBeenCalledWith(
expect.objectContaining({
timeout: 1200000, // 1200 seconds in milliseconds
baseURL: "https://myinstance.services.ai.azure.com/models",
apiKey: "test-key",
queryParams: expect.objectContaining({
"api-version": expect.any(String),
}),
}),
)
})

it("should handle zero timeout (no timeout)", () => {
;(getApiRequestTimeout as any).mockReturnValue(0)

it("should include custom headers in provider configuration", () => {
const options: ApiHandlerOptions = {
apiModelId: "gpt-4",
openAiModelId: "gpt-4",
openAiApiKey: "test-key",
openAiHeaders: { "X-Custom": "value" },
}

new OpenAiHandler(options)

expect(mockOpenAIConstructor).toHaveBeenCalledWith(
expect(mockCreateOpenAI).toHaveBeenCalledWith(
expect.objectContaining({
timeout: 0, // No timeout
headers: expect.objectContaining({
"X-Custom": "value",
}),
}),
)
})
Expand Down
Loading
Loading