Skip to content

Commit f9f619b

Browse files
Add suggested changes
1 parent e2af681 commit f9f619b

File tree

21 files changed

+232
-34
lines changed

21 files changed

+232
-34
lines changed

packages/types/src/providers/cerebras.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ export const cerebrasModels = {
3535
},
3636
"qwen-3-235b-a22b-instruct-2507": {
3737
maxTokens: 64000,
38-
contextWindow: 640000,
38+
contextWindow: 64000,
3939
supportsImages: false,
4040
supportsPromptCache: false,
4141
inputPrice: 0,
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import { CerebrasHandler } from "../cerebras"
3+
import { cerebrasModels, type CerebrasModelId } from "@roo-code/types"
4+
5+
// Mock fetch globally
6+
global.fetch = vi.fn()
7+
8+
describe("CerebrasHandler", () => {
9+
let handler: CerebrasHandler
10+
const mockOptions = {
11+
cerebrasApiKey: "test-api-key",
12+
apiModelId: "llama-3.3-70b" as CerebrasModelId,
13+
}
14+
15+
beforeEach(() => {
16+
vi.clearAllMocks()
17+
handler = new CerebrasHandler(mockOptions)
18+
})
19+
20+
describe("constructor", () => {
21+
it("should throw error when API key is missing", () => {
22+
expect(() => new CerebrasHandler({ cerebrasApiKey: "" })).toThrow("Cerebras API key is required")
23+
})
24+
25+
it("should initialize with valid API key", () => {
26+
expect(() => new CerebrasHandler(mockOptions)).not.toThrow()
27+
})
28+
})
29+
30+
describe("getModel", () => {
31+
it("should return correct model info", () => {
32+
const { id, info } = handler.getModel()
33+
expect(id).toBe("llama-3.3-70b")
34+
expect(info).toEqual(cerebrasModels["llama-3.3-70b"])
35+
})
36+
37+
it("should fallback to default model when apiModelId is not provided", () => {
38+
const handlerWithoutModel = new CerebrasHandler({ cerebrasApiKey: "test" })
39+
const { id } = handlerWithoutModel.getModel()
40+
expect(id).toBe("qwen-3-235b-a22b-instruct-2507") // cerebrasDefaultModelId
41+
})
42+
})
43+
44+
describe("message conversion", () => {
45+
it("should strip thinking tokens from assistant messages", () => {
46+
// This would test the stripThinkingTokens function
47+
// Implementation details would test the regex functionality
48+
})
49+
50+
it("should flatten complex message content to strings", () => {
51+
// This would test the flattenMessageContent function
52+
// Test various content types: strings, arrays, image objects
53+
})
54+
55+
it("should convert OpenAI messages to Cerebras format", () => {
56+
// This would test the convertToCerebrasMessages function
57+
// Ensure all messages have string content and proper role/content structure
58+
})
59+
})
60+
61+
describe("createMessage", () => {
62+
it("should make correct API request", async () => {
63+
// Mock successful API response
64+
const mockResponse = {
65+
ok: true,
66+
body: {
67+
getReader: () => ({
68+
read: vi.fn().mockResolvedValueOnce({ done: true, value: new Uint8Array() }),
69+
releaseLock: vi.fn(),
70+
}),
71+
},
72+
}
73+
vi.mocked(fetch).mockResolvedValueOnce(mockResponse as any)
74+
75+
const generator = handler.createMessage("System prompt", [])
76+
// Test that fetch was called with correct parameters
77+
expect(fetch).toHaveBeenCalledWith(
78+
"https://api.cerebras.ai/v1/chat/completions",
79+
expect.objectContaining({
80+
method: "POST",
81+
headers: expect.objectContaining({
82+
"Content-Type": "application/json",
83+
Authorization: "Bearer test-api-key",
84+
"User-Agent": "roo-cline/1.0.0",
85+
}),
86+
}),
87+
)
88+
})
89+
90+
it("should handle API errors properly", async () => {
91+
const mockErrorResponse = {
92+
ok: false,
93+
status: 400,
94+
text: () => Promise.resolve('{"error": "Bad Request"}'),
95+
}
96+
vi.mocked(fetch).mockResolvedValueOnce(mockErrorResponse as any)
97+
98+
const generator = handler.createMessage("System prompt", [])
99+
await expect(generator.next()).rejects.toThrow("Cerebras API Error: 400")
100+
})
101+
102+
it("should parse streaming responses correctly", async () => {
103+
// Test streaming response parsing
104+
// Mock ReadableStream with various data chunks
105+
// Verify thinking token extraction and usage tracking
106+
})
107+
108+
it("should handle temperature clamping", async () => {
109+
const handlerWithTemp = new CerebrasHandler({
110+
...mockOptions,
111+
modelTemperature: 2.0, // Above Cerebras max of 1.5
112+
})
113+
114+
vi.mocked(fetch).mockResolvedValueOnce({
115+
ok: true,
116+
body: { getReader: () => ({ read: () => Promise.resolve({ done: true }), releaseLock: vi.fn() }) },
117+
} as any)
118+
119+
await handlerWithTemp.createMessage("test", []).next()
120+
121+
const requestBody = JSON.parse(vi.mocked(fetch).mock.calls[0][1]?.body as string)
122+
expect(requestBody.temperature).toBe(1.5) // Should be clamped
123+
})
124+
})
125+
126+
describe("completePrompt", () => {
127+
it("should handle non-streaming completion", async () => {
128+
const mockResponse = {
129+
ok: true,
130+
json: () =>
131+
Promise.resolve({
132+
choices: [{ message: { content: "Test response" } }],
133+
}),
134+
}
135+
vi.mocked(fetch).mockResolvedValueOnce(mockResponse as any)
136+
137+
const result = await handler.completePrompt("Test prompt")
138+
expect(result).toBe("Test response")
139+
})
140+
})
141+
142+
describe("token usage and cost calculation", () => {
143+
it("should track token usage properly", () => {
144+
// Test that lastUsage is updated correctly
145+
// Test getApiCost returns calculated cost based on actual usage
146+
})
147+
148+
it("should provide usage estimates when API doesn't return usage", () => {
149+
// Test fallback token estimation logic
150+
})
151+
})
152+
})

src/api/providers/cerebras.ts

Lines changed: 37 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -134,22 +134,6 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
134134
: {}),
135135
}
136136

137-
console.log("[CEREBRAS DEBUG] Request URL:", `${CEREBRAS_BASE_URL}/chat/completions`)
138-
console.log("[CEREBRAS DEBUG] Request body:", JSON.stringify(requestBody, null, 2))
139-
console.log("[CEREBRAS DEBUG] API key present:", !!this.apiKey)
140-
console.log("[CEREBRAS DEBUG] Message conversion:")
141-
console.log(" - Original messages:", messages.length)
142-
console.log(" - OpenAI messages:", openaiMessages.length)
143-
console.log(" - Cerebras messages:", cerebrasMessages.length)
144-
console.log(
145-
" - All content is strings:",
146-
cerebrasMessages.every((msg) => typeof msg.content === "string"),
147-
)
148-
console.log(
149-
" - Thinking tokens stripped from assistant messages:",
150-
cerebrasMessages.filter((msg) => msg.role === "assistant").length > 0 ? "✅" : "N/A",
151-
)
152-
153137
try {
154138
const response = await fetch(`${CEREBRAS_BASE_URL}/chat/completions`, {
155139
method: "POST",
@@ -161,26 +145,33 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
161145
body: JSON.stringify(requestBody),
162146
})
163147

164-
console.log("[CEREBRAS DEBUG] Response status:", response.status)
165-
const headersObj: Record<string, string> = {}
166-
response.headers.forEach((value, key) => {
167-
headersObj[key] = value
168-
})
169-
console.log("[CEREBRAS DEBUG] Response headers:", headersObj)
170-
171148
if (!response.ok) {
172149
const errorText = await response.text()
173-
console.error("[CEREBRAS DEBUG] Error response body:", errorText)
174150

175-
let errorDetails = "Unknown error"
151+
let errorMessage = "Unknown error"
176152
try {
177153
const errorJson = JSON.parse(errorText)
178-
errorDetails = JSON.stringify(errorJson, null, 2)
154+
errorMessage = errorJson.error?.message || errorJson.message || JSON.stringify(errorJson, null, 2)
179155
} catch {
180-
errorDetails = errorText || `HTTP ${response.status}`
156+
errorMessage = errorText || `HTTP ${response.status}`
181157
}
182158

183-
throw new Error(`Cerebras API Error: ${response.status} - ${errorDetails}`)
159+
// Provide more actionable error messages
160+
if (response.status === 401) {
161+
throw new Error(
162+
`Cerebras API authentication failed. Please check your API key is valid and not expired.`,
163+
)
164+
} else if (response.status === 403) {
165+
throw new Error(
166+
`Cerebras API access forbidden. Your API key may not have access to the requested model or feature.`,
167+
)
168+
} else if (response.status === 429) {
169+
throw new Error(`Cerebras API rate limit exceeded. Please wait before making another request.`)
170+
} else if (response.status >= 500) {
171+
throw new Error(`Cerebras API server error (${response.status}). Please try again later.`)
172+
} else {
173+
throw new Error(`Cerebras API Error (${response.status}): ${errorMessage}`)
174+
}
184175
}
185176

186177
if (!response.body) {
@@ -241,7 +232,7 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
241232
}
242233
}
243234
} catch (error) {
244-
console.error("[CEREBRAS DEBUG] Failed to parse streaming data:", error, "Line:", line)
235+
// Silently ignore malformed streaming data lines
245236
}
246237
}
247238
}
@@ -270,8 +261,6 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
270261
outputTokens,
271262
}
272263
} catch (error) {
273-
console.error("[CEREBRAS] Streaming error:", error)
274-
275264
if (error instanceof Error) {
276265
throw new Error(`Cerebras API error: ${error.message}`)
277266
}
@@ -302,7 +291,23 @@ export class CerebrasHandler extends BaseProvider implements SingleCompletionHan
302291

303292
if (!response.ok) {
304293
const errorText = await response.text()
305-
throw new Error(`Cerebras API Error: ${response.status} - ${errorText}`)
294+
295+
// Provide consistent error handling with createMessage
296+
if (response.status === 401) {
297+
throw new Error(
298+
`Cerebras API authentication failed. Please check your API key is valid and not expired.`,
299+
)
300+
} else if (response.status === 403) {
301+
throw new Error(
302+
`Cerebras API access forbidden. Your API key may not have access to the requested model or feature.`,
303+
)
304+
} else if (response.status === 429) {
305+
throw new Error(`Cerebras API rate limit exceeded. Please wait before making another request.`)
306+
} else if (response.status >= 500) {
307+
throw new Error(`Cerebras API server error (${response.status}). Please try again later.`)
308+
} else {
309+
throw new Error(`Cerebras API Error (${response.status}): ${errorText}`)
310+
}
306311
}
307312

308313
const result = await response.json()

webview-ui/src/components/ui/hooks/useSelectedModel.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import {
66
anthropicModels,
77
bedrockDefaultModelId,
88
bedrockModels,
9+
cerebrasDefaultModelId,
10+
cerebrasModels,
911
deepSeekDefaultModelId,
1012
deepSeekModels,
1113
moonshotDefaultModelId,
@@ -224,11 +226,16 @@ function getSelectedModel({
224226
const info = claudeCodeModels[id as keyof typeof claudeCodeModels]
225227
return { id, info: { ...openAiModelInfoSaneDefaults, ...info } }
226228
}
229+
case "cerebras": {
230+
const id = apiConfiguration.apiModelId ?? cerebrasDefaultModelId
231+
const info = cerebrasModels[id as keyof typeof cerebrasModels]
232+
return { id, info }
233+
}
227234
// case "anthropic":
228235
// case "human-relay":
229236
// case "fake-ai":
230237
default: {
231-
provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai" | "cerebras"
238+
provider satisfies "anthropic" | "gemini-cli" | "human-relay" | "fake-ai"
232239
const id = apiConfiguration.apiModelId ?? anthropicDefaultModelId
233240
const info = anthropicModels[id as keyof typeof anthropicModels]
234241
return { id, info }

webview-ui/src/i18n/locales/ca/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/de/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/es/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/fr/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/hi/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

webview-ui/src/i18n/locales/id/settings.json

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)