Skip to content

Commit a412b50

Browse files
committed
Fix the internationalization issues and add unit tests for the Z AI provider
1 parent 7b51e16 commit a412b50

File tree

3 files changed

+239
-5
lines changed

3 files changed

+239
-5
lines changed
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
// npx vitest run src/api/providers/__tests__/zai.spec.ts
2+
3+
// Mock vscode first to avoid import errors
4+
vitest.mock("vscode", () => ({}))
5+
6+
import OpenAI from "openai"
7+
import { Anthropic } from "@anthropic-ai/sdk"
8+
9+
import {
10+
type InternationalZAiModelId,
11+
type MainlandZAiModelId,
12+
internationalZAiDefaultModelId,
13+
mainlandZAiDefaultModelId,
14+
internationalZAiModels,
15+
mainlandZAiModels,
16+
ZAI_DEFAULT_TEMPERATURE,
17+
} from "@roo-code/types"
18+
19+
import { ZAiHandler } from "../zai"
20+
21+
vitest.mock("openai", () => {
22+
const createMock = vitest.fn()
23+
return {
24+
default: vitest.fn(() => ({ chat: { completions: { create: createMock } } })),
25+
}
26+
})
27+
28+
describe("ZAiHandler", () => {
29+
let handler: ZAiHandler
30+
let mockCreate: any
31+
32+
beforeEach(() => {
33+
vitest.clearAllMocks()
34+
mockCreate = (OpenAI as unknown as any)().chat.completions.create
35+
})
36+
37+
describe("International Z AI", () => {
38+
beforeEach(() => {
39+
handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" })
40+
})
41+
42+
it("should use the correct international Z AI base URL", () => {
43+
new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" })
44+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" }))
45+
})
46+
47+
it("should use the provided API key for international", () => {
48+
const zaiApiKey = "test-zai-api-key"
49+
new ZAiHandler({ zaiApiKey, zaiApiLine: "international" })
50+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey }))
51+
})
52+
53+
it("should return international default model when no model is specified", () => {
54+
const model = handler.getModel()
55+
expect(model.id).toBe(internationalZAiDefaultModelId)
56+
expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId])
57+
})
58+
59+
it("should return specified international model when valid model is provided", () => {
60+
const testModelId: InternationalZAiModelId = "glm-4.5-air"
61+
const handlerWithModel = new ZAiHandler({
62+
apiModelId: testModelId,
63+
zaiApiKey: "test-zai-api-key",
64+
zaiApiLine: "international",
65+
})
66+
const model = handlerWithModel.getModel()
67+
expect(model.id).toBe(testModelId)
68+
expect(model.info).toEqual(internationalZAiModels[testModelId])
69+
})
70+
})
71+
72+
describe("China Z AI", () => {
73+
beforeEach(() => {
74+
handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" })
75+
})
76+
77+
it("should use the correct China Z AI base URL", () => {
78+
new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "china" })
79+
expect(OpenAI).toHaveBeenCalledWith(
80+
expect.objectContaining({ baseURL: "https://open.bigmodel.cn/api/paas/v4" }),
81+
)
82+
})
83+
84+
it("should use the provided API key for China", () => {
85+
const zaiApiKey = "test-zai-api-key"
86+
new ZAiHandler({ zaiApiKey, zaiApiLine: "china" })
87+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: zaiApiKey }))
88+
})
89+
90+
it("should return China default model when no model is specified", () => {
91+
const model = handler.getModel()
92+
expect(model.id).toBe(mainlandZAiDefaultModelId)
93+
expect(model.info).toEqual(mainlandZAiModels[mainlandZAiDefaultModelId])
94+
})
95+
96+
it("should return specified China model when valid model is provided", () => {
97+
const testModelId: MainlandZAiModelId = "glm-4.5-air"
98+
const handlerWithModel = new ZAiHandler({
99+
apiModelId: testModelId,
100+
zaiApiKey: "test-zai-api-key",
101+
zaiApiLine: "china",
102+
})
103+
const model = handlerWithModel.getModel()
104+
expect(model.id).toBe(testModelId)
105+
expect(model.info).toEqual(mainlandZAiModels[testModelId])
106+
})
107+
})
108+
109+
describe("Default behavior", () => {
110+
it("should default to international when no zaiApiLine is specified", () => {
111+
const handlerDefault = new ZAiHandler({ zaiApiKey: "test-zai-api-key" })
112+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ baseURL: "https://api.z.ai/api/paas/v4" }))
113+
114+
const model = handlerDefault.getModel()
115+
expect(model.id).toBe(internationalZAiDefaultModelId)
116+
expect(model.info).toEqual(internationalZAiModels[internationalZAiDefaultModelId])
117+
})
118+
119+
it("should use 'not-provided' as default API key when none is specified", () => {
120+
new ZAiHandler({ zaiApiLine: "international" })
121+
expect(OpenAI).toHaveBeenCalledWith(expect.objectContaining({ apiKey: "not-provided" }))
122+
})
123+
})
124+
125+
describe("API Methods", () => {
126+
beforeEach(() => {
127+
handler = new ZAiHandler({ zaiApiKey: "test-zai-api-key", zaiApiLine: "international" })
128+
})
129+
130+
it("completePrompt method should return text from Z AI API", async () => {
131+
const expectedResponse = "This is a test response from Z AI"
132+
mockCreate.mockResolvedValueOnce({ choices: [{ message: { content: expectedResponse } }] })
133+
const result = await handler.completePrompt("test prompt")
134+
expect(result).toBe(expectedResponse)
135+
})
136+
137+
it("should handle errors in completePrompt", async () => {
138+
const errorMessage = "Z AI API error"
139+
mockCreate.mockRejectedValueOnce(new Error(errorMessage))
140+
await expect(handler.completePrompt("test prompt")).rejects.toThrow(
141+
`Z AI completion error: ${errorMessage}`,
142+
)
143+
})
144+
145+
it("createMessage should yield text content from stream", async () => {
146+
const testContent = "This is test content from Z AI stream"
147+
148+
mockCreate.mockImplementationOnce(() => {
149+
return {
150+
[Symbol.asyncIterator]: () => ({
151+
next: vitest
152+
.fn()
153+
.mockResolvedValueOnce({
154+
done: false,
155+
value: { choices: [{ delta: { content: testContent } }] },
156+
})
157+
.mockResolvedValueOnce({ done: true }),
158+
}),
159+
}
160+
})
161+
162+
const stream = handler.createMessage("system prompt", [])
163+
const firstChunk = await stream.next()
164+
165+
expect(firstChunk.done).toBe(false)
166+
expect(firstChunk.value).toEqual({ type: "text", text: testContent })
167+
})
168+
169+
it("createMessage should yield usage data from stream", async () => {
170+
mockCreate.mockImplementationOnce(() => {
171+
return {
172+
[Symbol.asyncIterator]: () => ({
173+
next: vitest
174+
.fn()
175+
.mockResolvedValueOnce({
176+
done: false,
177+
value: {
178+
choices: [{ delta: {} }],
179+
usage: { prompt_tokens: 10, completion_tokens: 20 },
180+
},
181+
})
182+
.mockResolvedValueOnce({ done: true }),
183+
}),
184+
}
185+
})
186+
187+
const stream = handler.createMessage("system prompt", [])
188+
const firstChunk = await stream.next()
189+
190+
expect(firstChunk.done).toBe(false)
191+
expect(firstChunk.value).toEqual({ type: "usage", inputTokens: 10, outputTokens: 20 })
192+
})
193+
194+
it("createMessage should pass correct parameters to Z AI client", async () => {
195+
const modelId: InternationalZAiModelId = "glm-4.5"
196+
const modelInfo = internationalZAiModels[modelId]
197+
const handlerWithModel = new ZAiHandler({
198+
apiModelId: modelId,
199+
zaiApiKey: "test-zai-api-key",
200+
zaiApiLine: "international",
201+
})
202+
203+
mockCreate.mockImplementationOnce(() => {
204+
return {
205+
[Symbol.asyncIterator]: () => ({
206+
async next() {
207+
return { done: true }
208+
},
209+
}),
210+
}
211+
})
212+
213+
const systemPrompt = "Test system prompt for Z AI"
214+
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test message for Z AI" }]
215+
216+
const messageGenerator = handlerWithModel.createMessage(systemPrompt, messages)
217+
await messageGenerator.next()
218+
219+
expect(mockCreate).toHaveBeenCalledWith(
220+
expect.objectContaining({
221+
model: modelId,
222+
max_tokens: modelInfo.maxTokens,
223+
temperature: ZAI_DEFAULT_TEMPERATURE,
224+
messages: expect.arrayContaining([{ role: "system", content: systemPrompt }]),
225+
stream: true,
226+
stream_options: { include_usage: true },
227+
}),
228+
)
229+
})
230+
})
231+
})

webview-ui/src/components/settings/providers/ZAi.tsx

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) =>
3131
return (
3232
<>
3333
<div>
34-
<label className="block font-medium mb-1">Z AI Entrypoint</label>
34+
<label className="block font-medium mb-1">{t("settings:providers.zaiEntrypoint")}</label>
3535
<VSCodeDropdown
3636
value={apiConfiguration.zaiApiLine || "international"}
3737
onChange={handleInputChange("zaiApiLine")}
@@ -44,8 +44,7 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) =>
4444
</VSCodeOption>
4545
</VSCodeDropdown>
4646
<div className="text-xs text-vscode-descriptionForeground mt-1">
47-
Please select the appropriate API entrypoint based on your location. If you are in China, choose
48-
open.bigmodel.cn. Otherwise, choose api.z.ai.
47+
{t("settings:providers.zaiEntrypointDescription")}
4948
</div>
5049
</div>
5150
<div>
@@ -55,7 +54,7 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) =>
5554
onInput={handleInputChange("zaiApiKey")}
5655
placeholder={t("settings:placeholders.apiKey")}
5756
className="w-full">
58-
<label className="block font-medium mb-1">Z AI API Key</label>
57+
<label className="block font-medium mb-1">{t("settings:providers.zaiApiKey")}</label>
5958
</VSCodeTextField>
6059
<div className="text-sm text-vscode-descriptionForeground">
6160
{t("settings:providers.apiKeyStorageNotice")}
@@ -68,7 +67,7 @@ export const ZAi = ({ apiConfiguration, setApiConfigurationField }: ZAiProps) =>
6867
: "https://z.ai/manage-apikey/apikey-list"
6968
}
7069
appearance="secondary">
71-
Get Z AI API Key
70+
{t("settings:providers.getZaiApiKey")}
7271
</VSCodeButtonLink>
7372
)}
7473
</div>

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,10 @@
267267
"moonshotApiKey": "Moonshot API Key",
268268
"getMoonshotApiKey": "Get Moonshot API Key",
269269
"moonshotBaseUrl": "Moonshot Entrypoint",
270+
"zaiApiKey": "Z AI API Key",
271+
"getZaiApiKey": "Get Z AI API Key",
272+
"zaiEntrypoint": "Z AI Entrypoint",
273+
"zaiEntrypointDescription": "Please select the appropriate API entrypoint based on your location. If you are in China, choose open.bigmodel.cn. Otherwise, choose api.z.ai.",
270274
"geminiApiKey": "Gemini API Key",
271275
"getGroqApiKey": "Get Groq API Key",
272276
"groqApiKey": "Groq API Key",

0 commit comments

Comments
 (0)