mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(auth): only apply profile presets on login
This commit is contained in:
@@ -46,8 +46,8 @@ defineCommand({ auth }) → runtime/authStage → ctx.client → command.run(ctx
|
||||
- `resolveModelBaseUrl()` — model base URL;优先级 `--base-url` > `DASHSCOPE_BASE_URL` > config `base_url` > `REGIONS.cn`,返回前统一归一化为 URL origin(仅保留协议、host 和显式端口,去除 path、query、fragment)
|
||||
- `--config` 只选择 config 文件 block,不提升该 block 的字段优先级。对 `auth: "apiKey"` 命令,runtime 会先按叶子命令路径检查所选 Profile 的 `api_key_capabilities`:
|
||||
- `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一显式连接覆盖存在时,完全跳过自动降级,继续走统一的 flag > env > selected config file > 默认值
|
||||
- 配置文件显式声明的 `api_key_capabilities` 优先于内置预设;命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
|
||||
- 字段缺失且 Profile 命中内置套餐预设(当前为 `token-plan`)时,runtime 使用当前预设白名单兼容旧配置,但不自动写回磁盘;字段缺失且没有内置预设时不启用降级
|
||||
- 配置文件显式声明 `api_key_capabilities` 后,命中能力时保留所选 Profile,未命中时仅把 file-backed `api_key` / `base_url` 来源切到顶层 `default`,其他 Settings 仍来自所选 Profile
|
||||
- 字段缺失时不启用降级,包括命中内置套餐预设的 Profile;preset 只在 API Key 登录验证成功后物化写入,升级 preset 需要重新登录
|
||||
- fallback 反馈写 stderr:text 模式输出本地化句子,`--output json` 输出两空格缩进的多行 `warning` 对象;若后续鉴权失败,warning 与多行 `error` 对象以空行分隔,stdout 仍只保留命令结果
|
||||
- 显式 `auth login --config <name>` 在凭证验证并落盘成功后自动激活目标 Profile;未传
|
||||
`--config` 时继续写当前激活项,失败和 dry-run 不切换
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
`auth login --config ...`,凭证验证并落盘成功后自动激活该 Profile。
|
||||
- 激活状态只选择配置 block,不改变字段优先级;字段仍为 flag > env > selected config > 默认值。
|
||||
- API Key capability fallback 是窄例外:命名 Profile 显式配置 `api_key_capabilities` 后,不在白名单中的 `auth: "apiKey"` 叶子命令只把 file 层 `api_key` / `base_url` 切到顶层 `default`;所选 Profile 的其他 settings 和 `active_config` 均不变。如果 `--api-key` / `--base-url` 或 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一提供了更高优先级的模型连接参数,则整个 capability fallback 跳过,file 层也不切换;未显式提供的另一部分继续按 flag > env > 所选 Profile 解析。
|
||||
- 自定义 Profile 是否启用 capability fallback 只看 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。内置 `token-plan` 是当前唯一的名称 preset 兼容例外:旧配置缺少该字段时,runtime 使用当前内置白名单,但不自动写回磁盘。
|
||||
- Profile 是否启用 capability fallback 只看持久化的 `api_key_capabilities`,与名称无关:字段缺失表示关闭策略,`[]` 表示全部 API Key 命令 fallback。runtime 不注入内置 preset;升级内置 Plan Profile 的 preset 需要重新登录。
|
||||
- 对命中内置 preset 的 Profile,API Key 登录验证成功后会把当前 preset 中缺少的 capability 追加落盘,同时保留已有项且不做删除;Console/OpenAPI 登录、自定义 Profile、dry-run 和失败登录均不修改该白名单。
|
||||
- Capability ID 直接使用产品实际叶子命令路径并以 `.` 连接(例如 `video task get` → `video.task.get`);不新增命令元数据。新增或改名后的 API Key 路由未进入白名单时自然 fail closed。
|
||||
- Pipeline 等进程内调用链也要复用统一的 `buildSources()`,避免绕过激活状态。
|
||||
@@ -60,7 +60,7 @@
|
||||
- 旧配置无 `active_config` -> `default`。
|
||||
- 激活命名 Profile 后,无 `--config` 的命令选择该 Profile。
|
||||
- 任意名称 Profile 的叶子路由 capability 命中时使用自身 API Key;未命中或空白名单时使用 `default` API Key;`--api-key` / `--base-url` 和 `DASHSCOPE_API_KEY` / `DASHSCOPE_BASE_URL` 任一覆盖时跳过 fallback。
|
||||
- 旧内置 Plan Profile 缺 capability 字段时使用 preset;显式配置(含 `[]`)覆盖 preset,且不自动迁移写盘。
|
||||
- 旧内置 Plan Profile 缺 capability 字段时不启用 fallback;重新登录后使用并持久化当前 preset,显式配置(含 `[]`)按文件值生效。
|
||||
- 显式命名 `--config` 和 `--config default` 均覆盖激活项且不修改磁盘状态。
|
||||
- 激活不存在的 Profile 失败且不写盘。
|
||||
- 悬空 `active_config` 明确失败。
|
||||
|
||||
@@ -202,14 +202,13 @@ export interface ApiKeyResolutionSourceSelection {
|
||||
}
|
||||
|
||||
/**
|
||||
* Select the file-backed API-key source for a command capability. An explicit
|
||||
* flag or environment API connection override bypasses Profile capability
|
||||
* fallback entirely.
|
||||
* Select the file-backed API-key source for a command capability. Only a
|
||||
* persisted Profile capability list enables fallback. An explicit flag or
|
||||
* environment API connection override bypasses it entirely.
|
||||
*/
|
||||
export function selectApiKeyResolutionSources(
|
||||
sources: ResolutionSources,
|
||||
capability: string,
|
||||
presetCapabilities?: readonly string[],
|
||||
): ApiKeyResolutionSourceSelection {
|
||||
if (
|
||||
sources.flags.apiKey ||
|
||||
@@ -221,7 +220,7 @@ export function selectApiKeyResolutionSources(
|
||||
}
|
||||
if (!sources.configName) return { sources };
|
||||
|
||||
const allowedCapabilities = sources.file.api_key_capabilities ?? presetCapabilities;
|
||||
const allowedCapabilities = sources.file.api_key_capabilities;
|
||||
if (allowedCapabilities === undefined) return { sources };
|
||||
if (allowedCapabilities.includes(capability)) return { sources };
|
||||
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
selectApiKeyResolutionSources,
|
||||
} from "../src/config/loader.ts";
|
||||
import { getConfigPath } from "../src/config/paths.ts";
|
||||
import { getModelProfilePreset } from "../src/config/profile-presets.ts";
|
||||
|
||||
/** 在隔离的临时配置目录里执行,结束后恢复环境。 */
|
||||
async function inTempConfigDir(fn: () => Promise<void>): Promise<void> {
|
||||
@@ -446,42 +445,25 @@ test("显式 API Key 跳过 capability fallback,其他字段仍按既有优先
|
||||
});
|
||||
});
|
||||
|
||||
test("旧 token-plan Profile 缺少持久化字段时使用内置 capability 预设", async () => {
|
||||
test("旧 token-plan Profile 缺少 capability 字段时不启用 fallback", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
await writeConfigFile({ api_key: "sk-default" });
|
||||
await writeConfigFile({ api_key: "sk-token-plan" }, "token-plan");
|
||||
const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} };
|
||||
const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities;
|
||||
|
||||
const supported = selectApiKeyResolutionSources(
|
||||
selectedSources,
|
||||
"video.generate",
|
||||
presetCapabilities,
|
||||
);
|
||||
expect(supported.sources.file.api_key).toBe("sk-token-plan");
|
||||
|
||||
const fallback = selectApiKeyResolutionSources(
|
||||
selectedSources,
|
||||
"search.web",
|
||||
presetCapabilities,
|
||||
);
|
||||
expect(fallback.fallbackFrom).toBe("token-plan");
|
||||
expect(fallback.sources.file.api_key).toBe("sk-default");
|
||||
const selected = selectApiKeyResolutionSources(selectedSources, "search.web");
|
||||
expect(selected.fallbackFrom).toBeUndefined();
|
||||
expect(selected.sources.file.api_key).toBe("sk-token-plan");
|
||||
});
|
||||
});
|
||||
|
||||
test("token-plan 显式空 capability 白名单覆盖内置预设", async () => {
|
||||
test("token-plan 显式空 capability 白名单启用全量 fallback", async () => {
|
||||
await inTempConfigDir(async () => {
|
||||
await writeConfigFile({ api_key: "sk-default" });
|
||||
await writeConfigFile({ api_key: "sk-token-plan", api_key_capabilities: [] }, "token-plan");
|
||||
const selectedSources = { ...buildSources({ config: "token-plan" }), env: {} };
|
||||
const presetCapabilities = getModelProfilePreset("token-plan")?.apiKeyCapabilities;
|
||||
|
||||
const fallback = selectApiKeyResolutionSources(
|
||||
selectedSources,
|
||||
"video.generate",
|
||||
presetCapabilities,
|
||||
);
|
||||
const fallback = selectApiKeyResolutionSources(selectedSources, "video.generate");
|
||||
expect(fallback.fallbackFrom).toBe("token-plan");
|
||||
expect(fallback.sources.file.api_key).toBe("sk-default");
|
||||
});
|
||||
|
||||
@@ -15,7 +15,6 @@ import type {
|
||||
import {
|
||||
Client,
|
||||
DEFAULT_LANGUAGE,
|
||||
getModelProfilePreset,
|
||||
resolveApiKey,
|
||||
resolveConsole,
|
||||
resolveOpenApi,
|
||||
@@ -127,8 +126,7 @@ export const authStage: Middleware = async (ctx, next) => {
|
||||
};
|
||||
if (command.auth === "apiKey") {
|
||||
const capability = ctx.path.join(".");
|
||||
const presetCapabilities = getModelProfilePreset(sources.configName)?.apiKeyCapabilities;
|
||||
const selection = selectApiKeyResolutionSources(sources, capability, presetCapabilities);
|
||||
const selection = selectApiKeyResolutionSources(sources, capability);
|
||||
const apiSources = selection.sources;
|
||||
if (selection.fallbackFrom && !settings.quiet) {
|
||||
writeApiKeyFallbackNotice(ctx, selection.fallbackFrom);
|
||||
|
||||
@@ -139,7 +139,7 @@ test("authStage 为支持的 capability 保留所选 Profile API Key", async ()
|
||||
expect(context.client.exportApiCredential()?.token).toBe("sk-plan");
|
||||
});
|
||||
|
||||
test("authStage 为缺少 capability 字段的旧 token-plan 应用内置 preset", async () => {
|
||||
test("authStage 不为缺少 capability 字段的旧 token-plan 注入内置 preset", async () => {
|
||||
useTempConfigDir();
|
||||
await writeConfigFile({ api_key: "sk-default", base_url: "https://default.example.com" });
|
||||
await writeConfigFile(
|
||||
@@ -147,19 +147,12 @@ test("authStage 为缺少 capability 字段的旧 token-plan 应用内置 preset
|
||||
"token-plan",
|
||||
);
|
||||
|
||||
const supportedContext = makeContext(["image", "generate"], { config: "token-plan" });
|
||||
await runAuth(supportedContext);
|
||||
expect(supportedContext.client.exportApiCredential()).toMatchObject({
|
||||
const context = makeContext(["search", "web"], { config: "token-plan" });
|
||||
await runAuth(context);
|
||||
expect(context.client.exportApiCredential()).toMatchObject({
|
||||
token: "sk-token-plan",
|
||||
baseUrl: "https://token-plan.example.com",
|
||||
});
|
||||
|
||||
const unsupportedContext = makeContext(["search", "web"], { config: "token-plan" });
|
||||
await runAuth(unsupportedContext);
|
||||
expect(unsupportedContext.client.exportApiCredential()).toMatchObject({
|
||||
token: "sk-default",
|
||||
baseUrl: "https://default.example.com",
|
||||
});
|
||||
});
|
||||
|
||||
test("authStage 在 --api-key 覆盖 Profile API Key 时跳过 capability fallback", async () => {
|
||||
|
||||
@@ -98,12 +98,11 @@ A named Profile can limit the API Key commands it supports through `api_key_capa
|
||||
- Unless `--quiet` is used, the CLI reports fallback on stderr. With `--output json`, it emits a
|
||||
structured warning.
|
||||
|
||||
Custom Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback,
|
||||
while `[]` makes every API Key command fall back to `default`.
|
||||
|
||||
The built-in `token-plan` Profile is the compatibility exception: older configs without the field
|
||||
use the current built-in preset. A successful API Key login appends missing preset capabilities
|
||||
without removing existing entries.
|
||||
Profiles opt in only by defining `api_key_capabilities`. A missing field disables fallback, while
|
||||
`[]` makes every API Key command fall back to `default`. This applies to the built-in `token-plan`
|
||||
Profile too: the CLI never injects a newer preset at runtime. A successful Token Plan API Key login
|
||||
appends missing preset capabilities without removing existing entries, so log in again to upgrade
|
||||
the persisted preset.
|
||||
|
||||
```bash
|
||||
bl config set --config company-plan --key api-key-capabilities \
|
||||
@@ -128,7 +127,9 @@ still follow flag > environment > config.
|
||||
|
||||
Activation selects the entire Config for every credential domain, not only model consumption. The only exception is the API Key capability fallback described above. After activating `token-plan`, Token Plan management and Console commands still read their OpenAPI or Console credentials from that Profile. If those credentials remain in `default`, invoke the command with `--config default` or log the corresponding credential domain into `token-plan`.
|
||||
|
||||
The built-in `token-plan` profile defaults to:
|
||||
The built-in `token-plan` preset contains the following values. The CLI materializes them only after
|
||||
a successful `auth login --config token-plan`. It does not replace them at runtime; run the login
|
||||
command again to persist a newer preset:
|
||||
|
||||
- Base URL: `https://token-plan.cn-beijing.maas.aliyuncs.com`
|
||||
- Text model: `qwen3.8-max`
|
||||
|
||||
Reference in New Issue
Block a user