Skip to content

Commit f9012a6

Browse files
Merge pull request #119 from modelstudioai/fix/base-url-origin-only
fix(core): normalize model base URL to origin
2 parents 678f60b + 92ee845 commit f9012a6

8 files changed

Lines changed: 26 additions & 41 deletions

File tree

docs/agents/auth-change.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
4343
解析分工:
4444

4545
- `resolveApiKey()``auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key`
46-
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`返回前统一去除 query、fragment、尾斜杠和已知 SDK/API Base 后缀,同时保留自定义网关前缀
46+
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
4747
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值
4848
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
4949
`--config` 时继续写当前激活项,失败和 dry-run 不切换

packages/commands/tests/config-ui.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,10 +114,10 @@ test("POST /api/profile 写命名 profile(timeout 强制为 number),空串
114114
expect(readConfigFile("stage")).toMatchObject({
115115
api_key: "sk-stage",
116116
timeout: 90,
117-
base_url: "https://proxy.example.com/team",
117+
base_url: "https://proxy.example.com",
118118
});
119119
const rawConfig = JSON.parse(readFileSync(getConfigPath(), "utf8"));
120-
expect(rawConfig.stage.base_url).toBe("https://proxy.example.com/team");
120+
expect(rawConfig.stage.base_url).toBe("https://proxy.example.com");
121121

122122
// 空串清除 api_key(整块替换)
123123
const clear = await httpJson(port, "POST", `/api/profile?token=${TOKEN}`, {

packages/commands/tests/e2e/config.e2e.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,10 +247,10 @@ describe("e2e: config", () => {
247247
);
248248
expect(setResult.exitCode, setResult.stderr).toBe(0);
249249
expect(parseStdoutJson<{ base_url?: string }>(setResult.stdout).base_url).toBe(
250-
"https://proxy.example.com/bailian",
250+
"https://proxy.example.com",
251251
);
252252
expect(JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")).base_url).toBe(
253-
"https://proxy.example.com/bailian",
253+
"https://proxy.example.com",
254254
);
255255

256256
const invalidResult = await runCommandE2e(

packages/core/src/config/model-base-url.ts

Lines changed: 4 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,10 @@
11
import { BailianError } from "../errors/base.ts";
22
import { ExitCode } from "../errors/codes.ts";
33

4-
const KNOWN_API_BASE_SUFFIXES = ["/compatible-mode/v1", "/apps/anthropic"] as const;
5-
64
/**
7-
* Normalize a model-service base URL while preserving custom gateway prefixes.
8-
* CLI endpoints append their own API paths, so known SDK/API base suffixes must
9-
* not remain in the stored or resolved base URL.
5+
* Normalize a model-service base URL to its origin.
6+
* CLI endpoints append their own API paths, so user-provided paths, query
7+
* parameters, and fragments must not remain in the stored or resolved base URL.
108
*/
119
export function normalizeModelBaseUrl(input: string): string {
1210
const trimmed = input.trim();
@@ -21,19 +19,7 @@ export function normalizeModelBaseUrl(input: string): string {
2119
throw invalidModelBaseUrl(input);
2220
}
2321

24-
parsed.search = "";
25-
parsed.hash = "";
26-
27-
let pathname = parsed.pathname.replace(/\/+$/, "");
28-
const knownSuffix = KNOWN_API_BASE_SUFFIXES.find(
29-
(suffix) => pathname === suffix || pathname.endsWith(suffix),
30-
);
31-
if (knownSuffix) {
32-
pathname = pathname.slice(0, -knownSuffix.length).replace(/\/+$/, "");
33-
}
34-
parsed.pathname = pathname || "/";
35-
36-
return parsed.toString().replace(/\/$/, "");
22+
return parsed.origin;
3723
}
3824

3925
function invalidModelBaseUrl(input: string): BailianError {

packages/core/tests/config-priority.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ test("baseUrl:flag > env > file > 默认,所有来源统一归一化", () => {
4646
const file: ConfigFile = { base_url: "https://file.example.com/gateway/" };
4747
expect(resolveModelBaseUrl(src({ flags, env, file }))).toBe("https://flag.example.com");
4848
expect(resolveModelBaseUrl(src({ env, file }))).toBe("https://env.example.com");
49-
expect(resolveModelBaseUrl(src({ file }))).toBe("https://file.example.com/gateway");
49+
expect(resolveModelBaseUrl(src({ file }))).toBe("https://file.example.com");
5050
expect(resolveModelBaseUrl(src({}))).toBe("https://dashscope.aliyuncs.com");
5151
});
5252

packages/core/tests/config-store.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,9 @@ test("ConfigStore/AuthStore 写入前归一化 model Base URL", async () => {
5454
await configStore.write({
5555
base_url: "https://proxy.example.com/bailian/compatible-mode/v1/?query=one#fragment",
5656
});
57-
expect(readConfigFile().base_url).toBe("https://proxy.example.com/bailian");
57+
expect(readConfigFile().base_url).toBe("https://proxy.example.com");
5858
expect(JSON.parse(readFileSync(getConfigPath(), "utf8")).base_url).toBe(
59-
"https://proxy.example.com/bailian",
59+
"https://proxy.example.com",
6060
);
6161

6262
const authStore = makeAuthStore(buildSources({}));

packages/core/tests/index.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ test("parseConfigFile accepts only well-formed http(s) base_url", () => {
321321
expect(
322322
parseConfigFile({ base_url: "https://proxy.example.com/team/compatible-mode/v1?x=1#y" })
323323
.base_url,
324-
).toBe("https://proxy.example.com/team");
324+
).toBe("https://proxy.example.com");
325325
// Previously accepted because the value merely "starts with http".
326326
expect(parseConfigFile({ base_url: "httpfoo://evil" }).base_url).toBeUndefined();
327327
expect(parseConfigFile({ base_url: "not a url" }).base_url).toBeUndefined();

packages/core/tests/model-base-url.test.ts

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,28 +2,27 @@ import { expect, test } from "vite-plus/test";
22
import { BailianError } from "../src/errors/base.ts";
33
import { normalizeModelBaseUrl } from "../src/config/model-base-url.ts";
44

5-
test("normalizeModelBaseUrl removes URL noise and known API base suffixes", () => {
5+
test("normalizeModelBaseUrl keeps only the URL origin", () => {
66
expect(normalizeModelBaseUrl(" https://dashscope.aliyuncs.com/?region=cn#docs ")).toBe(
77
"https://dashscope.aliyuncs.com",
88
);
99
expect(
10-
normalizeModelBaseUrl("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/"),
10+
normalizeModelBaseUrl(
11+
"https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions",
12+
),
1113
).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com");
14+
expect(normalizeModelBaseUrl("https://example.com/api/v1/agentstudio")).toBe(
15+
"https://example.com",
16+
);
1217
expect(
13-
normalizeModelBaseUrl("https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic"),
14-
).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com");
18+
normalizeModelBaseUrl(
19+
"https://example.com/api/v1/services/aigc/image-generation/generation?model=qwen#docs",
20+
),
21+
).toBe("https://example.com");
1522
});
1623

17-
test("normalizeModelBaseUrl preserves ports and custom gateway prefixes", () => {
18-
expect(normalizeModelBaseUrl("http://localhost:8080/bailian/")).toBe(
19-
"http://localhost:8080/bailian",
20-
);
21-
expect(
22-
normalizeModelBaseUrl("https://proxy.example.com/bailian/compatible-mode/v1?tenant=one"),
23-
).toBe("https://proxy.example.com/bailian");
24-
expect(normalizeModelBaseUrl("https://proxy.example.com/custom/apps/anthropic#section")).toBe(
25-
"https://proxy.example.com/custom",
26-
);
24+
test("normalizeModelBaseUrl preserves explicit ports while removing paths", () => {
25+
expect(normalizeModelBaseUrl("http://localhost:8080/bailian/")).toBe("http://localhost:8080");
2726
});
2827

2928
test("normalizeModelBaseUrl rejects non-http and malformed URLs", () => {

0 commit comments

Comments
 (0)