Skip to content

Commit 0d38b54

Browse files
committed
fix: address PR review comments for Nebius provider
- Fix typo in Nebius component (was using litellmApiKey) - Fix inconsistent default base URL between UI and API handler - Add Zod validation for Nebius API responses - Add comprehensive unit tests for Nebius provider and fetcher - Improve error handling with schema validation
1 parent da46718 commit 0d38b54

File tree

4 files changed

+488
-27
lines changed

4 files changed

+488
-27
lines changed
Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
// npx jest src/api/providers/__tests__/nebius.test.ts
2+
3+
import { Anthropic } from "@anthropic-ai/sdk"
4+
import OpenAI from "openai"
5+
6+
import { NebiusHandler } from "../nebius"
7+
import { ApiHandlerOptions } from "../../../shared/api"
8+
9+
// Mock dependencies
10+
jest.mock("openai")
11+
jest.mock("delay", () => jest.fn(() => Promise.resolve()))
12+
jest.mock("../fetchers/modelCache", () => ({
13+
getModels: jest.fn().mockImplementation(() => {
14+
return Promise.resolve({
15+
"Qwen/Qwen2.5-32B-Instruct-fast": {
16+
maxTokens: 8192,
17+
contextWindow: 32768,
18+
supportsImages: false,
19+
supportsPromptCache: false,
20+
inputPrice: 0.13,
21+
outputPrice: 0.4,
22+
description: "Qwen 2.5 32B Instruct Fast",
23+
},
24+
"deepseek-ai/DeepSeek-R1": {
25+
maxTokens: 32000,
26+
contextWindow: 96000,
27+
supportsImages: false,
28+
supportsPromptCache: false,
29+
inputPrice: 0.8,
30+
outputPrice: 2.4,
31+
description: "DeepSeek R1",
32+
},
33+
})
34+
}),
35+
}))
36+
37+
describe("NebiusHandler", () => {
38+
const mockOptions: ApiHandlerOptions = {
39+
nebiusApiKey: "test-key",
40+
nebiusModelId: "Qwen/Qwen2.5-32B-Instruct-fast",
41+
nebiusBaseUrl: "https://api.studio.nebius.ai/v1",
42+
}
43+
44+
beforeEach(() => jest.clearAllMocks())
45+
46+
it("initializes with correct options", () => {
47+
const handler = new NebiusHandler(mockOptions)
48+
expect(handler).toBeInstanceOf(NebiusHandler)
49+
50+
expect(OpenAI).toHaveBeenCalledWith({
51+
baseURL: "https://api.studio.nebius.ai/v1",
52+
apiKey: mockOptions.nebiusApiKey,
53+
})
54+
})
55+
56+
it("uses default base URL when not provided", () => {
57+
const handler = new NebiusHandler({
58+
nebiusApiKey: "test-key",
59+
nebiusModelId: "Qwen/Qwen2.5-32B-Instruct-fast",
60+
})
61+
expect(handler).toBeInstanceOf(NebiusHandler)
62+
63+
expect(OpenAI).toHaveBeenCalledWith({
64+
baseURL: "https://api.studio.nebius.ai/v1",
65+
apiKey: "test-key",
66+
})
67+
})
68+
69+
describe("fetchModel", () => {
70+
it("returns correct model info when options are provided", async () => {
71+
const handler = new NebiusHandler(mockOptions)
72+
const result = await handler.fetchModel()
73+
74+
expect(result).toMatchObject({
75+
id: mockOptions.nebiusModelId,
76+
info: {
77+
maxTokens: 8192,
78+
contextWindow: 32768,
79+
supportsImages: false,
80+
supportsPromptCache: false,
81+
inputPrice: 0.13,
82+
outputPrice: 0.4,
83+
description: "Qwen 2.5 32B Instruct Fast",
84+
},
85+
})
86+
})
87+
88+
it("returns default model info when options are not provided", async () => {
89+
const handler = new NebiusHandler({})
90+
const result = await handler.fetchModel()
91+
expect(result.id).toBe("Qwen/Qwen2.5-32B-Instruct-fast")
92+
})
93+
})
94+
95+
describe("createMessage", () => {
96+
it("generates correct stream chunks", async () => {
97+
const handler = new NebiusHandler(mockOptions)
98+
99+
const mockStream = {
100+
async *[Symbol.asyncIterator]() {
101+
yield {
102+
choices: [{ delta: { content: "test response" } }],
103+
}
104+
yield {
105+
choices: [{ delta: {} }],
106+
usage: { prompt_tokens: 10, completion_tokens: 20 },
107+
}
108+
},
109+
}
110+
111+
// Mock OpenAI chat.completions.create
112+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
113+
114+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
115+
completions: { create: mockCreate },
116+
} as any
117+
118+
const systemPrompt = "test system prompt"
119+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
120+
121+
const generator = handler.createMessage(systemPrompt, messages)
122+
const chunks = []
123+
124+
for await (const chunk of generator) {
125+
chunks.push(chunk)
126+
}
127+
128+
// Verify stream chunks
129+
expect(chunks).toHaveLength(2) // One text chunk and one usage chunk
130+
expect(chunks[0]).toEqual({ type: "text", text: "test response" })
131+
expect(chunks[1]).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 })
132+
133+
// Verify OpenAI client was called with correct parameters
134+
expect(mockCreate).toHaveBeenCalledWith(
135+
expect.objectContaining({
136+
model: "Qwen/Qwen2.5-32B-Instruct-fast",
137+
messages: [
138+
{ role: "system", content: "test system prompt" },
139+
{ role: "user", content: "test message" },
140+
],
141+
temperature: 0,
142+
stream: true,
143+
stream_options: { include_usage: true },
144+
}),
145+
)
146+
})
147+
148+
it("handles R1 format for DeepSeek-R1 models", async () => {
149+
const handler = new NebiusHandler({
150+
...mockOptions,
151+
nebiusModelId: "deepseek-ai/DeepSeek-R1",
152+
})
153+
154+
const mockStream = {
155+
async *[Symbol.asyncIterator]() {
156+
yield {
157+
choices: [{ delta: { content: "test response" } }],
158+
}
159+
},
160+
}
161+
162+
const mockCreate = jest.fn().mockResolvedValue(mockStream)
163+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
164+
completions: { create: mockCreate },
165+
} as any
166+
167+
const systemPrompt = "test system prompt"
168+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
169+
170+
await handler.createMessage(systemPrompt, messages).next()
171+
172+
// Verify R1 format is used - the first message should combine system and user content
173+
expect(mockCreate).toHaveBeenCalledWith(
174+
expect.objectContaining({
175+
model: "deepseek-ai/DeepSeek-R1",
176+
messages: expect.arrayContaining([
177+
expect.objectContaining({
178+
role: "user",
179+
content: expect.stringContaining("test system prompt"),
180+
}),
181+
]),
182+
}),
183+
)
184+
})
185+
})
186+
187+
describe("completePrompt", () => {
188+
it("returns correct response", async () => {
189+
const handler = new NebiusHandler(mockOptions)
190+
const mockResponse = { choices: [{ message: { content: "test completion" } }] }
191+
192+
const mockCreate = jest.fn().mockResolvedValue(mockResponse)
193+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
194+
completions: { create: mockCreate },
195+
} as any
196+
197+
const result = await handler.completePrompt("test prompt")
198+
199+
expect(result).toBe("test completion")
200+
201+
expect(mockCreate).toHaveBeenCalledWith({
202+
model: mockOptions.nebiusModelId,
203+
max_tokens: 8192,
204+
temperature: 0,
205+
messages: [{ role: "user", content: "test prompt" }],
206+
})
207+
})
208+
209+
it("handles errors", async () => {
210+
const handler = new NebiusHandler(mockOptions)
211+
const mockError = new Error("API Error")
212+
213+
const mockCreate = jest.fn().mockRejectedValue(mockError)
214+
;(OpenAI as jest.MockedClass<typeof OpenAI>).prototype.chat = {
215+
completions: { create: mockCreate },
216+
} as any
217+
218+
await expect(handler.completePrompt("test prompt")).rejects.toThrow("nebius completion error: API Error")
219+
})
220+
})
221+
})

0 commit comments

Comments
 (0)