Skip to content

Commit 58680a6

Browse files
authored
Merge pull request #11 from dashscope/feat/intl_support
feat: add region selection for China and International DashScope endp…
2 parents d5688fd + 841073e commit 58680a6

3 files changed

Lines changed: 203 additions & 13 deletions

File tree

__tests__/claude-code-router-config.test.js

Lines changed: 144 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -120,22 +120,50 @@ describe("ClaudeCodeRouterConfig", () => {
120120
});
121121
});
122122

123+
describe("error handling", () => {
124+
beforeEach(() => {
125+
config = new ClaudeCodeRouterConfig();
126+
});
127+
128+
it("should handle transformer file write errors", async () => {
129+
const error = new Error("Write failed");
130+
fs.writeFile.mockRejectedValueOnce(error);
131+
fs.writeJson.mockResolvedValue(); // Make sure other fs operations succeed
132+
133+
await expect(config.createTransformerFile()).rejects.toThrow(
134+
"Write failed"
135+
);
136+
});
137+
138+
it("should handle config file write errors", async () => {
139+
const error = new Error("JSON write failed");
140+
fs.writeJson.mockRejectedValueOnce(error);
141+
fs.writeFile.mockResolvedValue(); // Make sure other fs operations succeed
142+
143+
await expect(config.createConfigFile("test-key", "cn")).rejects.toThrow(
144+
"JSON write failed"
145+
);
146+
});
147+
});
148+
123149
describe("createConfigFile", () => {
124150
beforeEach(() => {
125151
config = new ClaudeCodeRouterConfig();
126152
});
127153

128154
it("should create config file with API Key from environment variable", async () => {
129155
const testApiKey = "test-api-key-from-env";
156+
const testRegion = "cn";
130157

131-
await config.createConfigFile(testApiKey);
158+
await config.createConfigFile(testApiKey, testRegion);
132159

133160
expect(fs.writeJson).toHaveBeenCalledWith(
134161
config.configFile,
135162
expect.objectContaining({
136163
Providers: expect.arrayContaining([
137164
expect.objectContaining({
138165
api_key: testApiKey,
166+
api_base_url: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
139167
}),
140168
]),
141169
}),
@@ -145,15 +173,17 @@ describe("ClaudeCodeRouterConfig", () => {
145173

146174
it("should create config file with provided API Key", async () => {
147175
const testApiKey = "test-api-key-provided";
176+
const testRegion = "intl";
148177

149-
await config.createConfigFile(testApiKey);
178+
await config.createConfigFile(testApiKey, testRegion);
150179

151180
expect(fs.writeJson).toHaveBeenCalledWith(
152181
config.configFile,
153182
expect.objectContaining({
154183
Providers: expect.arrayContaining([
155184
expect.objectContaining({
156185
api_key: testApiKey,
186+
api_base_url: "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions",
157187
}),
158188
]),
159189
}),
@@ -162,7 +192,7 @@ describe("ClaudeCodeRouterConfig", () => {
162192
});
163193

164194
it("should use undefined API Key when no API Key is provided", async () => {
165-
await config.createConfigFile();
195+
await config.createConfigFile(undefined, "cn");
166196

167197
expect(fs.writeJson).toHaveBeenCalledWith(
168198
config.configFile,
@@ -178,7 +208,7 @@ describe("ClaudeCodeRouterConfig", () => {
178208
});
179209

180210
it("should contain correct configuration structure", async () => {
181-
await config.createConfigFile();
211+
await config.createConfigFile("test-key", "cn");
182212

183213
const writeJsonCall = fs.writeJson.mock.calls[0];
184214
const configContent = writeJsonCall[1];
@@ -232,6 +262,7 @@ describe("ClaudeCodeRouterConfig", () => {
232262
jest.spyOn(config, "createConfigFile").mockResolvedValue();
233263
jest.spyOn(config, "createTransformerFile").mockResolvedValue();
234264
jest.spyOn(config, "promptForApiKey").mockResolvedValue("user-input-key");
265+
jest.spyOn(config, "promptForRegion").mockResolvedValue("cn");
235266
});
236267

237268
afterEach(() => {
@@ -244,8 +275,9 @@ describe("ClaudeCodeRouterConfig", () => {
244275

245276
await config.setup();
246277

278+
expect(config.promptForRegion).toHaveBeenCalled();
247279
expect(config.createDirectories).toHaveBeenCalled();
248-
expect(config.createConfigFile).toHaveBeenCalledWith("env-test-key");
280+
expect(config.createConfigFile).toHaveBeenCalledWith("env-test-key", "cn");
249281
expect(config.createTransformerFile).toHaveBeenCalled();
250282
expect(config.promptForApiKey).not.toHaveBeenCalled();
251283
});
@@ -260,7 +292,7 @@ describe("ClaudeCodeRouterConfig", () => {
260292
"DASHSCOPE_API_KEY environment variable detected"
261293
)
262294
);
263-
expect(config.createConfigFile).toHaveBeenCalledWith("test-key-from-env");
295+
expect(config.createConfigFile).toHaveBeenCalledWith("test-key-from-env", "cn");
264296
});
265297

266298
it("should prompt for API Key when environment variable is not present", async () => {
@@ -274,7 +306,7 @@ describe("ClaudeCodeRouterConfig", () => {
274306
)
275307
);
276308
expect(config.promptForApiKey).toHaveBeenCalled();
277-
expect(config.createConfigFile).toHaveBeenCalledWith("user-input-key");
309+
expect(config.createConfigFile).toHaveBeenCalledWith("user-input-key", "cn");
278310
});
279311

280312
it("should handle errors during setup process", async () => {
@@ -287,6 +319,111 @@ describe("ClaudeCodeRouterConfig", () => {
287319
});
288320
});
289321

322+
describe("promptForRegion", () => {
323+
beforeEach(() => {
324+
config = new ClaudeCodeRouterConfig();
325+
326+
// Mock console.log
327+
jest.spyOn(console, "log").mockImplementation();
328+
});
329+
330+
afterEach(() => {
331+
console.log.mockRestore();
332+
});
333+
334+
it("should prompt for region and return 'cn' when user selects 1", async () => {
335+
const mockReadline = {
336+
question: jest.fn(),
337+
close: jest.fn()
338+
};
339+
340+
const readline = require("readline");
341+
jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline);
342+
343+
mockReadline.question.mockImplementation((prompt, callback) => {
344+
callback("1");
345+
});
346+
347+
const result = await config.promptForRegion();
348+
349+
expect(readline.createInterface).toHaveBeenCalledWith({
350+
input: process.stdin,
351+
output: process.stdout
352+
});
353+
expect(mockReadline.question).toHaveBeenCalled();
354+
expect(mockReadline.close).toHaveBeenCalled();
355+
expect(result).toBe("cn");
356+
});
357+
358+
it("should prompt for region and return 'intl' when user selects 2", async () => {
359+
const mockReadline = {
360+
question: jest.fn(),
361+
close: jest.fn()
362+
};
363+
364+
const readline = require("readline");
365+
jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline);
366+
367+
mockReadline.question.mockImplementation((prompt, callback) => {
368+
callback("2");
369+
});
370+
371+
const result = await config.promptForRegion();
372+
373+
expect(result).toBe("intl");
374+
expect(mockReadline.close).toHaveBeenCalled();
375+
});
376+
377+
it("should re-prompt when invalid input is provided", async () => {
378+
const mockReadline = {
379+
question: jest.fn(),
380+
close: jest.fn()
381+
};
382+
383+
const readline = require("readline");
384+
jest.spyOn(readline, "createInterface").mockReturnValue(mockReadline);
385+
386+
let callCount = 0;
387+
mockReadline.question.mockImplementation((prompt, callback) => {
388+
callCount++;
389+
if (callCount === 1) {
390+
callback("invalid");
391+
} else {
392+
callback("1");
393+
}
394+
});
395+
396+
const result = await config.promptForRegion();
397+
398+
expect(mockReadline.question).toHaveBeenCalledTimes(2);
399+
expect(result).toBe("cn");
400+
expect(console.log).toHaveBeenCalledWith(
401+
expect.stringContaining("Please enter 1 or 2")
402+
);
403+
});
404+
});
405+
406+
describe("getApiBaseUrl", () => {
407+
beforeEach(() => {
408+
config = new ClaudeCodeRouterConfig();
409+
});
410+
411+
it("should return China URL for 'cn' region", () => {
412+
const result = config.getApiBaseUrl("cn");
413+
expect(result).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions");
414+
});
415+
416+
it("should return International URL for 'intl' region", () => {
417+
const result = config.getApiBaseUrl("intl");
418+
expect(result).toBe("https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions");
419+
});
420+
421+
it("should default to China URL for unknown region", () => {
422+
const result = config.getApiBaseUrl("unknown");
423+
expect(result).toBe("https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions");
424+
});
425+
});
426+
290427
describe("promptForApiKey", () => {
291428
beforeEach(() => {
292429
config = new ClaudeCodeRouterConfig();

bin/claude-code-router-config.js

Lines changed: 57 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,42 @@ class ClaudeCodeRouterConfig {
2525
return locale.toLowerCase().includes('zh') ? 'zh' : 'en';
2626
}
2727

28+
async promptForRegion() {
29+
const rl = readline.createInterface({
30+
input: process.stdin,
31+
output: process.stdout
32+
});
33+
34+
return new Promise((resolve) => {
35+
const askForRegion = () => {
36+
const prompt = `${this.messages.regionPrompt}\n${this.messages.regionOption1}\n${this.messages.regionOption2}\n${this.messages.regionInput}`;
37+
38+
rl.question(prompt, (answer) => {
39+
const choice = answer.trim();
40+
if (choice === '1') {
41+
console.log(this.messages.regionSelected1);
42+
rl.close();
43+
resolve('cn');
44+
} else if (choice === '2') {
45+
console.log(this.messages.regionSelected2);
46+
rl.close();
47+
resolve('intl');
48+
} else {
49+
console.log(this.messages.regionInvalid);
50+
askForRegion();
51+
}
52+
});
53+
};
54+
askForRegion();
55+
});
56+
}
57+
58+
getApiBaseUrl(region) {
59+
return region === 'intl'
60+
? "https://dashscope-intl.aliyuncs.com/compatible-mode/v1/chat/completions"
61+
: "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions";
62+
}
63+
2864
getMessages() {
2965
const messages = {
3066
zh: {
@@ -38,6 +74,13 @@ class ClaudeCodeRouterConfig {
3874
step2: "2. 请确保已安装 @musistudio/claude-code-router",
3975
step3Warning: "3. ⚠️ 请手动配置环境变量DASHSCOPE_API_KEY:",
4076
step3Success: "3. ✅ API Key 已从环境变量自动配置",
77+
regionPrompt: "请选择服务区域 (Please select service region):",
78+
regionOption1: "1. 阿里云 (Alibaba Cloud China)",
79+
regionOption2: "2. 阿里云国际站 (Alibaba Cloud International)",
80+
regionInput: "请输入 1 或 2 (Enter 1 or 2): ",
81+
regionSelected1: "✅ 已选择阿里云中国站",
82+
regionSelected2: "✅ 已选择阿里云国际站",
83+
regionInvalid: "❌ 请输入 1 或 2",
4184
promptApiKey: "请输入您的 DashScope API Key:",
4285
apiKeyPrompt: "DashScope API Key",
4386
apiKeyConfigured: "✅ API Key 已配置完成",
@@ -65,6 +108,13 @@ class ClaudeCodeRouterConfig {
65108
step2: "2. Please ensure @musistudio/claude-code-router is installed",
66109
step3Warning: "3. ⚠️ Please manually set your DASHSCOPE_API_KEY environment variable:",
67110
step3Success: "3. ✅ API Key automatically configured from environment variable",
111+
regionPrompt: "Please select service region:",
112+
regionOption1: "1. Alibaba Cloud China",
113+
regionOption2: "2. Alibaba Cloud International",
114+
regionInput: "Enter 1 or 2: ",
115+
regionSelected1: "✅ Selected Alibaba Cloud China",
116+
regionSelected2: "✅ Selected Alibaba Cloud International",
117+
regionInvalid: "❌ Please enter 1 or 2",
68118
promptApiKey: "Please enter your DashScope API Key:",
69119
apiKeyPrompt: "DashScope API Key",
70120
apiKeyConfigured: "✅ API Key configured successfully",
@@ -113,6 +163,9 @@ class ClaudeCodeRouterConfig {
113163
try {
114164
console.log(this.messages.configuring);
115165

166+
// 询问用户选择服务区域
167+
const region = await this.promptForRegion();
168+
116169
// 检查环境变量
117170
let apiKey = process.env.DASHSCOPE_API_KEY;
118171
const hasEnvApiKey = !!apiKey;
@@ -129,7 +182,7 @@ class ClaudeCodeRouterConfig {
129182
await this.createDirectories();
130183

131184
// 创建配置文件
132-
await this.createConfigFile(apiKey);
185+
await this.createConfigFile(apiKey, region);
133186

134187
// 创建插件文件
135188
await this.createTransformerFile();
@@ -158,9 +211,10 @@ class ClaudeCodeRouterConfig {
158211
console.log(this.messages.createDir, this.configDir);
159212
}
160213

161-
async createConfigFile(apiKey) {
214+
async createConfigFile(apiKey, region) {
162215
// 使用传入的 API Key(可能来自环境变量或用户输入)
163216
const dashscopeApiKey = apiKey;
217+
const apiBaseUrl = this.getApiBaseUrl(region);
164218

165219
const configContent = {
166220
LOG: true,
@@ -183,8 +237,7 @@ class ClaudeCodeRouterConfig {
183237
Providers: [
184238
{
185239
name: "dashscope",
186-
api_base_url:
187-
"https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions",
240+
api_base_url: apiBaseUrl,
188241
api_key: dashscopeApiKey,
189242
models: ["qwen3-235b-a22b"],
190243
transformer: {

package-lock.json

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

0 commit comments

Comments
 (0)