Skip to content

Commit 3d39548

Browse files
committed
fix(auth): probe Compatible /models on *.maas.aliyuncs.com
1 parent 1d99e03 commit 3d39548

4 files changed

Lines changed: 203 additions & 15 deletions

File tree

‎docs/agents/auth-change.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
3333

3434
`~/.bailian/config.json` 可同时保存 `api_key`、`access_token` 与 `access_key_*`。登录任一种方式不得删除另一种:
3535

36-
- `bl auth login --api-key ...` 先通过只读 `GET /models` 校验 Key,校验成功后才原子写入 `api_key` 与实际匹配的 `base_url`。显式 `--base-url` 只校验该站点,不自动改写;未显式指定时,普通 `sk-*` / `sk-ws-*` 按“当前 Profile 已保存的同类站点 → 基于 `workspace_id` 构造的各地域 Workspace 专属站点 → 公共地域”顺序选择首个成功结果,`sk-sp-*` 并行探测国内站与新加坡 Token Plan,其他格式同时尝试两类候选。所有候选明确拒绝时返回 `AUTH`;网络、超时、非 JSON 或 5xx 视为校验无法确定并透传原错误。任一失败都不落盘、不激活 Profile
36+
- `bl auth login --api-key ...` 先通过只读 `GET /models` 校验 Key,校验成功后才原子写入 `api_key` 与实际匹配的 `base_url`。公共 DashScope 站点使用 `/api/v1/models?page_no=1&page_size=1`,`*.maas.aliyuncs.com`(Workspace 与 Token Plan)使用 `/compatible-mode/v1/models`。显式 `--base-url` 只校验该站点,不自动改写;未显式指定时,普通 `sk-*` / `sk-ws-*` 按“当前 Profile 已保存的同类站点 → 基于 `workspace_id` 构造的各地域 Workspace 专属站点 → 公共地域”顺序选择首个成功结果,`sk-sp-*` 并行探测国内站与新加坡 Token Plan,其他格式同时尝试两类候选。所有候选明确拒绝时返回 `AUTH`;网络、超时、非 JSON 或 5xx 视为校验无法确定并透传原错误。任一失败都不落盘、不激活 Profile
3737
- 未显式传 `--config` 时,`sk-sp-*` 自动写入并激活 `token-plan`;普通 `sk-*` / `sk-ws-*` 仅在当前为 `token-plan` 时改写并激活 `default`,否则保留当前 Profile;其他格式不自动切换。显式 `--config` 始终优先。`sk-sp-*` 无论写入哪个 Profile 都会携带 Token Plan 的预设默认模型,并在保留已有项的基础上补齐 `api_key_capabilities`,不自动删除任何已有能力。登录仍不得删除其他鉴权域的凭证
3838
- `bl auth login --console` 更新 `access_token` 以及回调携带的 console 作用域字段;若所选 Profile 没有模型 API Key,会要求控制台页面创建并回传一个普通 API Key 与 Base URL 一并保存。该回调结果不重复执行 `/models` 校验,且不会创建或配置 Token Plan
3939
- `bl auth login --open-api ...` 调用 OpenAPI 生成 CLI `access_token`,通过目标 Console 网关的只读 `loginInfo` 校验后,才一并写入 `access_key_id` / `access_key_secret` / `access_token` 和实际 `console_region` / `console_site`;失败不覆盖原凭证、不激活 Profile。一次成功登录同时产生 `openapi` 与 `console` 域凭证

‎packages/commands/src/commands/auth/login-api-key.ts‎

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -76,12 +76,15 @@ const KNOWN_BASE_URLS: Readonly<Record<ApiKeyEndpointKind, readonly string[]>> =
7676
],
7777
};
7878

79+
function usesCompatibleModelsApi(baseUrl: string, kind: ApiKeyEndpointKind): boolean {
80+
const hostname = new URL(normalizeModelBaseUrl(baseUrl)).hostname;
81+
return kind === "token-plan" || hostname.endsWith(".maas.aliyuncs.com");
82+
}
83+
7984
function modelsUrl(baseUrl: string, kind: ApiKeyEndpointKind): string {
80-
const url = new URL(
81-
kind === "token-plan" ? "/compatible-mode/v1/models" : "/api/v1/models",
82-
`${baseUrl}/`,
83-
);
84-
if (kind === "ordinary") {
85+
const compatible = usesCompatibleModelsApi(baseUrl, kind);
86+
const url = new URL(compatible ? "/compatible-mode/v1/models" : "/api/v1/models", `${baseUrl}/`);
87+
if (!compatible) {
8588
url.searchParams.set("page_no", "1");
8689
url.searchParams.set("page_size", "1");
8790
}
@@ -97,6 +100,14 @@ function isTokenPlanBaseUrl(baseUrl: string): boolean {
97100
return new URL(normalizeModelBaseUrl(baseUrl)).hostname.startsWith("token-plan.");
98101
}
99102

103+
function kindsForBaseUrl(
104+
kinds: readonly ApiKeyEndpointKind[],
105+
baseUrl: string,
106+
): ApiKeyEndpointKind[] {
107+
if (!isTokenPlanBaseUrl(baseUrl) || !kinds.includes("token-plan")) return [...kinds];
108+
return ["token-plan", ...kinds.filter((kind) => kind !== "token-plan")];
109+
}
110+
100111
function addCandidate(
101112
candidates: ApiKeyValidationCandidate[],
102113
seenUrls: Set<string>,
@@ -120,13 +131,15 @@ export function apiKeyValidationCandidates(
120131
const seenUrls = new Set<string>();
121132

122133
if (options.explicitBaseUrl) {
123-
for (const kind of kinds) addCandidate(candidates, seenUrls, options.explicitBaseUrl, kind);
134+
for (const kind of kindsForBaseUrl(kinds, options.explicitBaseUrl)) {
135+
addCandidate(candidates, seenUrls, options.explicitBaseUrl, kind);
136+
}
124137
return candidates;
125138
}
126139

127140
if (options.storedBaseUrl) {
128141
const storedIsTokenPlan = isTokenPlanBaseUrl(options.storedBaseUrl);
129-
for (const kind of kinds) {
142+
for (const kind of kindsForBaseUrl(kinds, options.storedBaseUrl)) {
130143
if (apiKeyKind === "unknown" || storedIsTokenPlan === (kind === "token-plan")) {
131144
addCandidate(candidates, seenUrls, options.storedBaseUrl, kind);
132145
}

‎packages/commands/tests/auth-api-key.test.ts‎

Lines changed: 68 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -73,25 +73,82 @@ test("普通 Key 优先使用已保存地址,再尝试 Workspace 专属地址
7373
}),
7474
).toEqual([
7575
"https://stored.example.com/api/v1/models?page_no=1&page_size=1",
76-
"https://llm-test-workspace.cn-beijing.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
77-
"https://llm-test-workspace.ap-southeast-1.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
78-
"https://llm-test-workspace.cn-hongkong.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
79-
"https://llm-test-workspace.ap-northeast-1.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
80-
"https://llm-test-workspace.eu-central-1.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
81-
"https://llm-test-workspace.us-east-1.maas.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
76+
"https://llm-test-workspace.cn-beijing.maas.aliyuncs.com/compatible-mode/v1/models",
77+
"https://llm-test-workspace.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models",
78+
"https://llm-test-workspace.cn-hongkong.maas.aliyuncs.com/compatible-mode/v1/models",
79+
"https://llm-test-workspace.ap-northeast-1.maas.aliyuncs.com/compatible-mode/v1/models",
80+
"https://llm-test-workspace.eu-central-1.maas.aliyuncs.com/compatible-mode/v1/models",
81+
"https://llm-test-workspace.us-east-1.maas.aliyuncs.com/compatible-mode/v1/models",
8282
"https://dashscope.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
8383
"https://dashscope-intl.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
8484
"https://dashscope-us.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
8585
"https://cn-hongkong.dashscope.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
8686
]);
8787
});
8888

89+
test("已保存的 Workspace MaaS 地址使用 Compatible /models", () => {
90+
expect(
91+
candidateUrls("sk-ws-placeholder", {
92+
storedBaseUrl: "https://ws-stored.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
93+
}),
94+
).toEqual([
95+
"https://ws-stored.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models",
96+
"https://dashscope.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
97+
"https://dashscope-intl.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
98+
"https://dashscope-us.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
99+
"https://cn-hongkong.dashscope.aliyuncs.com/api/v1/models?page_no=1&page_size=1",
100+
]);
101+
});
102+
103+
test("Workspace 地域候选先于公共站点选择首个成功地域", async () => {
104+
const originalFetch = httpTransport.fetch;
105+
const workspaceOrigin = "https://llm-test-workspace.ap-southeast-1.maas.aliyuncs.com";
106+
httpTransport.fetch = async (input) => {
107+
const url = requestUrl(input);
108+
if (url === `${workspaceOrigin}/compatible-mode/v1/models`) {
109+
return jsonResponse({ object: "list", data: [{ id: "workspace-model" }] });
110+
}
111+
if (url.startsWith("https://dashscope-intl.aliyuncs.com/")) {
112+
return jsonResponse({ data: [{ id: "public-model" }] });
113+
}
114+
const status = url.includes(".maas.aliyuncs.com/") ? 404 : 401;
115+
return jsonResponse({ code: "Rejected", message: "candidate rejected" }, status);
116+
};
117+
118+
try {
119+
await expect(
120+
validateApiKey({ identity, settings }, "sk-ws-placeholder", {
121+
workspaceId: "llm-test-workspace",
122+
}),
123+
).resolves.toEqual({
124+
baseUrl: workspaceOrigin,
125+
kind: "ordinary",
126+
});
127+
} finally {
128+
httpTransport.fetch = originalFetch;
129+
}
130+
});
131+
89132
test("其他格式 Key 不限制端点类型", () => {
90133
expect(
91134
apiKeyValidationCandidates("custom-key-placeholder", {}).map((candidate) => candidate.kind),
92135
).toEqual(["ordinary", "ordinary", "ordinary", "ordinary", "token-plan", "token-plan"]);
93136
});
94137

138+
test("未知格式 Key 指向 Token Plan host 时保留 Token Plan kind", () => {
139+
expect(
140+
apiKeyValidationCandidates("custom-key-placeholder", {
141+
explicitBaseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com",
142+
}),
143+
).toEqual([
144+
{
145+
baseUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com",
146+
kind: "token-plan",
147+
modelsUrl: "https://token-plan.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models",
148+
},
149+
]);
150+
});
151+
95152
test("显式 Base URL 只校验指定站点,并根据 Key 类型选择 /models 路径", () => {
96153
expect(
97154
candidateUrls("sk-sp-placeholder", {
@@ -103,6 +160,11 @@ test("显式 Base URL 只校验指定站点,并根据 Key 类型选择 /models
103160
explicitBaseUrl: "https://workspace.example.com/compatible-mode/v1",
104161
}),
105162
).toEqual(["https://workspace.example.com/api/v1/models?page_no=1&page_size=1"]);
163+
expect(
164+
candidateUrls("sk-ws-placeholder", {
165+
explicitBaseUrl: "https://ws-explicit.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1",
166+
}),
167+
).toEqual(["https://ws-explicit.ap-southeast-1.maas.aliyuncs.com/compatible-mode/v1/models"]);
106168
});
107169

108170
test("Token Plan 多地区并行校验选择实际接受 Key 的国际站点", async () => {

‎packages/commands/tests/e2e/auth.e2e.test.ts‎

Lines changed: 114 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,14 @@ let modelsOrigin: string;
2727

2828
beforeAll(async () => {
2929
modelsServer = createServer((request, response) => {
30-
if (request.method !== "GET" || !request.url?.includes("/models")) {
30+
const requestUrl = new URL(request.url ?? "/", "http://127.0.0.1");
31+
const isNativeModels =
32+
requestUrl.pathname === "/api/v1/models" &&
33+
requestUrl.searchParams.get("page_no") === "1" &&
34+
requestUrl.searchParams.get("page_size") === "1";
35+
const isCompatibleModels =
36+
requestUrl.pathname === "/compatible-mode/v1/models" && requestUrl.search === "";
37+
if (request.method !== "GET" || (!isNativeModels && !isCompatibleModels)) {
3138
response.writeHead(404, { "content-type": "application/json" });
3239
response.end(JSON.stringify({ code: "NotFound", message: "models route required" }));
3340
return;
@@ -504,6 +511,112 @@ describe("e2e: auth", () => {
504511
expect(config.base_url).toBe(modelsOrigin);
505512
});
506513

514+
test("auth login sk-ws Key 使用显式 Workspace MaaS Compatible /models", async () => {
515+
await withIsolatedHome(async (home, configDir) => {
516+
const workspaceOrigin = "https://ws-explicit.ap-southeast-1.maas.aliyuncs.com";
517+
const originalFetch = httpTransport.fetch;
518+
const requests: string[] = [];
519+
httpTransport.fetch = async (input) => {
520+
const endpoint = new URL(input instanceof Request ? input.url : input);
521+
requests.push(endpoint.toString());
522+
if (endpoint.toString() === `${workspaceOrigin}/compatible-mode/v1/models`) {
523+
return new Response(JSON.stringify({ object: "list", data: [] }), {
524+
headers: { "content-type": "application/json" },
525+
});
526+
}
527+
return new Response(JSON.stringify({ code: "NotFound", message: "route not found" }), {
528+
status: 404,
529+
headers: { "content-type": "application/json" },
530+
});
531+
};
532+
533+
try {
534+
const login = await runCommandInProcess(
535+
AUTH_ROUTES,
536+
[
537+
"auth",
538+
"login",
539+
"--api-key",
540+
"sk-ws-explicit-placeholder",
541+
"--base-url",
542+
workspaceOrigin,
543+
],
544+
{
545+
HOME: home,
546+
BAILIAN_CONFIG_DIR: configDir,
547+
DASHSCOPE_API_KEY: "",
548+
DASHSCOPE_BASE_URL: "",
549+
NODE_OPTIONS: "",
550+
HTTP_PROXY: "",
551+
HTTPS_PROXY: "",
552+
http_proxy: "",
553+
https_proxy: "",
554+
},
555+
);
556+
expect(login.exitCode, login.stderr).toBe(0);
557+
expect(requests).toEqual([`${workspaceOrigin}/compatible-mode/v1/models`]);
558+
expect(readConfigFile()).toMatchObject({
559+
api_key: "sk-ws-explicit-placeholder",
560+
base_url: workspaceOrigin,
561+
});
562+
} finally {
563+
httpTransport.fetch = originalFetch;
564+
}
565+
});
566+
});
567+
568+
test("auth login sk-ws Key 重用已保存的 Workspace MaaS Compatible /models", async () => {
569+
await withIsolatedHome(async (home, configDir) => {
570+
const workspaceOrigin = "https://ws-stored.ap-southeast-1.maas.aliyuncs.com";
571+
await writeConfigFile({ base_url: workspaceOrigin });
572+
const originalFetch = httpTransport.fetch;
573+
const requests: string[] = [];
574+
httpTransport.fetch = async (input) => {
575+
const endpoint = new URL(input instanceof Request ? input.url : input);
576+
requests.push(endpoint.toString());
577+
if (endpoint.toString() === `${workspaceOrigin}/compatible-mode/v1/models`) {
578+
return new Response(JSON.stringify({ object: "list", data: [] }), {
579+
headers: { "content-type": "application/json" },
580+
});
581+
}
582+
return new Response(
583+
JSON.stringify({ code: "InvalidApiKey", message: "invalid key for this endpoint" }),
584+
{
585+
status: 401,
586+
headers: { "content-type": "application/json" },
587+
},
588+
);
589+
};
590+
591+
try {
592+
const login = await runCommandInProcess(
593+
AUTH_ROUTES,
594+
["auth", "login", "--api-key", "sk-ws-stored-placeholder"],
595+
{
596+
HOME: home,
597+
BAILIAN_CONFIG_DIR: configDir,
598+
DASHSCOPE_API_KEY: "",
599+
DASHSCOPE_BASE_URL: "",
600+
NODE_OPTIONS: "",
601+
HTTP_PROXY: "",
602+
HTTPS_PROXY: "",
603+
http_proxy: "",
604+
https_proxy: "",
605+
},
606+
);
607+
expect(login.exitCode, login.stderr).toBe(0);
608+
expect(requests).toContain(`${workspaceOrigin}/compatible-mode/v1/models`);
609+
expect(requests).not.toContain(`${workspaceOrigin}/api/v1/models?page_no=1&page_size=1`);
610+
expect(readConfigFile()).toMatchObject({
611+
api_key: "sk-ws-stored-placeholder",
612+
base_url: workspaceOrigin,
613+
});
614+
} finally {
615+
httpTransport.fetch = originalFetch;
616+
}
617+
});
618+
});
619+
507620
test("auth login --config token-plan 接受 Anthropic SDK Base URL", async () => {
508621
const configDir = makeE2eOutputDir("auth-token-plan-anthropic-base-url");
509622
const login = await runCommandE2e(

0 commit comments

Comments
 (0)