From 92ee845bdd5b37139a2c24ccf5239f06afa5b0ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=8B=A5=E9=BA=92?= Date: Thu, 23 Jul 2026 19:31:02 +0800 Subject: [PATCH] fix(core): normalize model base URL to origin --- docs/agents/auth-change.md | 2 +- packages/commands/tests/config-ui.test.ts | 4 +-- .../commands/tests/e2e/config.e2e.test.ts | 4 +-- packages/core/src/config/model-base-url.ts | 22 +++------------ packages/core/tests/config-priority.test.ts | 2 +- packages/core/tests/config-store.test.ts | 4 +-- packages/core/tests/index.test.ts | 2 +- packages/core/tests/model-base-url.test.ts | 27 +++++++++---------- 8 files changed, 26 insertions(+), 41 deletions(-) diff --git a/docs/agents/auth-change.md b/docs/agents/auth-change.md index 55f92ba9..6bc53a9d 100644 --- a/docs/agents/auth-change.md +++ b/docs/agents/auth-change.md @@ -43,7 +43,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx 解析分工: - `resolveApiKey()` — `auth: "apiKey"` 命令;优先级 `--api-key` > `DASHSCOPE_API_KEY` > config `api_key` -- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一去除 query、fragment、尾斜杠和已知 SDK/API Base 后缀,同时保留自定义网关前缀 +- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment) - `--config` 只选择 config 文件 block,不提升该 block 的字段优先级;内置套餐 Profile(当前为 `token-plan`)的预设仅在登录时物化写入,运行时继续走统一的 flag > env > selected config file > 默认值 - 显式 `auth login --config ` 在凭证验证并落盘成功后自动激活目标 Profile;未传 `--config` 时继续写当前激活项,失败和 dry-run 不切换 diff --git a/packages/commands/tests/config-ui.test.ts b/packages/commands/tests/config-ui.test.ts index db309265..cb350376 100644 --- a/packages/commands/tests/config-ui.test.ts +++ b/packages/commands/tests/config-ui.test.ts @@ -114,10 +114,10 @@ test("POST /api/profile 写命名 profile(timeout 强制为 number),空串 expect(readConfigFile("stage")).toMatchObject({ api_key: "sk-stage", timeout: 90, - base_url: "https://proxy.example.com/team", + base_url: "https://proxy.example.com", }); const rawConfig = JSON.parse(readFileSync(getConfigPath(), "utf8")); - expect(rawConfig.stage.base_url).toBe("https://proxy.example.com/team"); + expect(rawConfig.stage.base_url).toBe("https://proxy.example.com"); // 空串清除 api_key(整块替换) const clear = await httpJson(port, "POST", `/api/profile?token=${TOKEN}`, { diff --git a/packages/commands/tests/e2e/config.e2e.test.ts b/packages/commands/tests/e2e/config.e2e.test.ts index 1391c1a5..a0a48f9f 100644 --- a/packages/commands/tests/e2e/config.e2e.test.ts +++ b/packages/commands/tests/e2e/config.e2e.test.ts @@ -247,10 +247,10 @@ describe("e2e: config", () => { ); expect(setResult.exitCode, setResult.stderr).toBe(0); expect(parseStdoutJson<{ base_url?: string }>(setResult.stdout).base_url).toBe( - "https://proxy.example.com/bailian", + "https://proxy.example.com", ); expect(JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")).base_url).toBe( - "https://proxy.example.com/bailian", + "https://proxy.example.com", ); const invalidResult = await runCommandE2e( diff --git a/packages/core/src/config/model-base-url.ts b/packages/core/src/config/model-base-url.ts index 56101b70..9d2a1d2b 100644 --- a/packages/core/src/config/model-base-url.ts +++ b/packages/core/src/config/model-base-url.ts @@ -1,12 +1,10 @@ import { BailianError } from "../errors/base.ts"; import { ExitCode } from "../errors/codes.ts"; -const KNOWN_API_BASE_SUFFIXES = ["/compatible-mode/v1", "/apps/anthropic"] as const; - /** - * Normalize a model-service base URL while preserving custom gateway prefixes. - * CLI endpoints append their own API paths, so known SDK/API base suffixes must - * not remain in the stored or resolved base URL. + * Normalize a model-service base URL to its origin. + * CLI endpoints append their own API paths, so user-provided paths, query + * parameters, and fragments must not remain in the stored or resolved base URL. */ export function normalizeModelBaseUrl(input: string): string { const trimmed = input.trim(); @@ -21,19 +19,7 @@ export function normalizeModelBaseUrl(input: string): string { throw invalidModelBaseUrl(input); } - parsed.search = ""; - parsed.hash = ""; - - let pathname = parsed.pathname.replace(/\/+$/, ""); - const knownSuffix = KNOWN_API_BASE_SUFFIXES.find( - (suffix) => pathname === suffix || pathname.endsWith(suffix), - ); - if (knownSuffix) { - pathname = pathname.slice(0, -knownSuffix.length).replace(/\/+$/, ""); - } - parsed.pathname = pathname || "/"; - - return parsed.toString().replace(/\/$/, ""); + return parsed.origin; } function invalidModelBaseUrl(input: string): BailianError { diff --git a/packages/core/tests/config-priority.test.ts b/packages/core/tests/config-priority.test.ts index 89788672..726513c6 100644 --- a/packages/core/tests/config-priority.test.ts +++ b/packages/core/tests/config-priority.test.ts @@ -46,7 +46,7 @@ test("baseUrl:flag > env > file > 默认,所有来源统一归一化", () => { const file: ConfigFile = { base_url: "https://file.example.com/gateway/" }; expect(resolveModelBaseUrl(src({ flags, env, file }))).toBe("https://flag.example.com"); expect(resolveModelBaseUrl(src({ env, file }))).toBe("https://env.example.com"); - expect(resolveModelBaseUrl(src({ file }))).toBe("https://file.example.com/gateway"); + expect(resolveModelBaseUrl(src({ file }))).toBe("https://file.example.com"); expect(resolveModelBaseUrl(src({}))).toBe("https://dashscope.aliyuncs.com"); }); diff --git a/packages/core/tests/config-store.test.ts b/packages/core/tests/config-store.test.ts index c266797e..a907a2ba 100644 --- a/packages/core/tests/config-store.test.ts +++ b/packages/core/tests/config-store.test.ts @@ -54,9 +54,9 @@ test("ConfigStore/AuthStore 写入前归一化 model Base URL", async () => { await configStore.write({ base_url: "https://proxy.example.com/bailian/compatible-mode/v1/?query=one#fragment", }); - expect(readConfigFile().base_url).toBe("https://proxy.example.com/bailian"); + expect(readConfigFile().base_url).toBe("https://proxy.example.com"); expect(JSON.parse(readFileSync(getConfigPath(), "utf8")).base_url).toBe( - "https://proxy.example.com/bailian", + "https://proxy.example.com", ); const authStore = makeAuthStore(buildSources({})); diff --git a/packages/core/tests/index.test.ts b/packages/core/tests/index.test.ts index 7c8dc3b8..b8aaf7fa 100644 --- a/packages/core/tests/index.test.ts +++ b/packages/core/tests/index.test.ts @@ -321,7 +321,7 @@ test("parseConfigFile accepts only well-formed http(s) base_url", () => { expect( parseConfigFile({ base_url: "https://proxy.example.com/team/compatible-mode/v1?x=1#y" }) .base_url, - ).toBe("https://proxy.example.com/team"); + ).toBe("https://proxy.example.com"); // Previously accepted because the value merely "starts with http". expect(parseConfigFile({ base_url: "httpfoo://evil" }).base_url).toBeUndefined(); expect(parseConfigFile({ base_url: "not a url" }).base_url).toBeUndefined(); diff --git a/packages/core/tests/model-base-url.test.ts b/packages/core/tests/model-base-url.test.ts index cfcc613f..1a0877ce 100644 --- a/packages/core/tests/model-base-url.test.ts +++ b/packages/core/tests/model-base-url.test.ts @@ -2,28 +2,27 @@ import { expect, test } from "vite-plus/test"; import { BailianError } from "../src/errors/base.ts"; import { normalizeModelBaseUrl } from "../src/config/model-base-url.ts"; -test("normalizeModelBaseUrl removes URL noise and known API base suffixes", () => { +test("normalizeModelBaseUrl keeps only the URL origin", () => { expect(normalizeModelBaseUrl(" https://dashscope.aliyuncs.com/?region=cn#docs ")).toBe( "https://dashscope.aliyuncs.com", ); expect( - normalizeModelBaseUrl("https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/"), + normalizeModelBaseUrl( + "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/chat/completions", + ), ).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com"); + expect(normalizeModelBaseUrl("https://example.com/api/v1/agentstudio")).toBe( + "https://example.com", + ); expect( - normalizeModelBaseUrl("https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic"), - ).toBe("https://token-plan.cn-beijing.maas.aliyuncs.com"); + normalizeModelBaseUrl( + "https://example.com/api/v1/services/aigc/image-generation/generation?model=qwen#docs", + ), + ).toBe("https://example.com"); }); -test("normalizeModelBaseUrl preserves ports and custom gateway prefixes", () => { - expect(normalizeModelBaseUrl("http://localhost:8080/bailian/")).toBe( - "http://localhost:8080/bailian", - ); - expect( - normalizeModelBaseUrl("https://proxy.example.com/bailian/compatible-mode/v1?tenant=one"), - ).toBe("https://proxy.example.com/bailian"); - expect(normalizeModelBaseUrl("https://proxy.example.com/custom/apps/anthropic#section")).toBe( - "https://proxy.example.com/custom", - ); +test("normalizeModelBaseUrl preserves explicit ports while removing paths", () => { + expect(normalizeModelBaseUrl("http://localhost:8080/bailian/")).toBe("http://localhost:8080"); }); test("normalizeModelBaseUrl rejects non-http and malformed URLs", () => {