Skip to content

Commit afa43a4

Browse files
authored
Merge pull request #128 from modelstudioai/feat/config-agent-fix
feat(config-agent): align agent writers, add --key/--region, prepare 1.12.0
2 parents 3b7993e + bbf45a5 commit afa43a4

21 files changed

Lines changed: 1324 additions & 139 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
66

77
[中文版](CHANGELOG.zh.md) · [README](README.md) · [Contributing](CONTRIBUTING.md)
88

9+
## [1.12.0] - 2026-07-28
10+
11+
### Added
12+
13+
- **`bl config agent --key` / `--region`** — run commands generated by the Model Studio web console as-is: `--key` accepts the console's encoded API key and decodes it locally (use instead of `--api-key`), and `--region` derives the Token Plan endpoint from a region name (use instead of `--base-url`).
14+
- **`bl config agent --context-window`** — set the context window written to the OpenClaw configuration (default 256000).
15+
- **`bl config agent --wire-api`** — choose the wire protocol written to the Codex configuration; `chat` is kept for legacy Codex 0.80.0 and earlier (a warning is shown).
16+
17+
### Changed
18+
19+
- `bl config agent` for Codex now writes `wire_api = "responses"` by default, matching current Codex releases that no longer accept `chat`.
20+
- `bl config agent` for Qwen Code now writes the `DASHSCOPE_API_KEY` environment variable instead of `BAILIAN_CLI_API_KEY`.
21+
22+
### Fixed
23+
24+
- `bl config agent` configurations now match each agent's official format: Claude Code honors `CLAUDE_CONFIG_DIR` and removes a stale `ANTHROPIC_API_KEY`; Qwen Code uses the v3 settings schema and writes credentials so a system-level `OPENAI_API_KEY` no longer takes precedence; OpenCode accepts JSONC config files (comments and trailing commas); OpenClaw registers the primary model in the model allowlist with complete cost metadata; Hermes uses the official flat `model.*` layout; Codex writes the official `env_key` with an `auth.json` fallback.
25+
- `bl config agent` now preserves existing user configuration when writing: it merges instead of overwriting, avoids duplicate provider entries, and keeps custom display names.
26+
927
## [1.11.2] - 2026-07-28
1028

1129
### Changed

CHANGELOG.zh.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,24 @@
66

77
[English](CHANGELOG.md) · [README](README.zh.md) · [参与贡献](CONTRIBUTING.zh.md)
88

9+
## [1.12.0] - 2026-07-28
10+
11+
### 新增
12+
13+
- **`bl config agent --key` / `--region`** —— 百炼控制台生成的命令可直接运行:`--key` 接收控制台编码后的 API Key 并在本地解码(与 `--api-key` 二选一);`--region` 根据地域名自动派生 Token Plan 接入地址(与 `--base-url` 二选一)。
14+
- **`bl config agent --context-window`** —— 设置写入 OpenClaw 配置的上下文窗口大小(默认 256000)。
15+
- **`bl config agent --wire-api`** —— 选择写入 Codex 配置的通信协议;`chat` 仅保留给 Codex 0.80.0 及更早版本(会显示警告)。
16+
17+
### 变更
18+
19+
- `bl config agent` 配置 Codex 时默认写入 `wire_api = "responses"`,以适配已不再支持 `chat` 的新版 Codex。
20+
- `bl config agent` 配置 Qwen Code 时改用 `DASHSCOPE_API_KEY` 环境变量,不再使用 `BAILIAN_CLI_API_KEY`
21+
22+
### 修复
23+
24+
- `bl config agent` 写入的配置现已与各 Agent 官方格式对齐:Claude Code 尊重 `CLAUDE_CONFIG_DIR` 并清理残留的 `ANTHROPIC_API_KEY`;Qwen Code 采用 v3 配置 schema 并正确写入凭证,避免被系统级 `OPENAI_API_KEY` 干扰;OpenCode 支持带注释和尾部逗号的 JSONC 配置文件;OpenClaw 会将主模型注册进模型白名单并补齐计费元数据;Hermes 改用官方扁平 `model.*` 结构;Codex 写入官方 `env_key` 并支持 `auth.json` 兜底。
25+
- `bl config agent` 写入配置时现会保留用户已有配置:合并而非覆盖,避免重复添加 provider 条目,并保留用户自定义的显示名。
26+
927
## [1.11.2] - 2026-07-28
1028

1129
### 变更

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli",
3-
"version": "1.11.2",
3+
"version": "1.12.0",
44
"description": "CLI for Aliyun Model Studio (DashScope) AI Platform.",
55
"keywords": [
66
"agent",

packages/commands/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "bailian-cli-commands",
3-
"version": "1.11.2",
3+
"version": "1.12.0",
44
"description": "Command library for bailian-cli products (knowledge, memory, media, …). See https://www.npmjs.com/package/bailian-cli for usage.",
55
"homepage": "https://bailian.console.aliyun.com/cli",
66
"bugs": {
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import { BailianError, ExitCode } from "bailian-cli-core";
2+
3+
/**
4+
* Decoder for the obfuscated API key ("o1_…") produced by the Model Studio web
5+
* console. Ported verbatim from the frontend `encodeTokenPlanKey` counterpart:
6+
* token = "o1_" + salt(6) + feistel-obfuscated payload + crc32 checksum(6),
7+
* all over a 65-character alphabet. Pure logic, no dependencies; the CLI only
8+
* ever needs the decode direction.
9+
*/
10+
11+
const TOKEN_PREFIX = "o1_";
12+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_.";
13+
const ALPHABET_SIZE = ALPHABET.length;
14+
const ALPHABET_INDEX = new Map(ALPHABET.split("").map((character, index) => [character, index]));
15+
const KEY_PATTERN = /^[A-Za-z0-9._-]+$/;
16+
const SALT_LENGTH = 6;
17+
const CHECKSUM_LENGTH = 6;
18+
const FEISTEL_ROUNDS = 8;
19+
20+
function invalidCredential(): BailianError {
21+
return new BailianError(
22+
"Invalid obfuscated API key.",
23+
ExitCode.USAGE,
24+
'--key expects the obfuscated key copied from the web console (starts with "o1_").',
25+
);
26+
}
27+
28+
function toDigits(value: string): number[] {
29+
const digits: number[] = [];
30+
for (const character of value) {
31+
const digit = ALPHABET_INDEX.get(character);
32+
if (digit === undefined) throw invalidCredential();
33+
digits.push(digit);
34+
}
35+
return digits;
36+
}
37+
38+
function fromDigits(digits: number[]): string {
39+
return digits.map((digit) => ALPHABET[digit]).join("");
40+
}
41+
42+
function mixState(state: number, value: number): number {
43+
return Math.imul((state ^ value) >>> 0, 0x01000193) >>> 0;
44+
}
45+
46+
function nextState(state: number): number {
47+
let next = state >>> 0;
48+
next ^= next << 13;
49+
next ^= next >>> 17;
50+
next ^= next << 5;
51+
return next >>> 0;
52+
}
53+
54+
function createRoundMask(right: number[], salt: string, round: number, length: number): number[] {
55+
let state = (0x811c9dc5 ^ Math.imul(round + 1, 0x9e3779b1)) >>> 0;
56+
57+
state = mixState(state, right.length);
58+
state = mixState(state, length);
59+
for (const character of salt) {
60+
state = mixState(state, (ALPHABET_INDEX.get(character) ?? -1) + 1);
61+
}
62+
for (const digit of right) {
63+
state = mixState(state, digit + 1);
64+
}
65+
66+
state ^= state >>> 16;
67+
state = Math.imul(state, 0x85ebca6b) >>> 0;
68+
state ^= state >>> 13;
69+
state = Math.imul(state, 0xc2b2ae35) >>> 0;
70+
state ^= state >>> 16;
71+
state = state >>> 0 || 0x6d2b79f5;
72+
73+
const mask: number[] = [];
74+
for (let index = 0; index < length; index += 1) {
75+
state = (state + Math.imul(index + 1, 0x9e3779b1)) >>> 0;
76+
state = nextState(state);
77+
mask.push(state % ALPHABET_SIZE);
78+
}
79+
return mask;
80+
}
81+
82+
function deobfuscatePayload(payload: string, salt: string): string {
83+
const digits = toDigits(payload);
84+
const midpoint = Math.floor(digits.length / 2);
85+
let left = digits.slice(0, midpoint);
86+
let right = digits.slice(midpoint);
87+
88+
for (let round = FEISTEL_ROUNDS - 1; round >= 0; round -= 1) {
89+
const previousRight = left;
90+
const mask = createRoundMask(previousRight, salt, round, right.length);
91+
const previousLeft = right.map(
92+
(digit, index) => (digit - mask[index] + ALPHABET_SIZE) % ALPHABET_SIZE,
93+
);
94+
left = previousLeft;
95+
right = previousRight;
96+
}
97+
98+
return fromDigits([...left, ...right]);
99+
}
100+
101+
function crc32(value: string): number {
102+
let checksum = 0xffffffff;
103+
for (let index = 0; index < value.length; index += 1) {
104+
checksum ^= value.charCodeAt(index);
105+
for (let bit = 0; bit < 8; bit += 1) {
106+
const mask = -(checksum & 1);
107+
checksum = (checksum >>> 1) ^ (0xedb88320 & mask);
108+
}
109+
}
110+
return (checksum ^ 0xffffffff) >>> 0;
111+
}
112+
113+
function encodeBase65Number(value: number, length: number): string {
114+
let remaining = value >>> 0;
115+
const encoded = Array<string>(length).fill(ALPHABET[0]);
116+
117+
for (let index = length - 1; index >= 0; index -= 1) {
118+
encoded[index] = ALPHABET[remaining % ALPHABET_SIZE];
119+
remaining = Math.floor(remaining / ALPHABET_SIZE);
120+
}
121+
if (remaining !== 0) throw invalidCredential();
122+
return encoded.join("");
123+
}
124+
125+
function validateSalt(salt: string): void {
126+
if (salt.length !== SALT_LENGTH || !KEY_PATTERN.test(salt)) {
127+
throw invalidCredential();
128+
}
129+
}
130+
131+
/** Decode an "o1_…" obfuscated token back into the plain API key. */
132+
export function decodeTokenPlanKey(token: string): string {
133+
const minimumLength = TOKEN_PREFIX.length + SALT_LENGTH + CHECKSUM_LENGTH + 1;
134+
if (token.length < minimumLength || !token.startsWith(TOKEN_PREFIX)) {
135+
throw invalidCredential();
136+
}
137+
138+
const body = token.slice(TOKEN_PREFIX.length);
139+
if (!KEY_PATTERN.test(body)) throw invalidCredential();
140+
141+
const salt = body.slice(0, SALT_LENGTH);
142+
const payload = body.slice(SALT_LENGTH, -CHECKSUM_LENGTH);
143+
const checksum = body.slice(-CHECKSUM_LENGTH);
144+
validateSalt(salt);
145+
if (!payload) throw invalidCredential();
146+
147+
const apiKey = deobfuscatePayload(payload, salt);
148+
if (!KEY_PATTERN.test(apiKey)) throw invalidCredential();
149+
150+
const expectedChecksum = encodeBase65Number(crc32(apiKey), CHECKSUM_LENGTH);
151+
if (checksum !== expectedChecksum) throw invalidCredential();
152+
return apiKey;
153+
}

packages/commands/src/commands/config/agent/index.ts

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { platform } from "os";
22
import { defineCommand, detectOutputFormat, maskToken, type FlagsDef } from "bailian-cli-core";
33
import { emitResult, emitBare } from "bailian-cli-runtime";
44
import { AGENTS, VALID_AGENT_NAMES, type WriteParams } from "./writers.ts";
5+
import { decodeTokenPlanKey } from "./decode-key.ts";
6+
import { resolveRegionBaseUrl } from "./writers/utils.ts";
57

68
const FLAGS = {
79
agent: {
@@ -11,30 +13,76 @@ const FLAGS = {
1113
required: true,
1214
choices: VALID_AGENT_NAMES,
1315
},
14-
baseUrl: { type: "string", valueHint: "<url>", description: "API base URL", required: true },
15-
apiKey: { type: "string", valueHint: "<key>", description: "API key", required: true },
16+
baseUrl: {
17+
type: "string",
18+
valueHint: "<url>",
19+
description: "API base URL",
20+
},
21+
region: {
22+
type: "string",
23+
valueHint: "<region>",
24+
description:
25+
"Model Studio region (e.g. cn-beijing, ap-southeast-1); converted into --base-url. Token Plan only",
26+
},
27+
apiKey: {
28+
type: "string",
29+
valueHint: "<key>",
30+
description: "API key",
31+
},
32+
key: {
33+
type: "string",
34+
valueHint: "<encoded>",
35+
description:
36+
'Obfuscated API key from the web console (starts with "o1_"); decoded into --api-key',
37+
},
1638
model: {
1739
type: "string",
1840
valueHint: "<model>",
1941
description: "Default model name",
2042
required: true,
2143
},
44+
contextWindow: {
45+
type: "number",
46+
valueHint: "<tokens>",
47+
description: "OpenClaw only: model context window in tokens (default: 256000)",
48+
},
49+
wireApi: {
50+
type: "string",
51+
valueHint: "<api>",
52+
description:
53+
'Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0',
54+
choices: ["chat", "responses"],
55+
},
2256
} satisfies FlagsDef;
2357

2458
export default defineCommand({
2559
description: "Configure a coding agent to use DashScope API",
2660
auth: "none",
27-
usageArgs: "--agent <name> --base-url <url> --api-key <key> --model <model>",
61+
usageArgs:
62+
"--agent <name> (--base-url <url> | --region <region>) (--api-key <key> | --key <encoded>) --model <model>",
2863
flags: FLAGS,
2964
exampleArgs: [
3065
"--agent claude-code --base-url https://dashscope.aliyuncs.com/apps/anthropic --api-key sk-xxxxx --model qwen3-max",
3166
"--agent qwen-code --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus",
3267
"--agent codex --base-url https://dashscope.aliyuncs.com/compatible-mode/v1 --api-key sk-xxxxx --model qwen3-coder-plus",
3368
],
69+
validate(flags) {
70+
if (!flags.baseUrl && !flags.region) return "one of --base-url or --region is required";
71+
if (flags.baseUrl && flags.region) return "--base-url and --region are mutually exclusive";
72+
if (!flags.apiKey && !flags.key) return "one of --api-key or --key is required";
73+
if (flags.apiKey && flags.key) return "--api-key and --key are mutually exclusive";
74+
return undefined;
75+
},
3476
async run(ctx) {
3577
const { settings, flags } = ctx;
3678
const agentName = flags.agent;
37-
const { baseUrl, apiKey, model } = flags;
79+
const { model, contextWindow, wireApi } = flags;
80+
// --region is a Token Plan convenience: convert it into a base URL and use
81+
// it exactly as --base-url would be.
82+
const baseUrl = flags.region ? resolveRegionBaseUrl(flags.region) : flags.baseUrl!;
83+
// --key carries the web console's obfuscated form; decode it up front so
84+
// even --dry-run validates the token.
85+
const apiKey = flags.key ? decodeTokenPlanKey(flags.key) : flags.apiKey!;
3886
const agentDef = AGENTS[agentName];
3987
const format = detectOutputFormat(settings.output);
4088

@@ -59,13 +107,22 @@ export default defineCommand({
59107
return;
60108
}
61109

62-
const params: WriteParams = { baseUrl, apiKey, model };
110+
const params: WriteParams = {
111+
baseUrl,
112+
apiKey,
113+
model,
114+
contextWindow,
115+
wireApi,
116+
};
63117
const summary = agentDef.write(params);
64118

65119
if (!settings.quiet) {
66120
emitBare(`${agentDef.label} configured successfully.`);
67121
for (const path of summary.paths) emitBare(` Written: ${path}`);
68122
emitBare(` ${summary.nextStep}`);
123+
for (const warning of summary.warnings ?? []) {
124+
process.stderr.write(`Warning: ${warning}\n`);
125+
}
69126
}
70127
},
71128
});

0 commit comments

Comments
 (0)