Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 3 additions & 7 deletions INSTALL.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,9 @@ which bl # Windows: where.exe bl
### 推荐:浏览器登录(控制台会话)

1. 执行 `bl auth status --output json`,判断是否已配置。
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`。
3. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整凭据。

### 备选:API Key / Token Plan

- 普通 Key:`bl auth login --api-key <Key>`
- Token Plan:`bl auth login --config token-plan --api-key <Key>`
2. 若未配置,在**用户本机终端**执行 `bl auth login --console`(国际站执行 `bl auth login --console --console-site international`);需要时会自动创建普通 API Key。
3. 如果使用 Token Plan 等订阅计划,不能使用 `--console`,请执行 `bl auth login --api-key <API_KEY>`。
4. 登录成功后执行 `bl auth status --output json` 确认;汇报时只使用 masked 字段,**禁止**回显完整凭据。

### Agent 安全约束

Expand Down
20 changes: 9 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,26 @@ Once installed, just describe your task to your AI Agent — no need to assemble

## Authentication

### API Key
### Console Login (OAuth, Recommended)

Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). It can create an ordinary API key when needed. Token Plan and other subscription plans cannot use `--console`; use the API Key login below.

```bash
bl auth login --api-key sk-xxxxx
bl auth login --console
```

Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).

```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
Use `--console-site international` for the international console.

### Console Login (OAuth)
### API Key

Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
To use an existing ordinary API key or a Token Plan subscription key, run the same command:

```bash
bl auth login --console
bl auth login --api-key <API_KEY>
```

Get an ordinary key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key), or a Token Plan key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).

### Alibaba Cloud OpenAPI AK/SK

Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
Expand Down
20 changes: 9 additions & 11 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,26 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex

## 认证方式

### API Key
### 控制台登录(OAuth,推荐)

大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式,并可在需要时创建普通 API Key。Token Plan 等订阅计划不能使用 `--console`,请使用下面的 API Key 登录方式

```bash
bl auth login --api-key sk-xxxxx
bl auth login --console
```

Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。

```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
国际站请使用 `--console-site international`。

### 控制台登录(OAuth)
### API Key

控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
使用已有的普通 API Key 或 Token Plan 订阅 Key,都执行同一条命令:

```bash
bl auth login --console
bl auth login --api-key <API_KEY>
```

普通 Key 前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取,Token Plan Key 前往 [订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取。

### 阿里云 OpenAPI AK/SK

Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Expand Down
5 changes: 3 additions & 2 deletions docs/agents/auth-change.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx

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

- `bl auth login --api-key ...` 更新 `api_key`;显式 `base_url` 会一并写入,所选命名 Profile 若命中内置套餐预设(当前为 `token-plan`),则在尚未保存 `base_url` 时补写预设地址,并把该预设的默认模型物化写入。API Key 落盘成功后,`api_key_capabilities` 保留已有项并追加当前 preset 中缺少的项,不自动删除任何已有能力;无 preset 的自定义 Profile 不做合并。登录仍不得删除其他鉴权域的凭证
- `bl auth login --console` 只更新 `access_token` 以及回调携带的 console 作用域字段
- `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
- 未显式传 `--config` 时,`sk-sp-*` 自动写入并激活 `token-plan`;普通 `sk-*` / `sk-ws-*` 仅在当前为 `token-plan` 时改写并激活 `default`,否则保留当前 Profile;其他格式不自动切换。显式 `--config` 始终优先。`sk-sp-*` 无论写入哪个 Profile 都会携带 Token Plan 的预设默认模型,并在保留已有项的基础上补齐 `api_key_capabilities`,不自动删除任何已有能力。登录仍不得删除其他鉴权域的凭证
- `bl auth login --console` 更新 `access_token` 以及回调携带的 console 作用域字段;若所选 Profile 没有模型 API Key,会要求控制台页面创建并回传一个普通 API Key 与 Base URL 一并保存。该回调结果不重复执行 `/models` 校验,且不会创建或配置 Token Plan
- `bl auth login --open-api ...` 更新 `access_key_id` / `access_key_secret`,同时会调用 OpenAPI 生成 CLI `access_token` 并一并写入;即一次 `--open-api` 登录同时产生 `openapi` 与 `console` 域凭证
- `bl auth logout --console` 只清 `access_token`
- `bl auth logout --open-api` 只清 `access_key_id` / `access_key_secret` / `security_token`
Expand Down
20 changes: 9 additions & 11 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,28 +124,26 @@ Once installed, just describe your task to your AI Agent — no need to assemble

## Authentication

### API Key
### Console Login (OAuth, Recommended)

Required for most commands. Get your key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key).
Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). It can create an ordinary API key when needed. Token Plan and other subscription plans cannot use `--console`; use the API Key login below.

```bash
bl auth login --api-key sk-xxxxx
bl auth login --console
```

Get or copy your Token Plan API key from the [Token Plan subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).

```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
Use `--console-site international` for the international console.

### Console Login (OAuth)
### API Key

Required for console capability commands (model list, app list, MCP list, workspace, usage queries, rate-limit increases, direct console calls). Opens the Bailian console in your browser to sign in.
To use an existing ordinary API key or a Token Plan subscription key, run the same command:

```bash
bl auth login --console
bl auth login --api-key <API_KEY>
```

Get an ordinary key from the [DashScope Console](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key), or a Token Plan key from the [subscription overview](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview).

### Alibaba Cloud OpenAPI AK/SK

Token Plan seat and member management requires an Alibaba Cloud AccessKey. Get yours from the [RAM Console](https://ram.console.aliyun.com/manage/ak).
Expand Down
20 changes: 9 additions & 11 deletions packages/cli/README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -123,28 +123,26 @@ irm https://bailian.aliyun.com/cli/install.ps1 | iex

## 认证方式

### API Key
### 控制台登录(OAuth,推荐)

大部分命令均需要 API Key。前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取
控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式,并可在需要时创建普通 API Key。Token Plan 等订阅计划不能使用 `--console`,请使用下面的 API Key 登录方式

```bash
bl auth login --api-key sk-xxxxx
bl auth login --console
```

Token Plan 的 API Key 前往 [Token Plan 订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取或复制。

```bash
bl auth login --config token-plan --api-key sk-sp-xxxxx
```
国际站请使用 `--console-site international`。

### 控制台登录(OAuth)
### API Key

控制台能力命令(模型列表、应用列表、MCP 列表、工作空间、用量查询、限流提额、控制台直调)需要使用此登录方式。打开浏览器跳转百炼控制台完成登录。
使用已有的普通 API Key 或 Token Plan 订阅 Key,都执行同一条命令:

```bash
bl auth login --console
bl auth login --api-key <API_KEY>
```

普通 Key 前往 [DashScope 控制台](https://bailian.console.aliyun.com/cn-beijing/?source_channel=key_github&tab=app#/api-key) 获取,Token Plan Key 前往 [订阅详情](https://bailian.console.aliyun.com/cn-beijing?tab=plan#/efm/subscription/overview) 获取。

### 阿里云 OpenAPI AK/SK

Token Plan 的席位与成员管理需要阿里云 AccessKey。前往 [RAM 控制台](https://ram.console.aliyun.com/manage/ak) 获取。
Expand Down
189 changes: 184 additions & 5 deletions packages/commands/src/commands/auth/login-api-key.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { normalizeModelBaseUrl, type AuthPersistPatch, type AuthStore } from "bailian-cli-core";
import {
BailianError,
ExitCode,
getApiKeyLoginKind,
normalizeModelBaseUrl,
requestJson,
type ApiKeyLoginKind,
type AuthPersistPatch,
type AuthStore,
type Identity,
type Settings,
} from "bailian-cli-core";

interface ApiKeyLoginDeps {
interface ApiKeyValidationDeps {
identity: Identity;
settings: Settings;
}

interface ApiKeyLoginDeps extends ApiKeyValidationDeps {
authStore: AuthStore;
}

Expand All @@ -17,10 +33,173 @@ interface ApiKeyLoginProfile {
persistPatch?: AuthPersistPatch;
}

type ApiKeyEndpointKind = Exclude<ApiKeyLoginKind, "unknown">;

interface ApiKeyValidationCandidate {
baseUrl: string;
kind: ApiKeyEndpointKind;
modelsUrl: string;
}

export interface ApiKeyValidationOptions {
explicitBaseUrl?: string;
storedBaseUrl?: string;
workspaceId?: string;
}

export interface ApiKeyValidationResult {
baseUrl: string;
kind: ApiKeyEndpointKind;
}

const API_KEY_VALIDATION_TIMEOUT_SECONDS = 5;
const DEFINITIVE_VALIDATION_FAILURE_STATUSES = new Set([200, 400, 401, 403, 404, 405]);
const ORDINARY_WORKSPACE_REGIONS = [
"cn-beijing",
"ap-southeast-1",
"cn-hongkong",
"ap-northeast-1",
"eu-central-1",
"us-east-1",
] as const;

const KNOWN_BASE_URLS: Readonly<Record<ApiKeyEndpointKind, readonly string[]>> = {
ordinary: [
"https://dashscope.aliyuncs.com",
"https://dashscope-intl.aliyuncs.com",
"https://dashscope-us.aliyuncs.com",
"https://cn-hongkong.dashscope.aliyuncs.com",
],
"token-plan": [
"https://token-plan.cn-beijing.maas.aliyuncs.com",
"https://token-plan.ap-southeast-1.maas.aliyuncs.com",
],
};

function modelsUrl(baseUrl: string, kind: ApiKeyEndpointKind): string {
const url = new URL(
kind === "token-plan" ? "/compatible-mode/v1/models" : "/api/v1/models",
`${baseUrl}/`,
);
if (kind === "ordinary") {
url.searchParams.set("page_no", "1");
url.searchParams.set("page_size", "1");
}
return url.toString();
}

function validationKinds(apiKeyKind: ApiKeyLoginKind): ApiKeyEndpointKind[] {
if (apiKeyKind === "unknown") return ["ordinary", "token-plan"];
return [apiKeyKind];
}

function isTokenPlanBaseUrl(baseUrl: string): boolean {
return new URL(normalizeModelBaseUrl(baseUrl)).hostname.startsWith("token-plan.");
}

function addCandidate(
candidates: ApiKeyValidationCandidate[],
seenUrls: Set<string>,
baseUrl: string,
kind: ApiKeyEndpointKind,
): void {
const normalizedBaseUrl = normalizeModelBaseUrl(baseUrl);
const url = modelsUrl(normalizedBaseUrl, kind);
if (seenUrls.has(url)) return;
seenUrls.add(url);
candidates.push({ baseUrl: normalizedBaseUrl, kind, modelsUrl: url });
}

export function apiKeyValidationCandidates(
key: string,
options: ApiKeyValidationOptions,
): ApiKeyValidationCandidate[] {
const apiKeyKind = getApiKeyLoginKind(key);
const kinds = validationKinds(apiKeyKind);
const candidates: ApiKeyValidationCandidate[] = [];
const seenUrls = new Set<string>();

if (options.explicitBaseUrl) {
for (const kind of kinds) addCandidate(candidates, seenUrls, options.explicitBaseUrl, kind);
return candidates;
}

if (options.storedBaseUrl) {
const storedIsTokenPlan = isTokenPlanBaseUrl(options.storedBaseUrl);
for (const kind of kinds) {
if (apiKeyKind === "unknown" || storedIsTokenPlan === (kind === "token-plan")) {
addCandidate(candidates, seenUrls, options.storedBaseUrl, kind);
}
}
}

if (options.workspaceId && kinds.includes("ordinary")) {
for (const region of ORDINARY_WORKSPACE_REGIONS) {
addCandidate(
candidates,
seenUrls,
`https://${options.workspaceId}.${region}.maas.aliyuncs.com`,
"ordinary",
);
}
}

for (const kind of kinds) {
for (const baseUrl of KNOWN_BASE_URLS[kind]) {
addCandidate(candidates, seenUrls, baseUrl, kind);
}
}
return candidates;
}

function isDefinitiveCandidateFailure(error: unknown): boolean {
if (!(error instanceof BailianError)) return false;
const status = error.api?.httpStatus;
return status !== undefined && DEFINITIVE_VALIDATION_FAILURE_STATUSES.has(status);
}

/** Validate a key without consuming model quota and return the first matching endpoint. */
export async function validateApiKey(
deps: ApiKeyValidationDeps,
key: string,
options: ApiKeyValidationOptions,
): Promise<ApiKeyValidationResult> {
const candidates = apiKeyValidationCandidates(key, options);
const results = await Promise.all(
candidates.map(async (candidate) => {
try {
await requestJson<unknown>(deps, {
url: candidate.modelsUrl,
headers: { Authorization: `Bearer ${key}` },
timeout: Math.min(deps.settings.timeout, API_KEY_VALIDATION_TIMEOUT_SECONDS),
});
return { candidate };
} catch (error) {
return { candidate, error };
}
}),
);

const success = results.find((result) => result.error === undefined);
if (success) return { baseUrl: success.candidate.baseUrl, kind: success.candidate.kind };

const inconclusive = results.find((result) => !isDefinitiveCandidateFailure(result.error));
if (inconclusive?.error !== undefined) throw inconclusive.error;

const explicitHint = options.explicitBaseUrl
? "Check that the API key belongs to this Base URL."
: "For a workspace-specific or custom endpoint, retry with --base-url <url>.";
throw new BailianError(
"API key validation failed: the key was not accepted by any supported model endpoint.",
ExitCode.AUTH,
explicitHint,
);
}

/**
* Persist an API key (and optional profile defaults) without a live model probe.
* Login is credential storage; connectivity is verified on the first API command.
* A former chat/completions smoke test conflated quota/model-access 403s with bad keys.
* Persist an already-trusted API key and optional profile defaults.
* Console login uses this directly because its callback already supplies the key and endpoint;
* direct API-key login must call validateApiKey first.
*/
export async function persistApiKey(
deps: ApiKeyLoginDeps,
Expand Down
Loading