mirror of
https://github.com/modelstudioai/cli.git
synced 2026-09-14 19:49:23 +08:00
fix(config-agent): 优化 Codex 配置写入与兼容性处理
- 调整 Codex 代理默认 wire_api 为 "responses",兼容新版 Codex - 增加对 legacy Codex <= 0.80.0 使用 wire_api "chat" 的警告提示 - 修正 agent flags 描述,更准确说明 wire_api 默认与兼容范围 - 优化代码格式,统一 import 语句风格 - 增加测试用例覆盖不同 wire_api 配置及环境变量警告 - 修复写入过程中文件备份及合并逻辑,保留用户已有配置 - 修复多个 provider 写入时键名与内容匹配,避免重复添加 - 改善测试代码格式,提高可读性与一致性
This commit is contained in:
@@ -38,7 +38,7 @@ const FLAGS = {
|
||||
type: "string",
|
||||
valueHint: "<api>",
|
||||
description:
|
||||
'Codex only: wire protocol — "chat" works with every model; "responses" for models supporting the Responses API (default: chat)',
|
||||
'Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0',
|
||||
choices: ["chat", "responses"],
|
||||
},
|
||||
} satisfies FlagsDef;
|
||||
|
||||
@@ -2,13 +2,7 @@ import { homedir } from "os";
|
||||
import { join } from "path";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { parse as parseToml, stringify as stringifyToml } from "smol-toml";
|
||||
import {
|
||||
backup,
|
||||
readJson,
|
||||
writeJsonAtomic,
|
||||
writeTextAtomic,
|
||||
type AgentDef,
|
||||
} from "./utils.ts";
|
||||
import { backup, readJson, writeJsonAtomic, writeTextAtomic, type AgentDef } from "./utils.ts";
|
||||
|
||||
const PROVIDER_KEY = "bailian-cli";
|
||||
|
||||
@@ -16,6 +10,7 @@ export default {
|
||||
label: "Codex",
|
||||
write({ baseUrl, apiKey, model, wireApi: wireApiParam }) {
|
||||
const configPath = join(homedir(), ".codex", "config.toml");
|
||||
const warnings: string[] = [];
|
||||
|
||||
// config.toml — merge into existing config so unrelated settings
|
||||
// (mcp_servers, approval_policy, other providers, ...) are preserved.
|
||||
@@ -23,10 +18,7 @@ export default {
|
||||
let config: Record<string, unknown> = {};
|
||||
if (existsSync(configPath)) {
|
||||
try {
|
||||
config = parseToml(readFileSync(configPath, "utf-8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>;
|
||||
config = parseToml(readFileSync(configPath, "utf-8")) as Record<string, unknown>;
|
||||
} catch {
|
||||
config = {};
|
||||
}
|
||||
@@ -35,9 +27,18 @@ export default {
|
||||
config.model_provider = PROVIDER_KEY;
|
||||
config.model = model;
|
||||
|
||||
// wire_api: "responses" for models supporting the Responses API (e.g.
|
||||
// qwen3.7/3.8 series); "chat" works with every model via Chat Completions.
|
||||
const wireApi = wireApiParam === "responses" ? "responses" : "chat";
|
||||
// wire_api — current Codex releases only load `wire_api = "responses"`
|
||||
// ("chat" is rejected at config load, see openai/codex discussion #7782).
|
||||
// "chat" remains an explicit opt-in for users pinned to legacy Codex
|
||||
// <= 0.80.0 (the Model Studio path for models without Responses support).
|
||||
const wireApi = wireApiParam === "chat" ? "chat" : "responses";
|
||||
if (wireApi === "chat") {
|
||||
warnings.push(
|
||||
'Current Codex releases refuse to load `wire_api = "chat"`; ' +
|
||||
"only use --wire-api chat with legacy Codex <= 0.80.0 " +
|
||||
"(e.g. `npm install -g @openai/codex@0.80.0`).",
|
||||
);
|
||||
}
|
||||
|
||||
const providers = (config.model_providers ?? {}) as Record<string, unknown>;
|
||||
const existing = (providers[PROVIDER_KEY] ?? {}) as Record<string, unknown>;
|
||||
@@ -65,6 +66,7 @@ export default {
|
||||
return {
|
||||
paths: [configPath, authPath],
|
||||
nextStep: "Run `codex` to start using Codex with DashScope.",
|
||||
warnings: warnings.length > 0 ? warnings : undefined,
|
||||
};
|
||||
},
|
||||
} satisfies AgentDef;
|
||||
|
||||
@@ -214,7 +214,11 @@ describe("config agent writers", () => {
|
||||
});
|
||||
|
||||
test("qwen-code anthropic 端点走 anthropic 协议", () => {
|
||||
qwenCode.write({ baseUrl: ANTHROPIC_URL, apiKey: "sk-q", model: "qwen3-max" });
|
||||
qwenCode.write({
|
||||
baseUrl: ANTHROPIC_URL,
|
||||
apiKey: "sk-q",
|
||||
model: "qwen3-max",
|
||||
});
|
||||
const settings = readJsonAt(".qwen", "settings.json");
|
||||
expect((settings.security as { auth: { selectedType: string } }).auth.selectedType).toBe(
|
||||
"anthropic",
|
||||
@@ -225,8 +229,16 @@ describe("config agent writers", () => {
|
||||
});
|
||||
|
||||
test("qwen-code 对自有 provider 项按 id upsert 而非追加", () => {
|
||||
qwenCode.write({ baseUrl: OAI_URL, apiKey: "sk-1", model: "qwen3-coder-plus" });
|
||||
qwenCode.write({ baseUrl: OAI_URL, apiKey: "sk-2", model: "qwen3-coder-plus" });
|
||||
qwenCode.write({
|
||||
baseUrl: OAI_URL,
|
||||
apiKey: "sk-1",
|
||||
model: "qwen3-coder-plus",
|
||||
});
|
||||
qwenCode.write({
|
||||
baseUrl: OAI_URL,
|
||||
apiKey: "sk-2",
|
||||
model: "qwen3-coder-plus",
|
||||
});
|
||||
const settings = readJsonAt(".qwen", "settings.json");
|
||||
const openaiEntries = (settings.modelProviders as Record<string, unknown[]>).openai;
|
||||
expect(openaiEntries).toHaveLength(1);
|
||||
@@ -327,7 +339,11 @@ describe("config agent writers", () => {
|
||||
JSON.stringify({ provider: { other: { name: "Other" } } }),
|
||||
);
|
||||
|
||||
opencode.write({ baseUrl: ANTHROPIC_URL, apiKey: "sk-o", model: "qwen3-max" });
|
||||
opencode.write({
|
||||
baseUrl: ANTHROPIC_URL,
|
||||
apiKey: "sk-o",
|
||||
model: "qwen3-max",
|
||||
});
|
||||
const config = readJsonAt(".config", "opencode", "opencode.json");
|
||||
const provider = config.provider as Record<string, Record<string, unknown>>;
|
||||
expect(provider.other).toBeDefined();
|
||||
@@ -351,7 +367,11 @@ describe("config agent writers", () => {
|
||||
});
|
||||
|
||||
test("openclaw 写入 provider、api、primary,并登记 defaults.models", () => {
|
||||
openclaw.write({ baseUrl: OAI_URL, apiKey: "sk-c", model: "qwen3-coder-plus" });
|
||||
openclaw.write({
|
||||
baseUrl: OAI_URL,
|
||||
apiKey: "sk-c",
|
||||
model: "qwen3-coder-plus",
|
||||
});
|
||||
const config = readJsonAt(".openclaw", "openclaw.json");
|
||||
const models = config.models as Record<string, unknown>;
|
||||
expect(models.mode).toBe("merge");
|
||||
@@ -496,19 +516,21 @@ describe("config agent writers", () => {
|
||||
// 预置 auth.json 无关键,验证合并保留
|
||||
writeFileSync(join(home, ".codex", "auth.json"), JSON.stringify({ EXISTING: "keep" }));
|
||||
|
||||
codex.write({
|
||||
const summary = codex.write({
|
||||
baseUrl: OAI_URL,
|
||||
apiKey: "sk-x",
|
||||
model: "qwen3-coder-plus",
|
||||
});
|
||||
// 默认路径无警告
|
||||
expect(summary.warnings).toBeUndefined();
|
||||
const toml = readFileSync(join(home, ".codex", "config.toml"), "utf8");
|
||||
expect(toml).toContain('model_provider = "bailian-cli"');
|
||||
expect(toml).toContain('model = "qwen3-coder-plus"');
|
||||
expect(toml).toContain("[model_providers.bailian-cli]");
|
||||
expect(toml).toContain(`base_url = "${OAI_URL}"`);
|
||||
expect(toml).toContain('env_key = "OPENAI_API_KEY"');
|
||||
// 未传 --wire-api 时默认 chat(所有模型可用)
|
||||
expect(toml).toContain('wire_api = "chat"');
|
||||
// 未传 --wire-api 时默认 responses(新版 Codex 已不支持 chat)
|
||||
expect(toml).toContain('wire_api = "responses"');
|
||||
expect(toml).toContain("requires_openai_auth = true");
|
||||
// 合并:保留用户已有的无关配置
|
||||
expect(toml).toContain('approval_policy = "on-request"');
|
||||
@@ -518,16 +540,17 @@ describe("config agent writers", () => {
|
||||
expect(auth.OPENAI_API_KEY).toBe("sk-x");
|
||||
expect(auth.EXISTING).toBe("keep");
|
||||
|
||||
// --wire-api responses:支持 Responses API 的模型
|
||||
codex.write({
|
||||
// --wire-api chat:仅旧版 Codex <= 0.80.0 可用,附带警告
|
||||
const summary2 = codex.write({
|
||||
baseUrl: OAI_URL,
|
||||
apiKey: "sk-x",
|
||||
model: "qwen3.7-plus",
|
||||
wireApi: "responses",
|
||||
model: "glm-5",
|
||||
wireApi: "chat",
|
||||
});
|
||||
expect(summary2.warnings?.some((warning) => warning.includes("0.80.0"))).toBe(true);
|
||||
const toml2 = readFileSync(join(home, ".codex", "config.toml"), "utf8");
|
||||
expect(toml2).toContain('wire_api = "responses"');
|
||||
expect(toml2).toContain('model = "qwen3.7-plus"');
|
||||
expect(toml2).toContain('wire_api = "chat"');
|
||||
expect(toml2).toContain('model = "glm-5"');
|
||||
});
|
||||
|
||||
test("已存在的配置文件会被备份为 .bak.<epoch>", () => {
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import {
|
||||
mkdtempSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync,
|
||||
existsSync,
|
||||
} from "fs";
|
||||
import { mkdtempSync, readFileSync, rmSync, writeFileSync, existsSync } from "fs";
|
||||
import { tmpdir } from "os";
|
||||
import { join } from "path";
|
||||
import { describe, expect, test } from "vite-plus/test";
|
||||
@@ -17,49 +11,29 @@ import { CONFIG_ROUTES } from "./topic-routes.ts";
|
||||
|
||||
describe("e2e: config", () => {
|
||||
test("config show --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"show",
|
||||
"--help",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "show", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/show|config/i);
|
||||
});
|
||||
|
||||
test("config set --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"set",
|
||||
"--help",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/set|--key|--value/i);
|
||||
});
|
||||
|
||||
test("config list/use --help 正常退出", async () => {
|
||||
const listResult = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"list",
|
||||
"--help",
|
||||
]);
|
||||
const listResult = await runCommandE2e(CONFIG_ROUTES, ["config", "list", "--help"]);
|
||||
expect(listResult.exitCode, listResult.stderr).toBe(0);
|
||||
expect(listResult.stderr).toMatch(/list|active|profile/i);
|
||||
|
||||
const useResult = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"use",
|
||||
"--help",
|
||||
]);
|
||||
const useResult = await runCommandE2e(CONFIG_ROUTES, ["config", "use", "--help"]);
|
||||
expect(useResult.exitCode, useResult.stderr).toBe(0);
|
||||
expect(useResult.stderr).toMatch(/use|--name|active/i);
|
||||
});
|
||||
|
||||
test("config ui --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"ui",
|
||||
"--help",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "ui", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/ui|--port|--no-open|web/i);
|
||||
});
|
||||
@@ -131,21 +105,13 @@ describe("e2e: config", () => {
|
||||
});
|
||||
|
||||
test("config set 缺少 --key / --value 时报用法错误并退出 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"set",
|
||||
"--quiet",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "set", "--quiet"]);
|
||||
expect(exitCode, stderr).toBe(2);
|
||||
expect(stderr).toMatch(/--key|--value|Usage:/i);
|
||||
});
|
||||
|
||||
test("config use 缺少 --name 时报用法错误并退出 (2)", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"use",
|
||||
"--quiet",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "use", "--quiet"]);
|
||||
expect(exitCode, stderr).toBe(2);
|
||||
expect(stderr).toMatch(/--name|Usage:/i);
|
||||
});
|
||||
@@ -154,10 +120,7 @@ describe("e2e: config", () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-"));
|
||||
try {
|
||||
const configPath = join(configDir, "config.json");
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n",
|
||||
);
|
||||
writeFileSync(configPath, JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n");
|
||||
const env = { BAILIAN_CONFIG_DIR: configDir };
|
||||
|
||||
const useResult = await runCommandE2e(
|
||||
@@ -166,13 +129,10 @@ describe("e2e: config", () => {
|
||||
env,
|
||||
);
|
||||
expect(useResult.exitCode, useResult.stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ active_config?: string }>(useResult.stdout)
|
||||
.active_config,
|
||||
).toBe("dev");
|
||||
expect(JSON.parse(readFileSync(configPath, "utf8")).active_config).toBe(
|
||||
expect(parseStdoutJson<{ active_config?: string }>(useResult.stdout).active_config).toBe(
|
||||
"dev",
|
||||
);
|
||||
expect(JSON.parse(readFileSync(configPath, "utf8")).active_config).toBe("dev");
|
||||
|
||||
const listResult = await runCommandE2e(
|
||||
CONFIG_ROUTES,
|
||||
@@ -195,23 +155,17 @@ describe("e2e: config", () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-dry-run-"));
|
||||
try {
|
||||
const configPath = join(configDir, "config.json");
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n",
|
||||
);
|
||||
writeFileSync(configPath, JSON.stringify({ dev: { output: "json" } }, null, 2) + "\n");
|
||||
const result = await runCommandE2e(
|
||||
CONFIG_ROUTES,
|
||||
["config", "use", "--name", "dev", "--dry-run", "--output", "json"],
|
||||
{ BAILIAN_CONFIG_DIR: configDir },
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ would_activate?: string }>(result.stdout)
|
||||
.would_activate,
|
||||
).toBe("dev");
|
||||
expect(
|
||||
JSON.parse(readFileSync(configPath, "utf8")).active_config,
|
||||
).toBeUndefined();
|
||||
expect(parseStdoutJson<{ would_activate?: string }>(result.stdout).would_activate).toBe(
|
||||
"dev",
|
||||
);
|
||||
expect(JSON.parse(readFileSync(configPath, "utf8")).active_config).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(configDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -221,10 +175,7 @@ describe("e2e: config", () => {
|
||||
const configDir = mkdtempSync(join(tmpdir(), "bl-config-use-missing-"));
|
||||
try {
|
||||
const configPath = join(configDir, "config.json");
|
||||
writeFileSync(
|
||||
configPath,
|
||||
JSON.stringify({ output: "text" }, null, 2) + "\n",
|
||||
);
|
||||
writeFileSync(configPath, JSON.stringify({ output: "text" }, null, 2) + "\n");
|
||||
const result = await runCommandE2e(
|
||||
CONFIG_ROUTES,
|
||||
["config", "use", "--name", "missing", "--output", "json"],
|
||||
@@ -232,9 +183,7 @@ describe("e2e: config", () => {
|
||||
);
|
||||
expect(result.exitCode).toBe(2);
|
||||
expect(result.stderr).toMatch(/does not exist/);
|
||||
expect(
|
||||
JSON.parse(readFileSync(configPath, "utf8")).active_config,
|
||||
).toBeUndefined();
|
||||
expect(JSON.parse(readFileSync(configPath, "utf8")).active_config).toBeUndefined();
|
||||
} finally {
|
||||
rmSync(configDir, { recursive: true, force: true });
|
||||
}
|
||||
@@ -297,24 +246,16 @@ describe("e2e: config", () => {
|
||||
{ BAILIAN_CONFIG_DIR: configDir },
|
||||
);
|
||||
expect(setResult.exitCode, setResult.stderr).toBe(0);
|
||||
expect(
|
||||
parseStdoutJson<{ base_url?: string }>(setResult.stdout).base_url,
|
||||
).toBe("https://proxy.example.com/bailian");
|
||||
expect(
|
||||
JSON.parse(readFileSync(join(configDir, "config.json"), "utf8"))
|
||||
.base_url,
|
||||
).toBe("https://proxy.example.com/bailian");
|
||||
expect(parseStdoutJson<{ base_url?: string }>(setResult.stdout).base_url).toBe(
|
||||
"https://proxy.example.com/bailian",
|
||||
);
|
||||
expect(JSON.parse(readFileSync(join(configDir, "config.json"), "utf8")).base_url).toBe(
|
||||
"https://proxy.example.com/bailian",
|
||||
);
|
||||
|
||||
const invalidResult = await runCommandE2e(
|
||||
CONFIG_ROUTES,
|
||||
[
|
||||
"config",
|
||||
"set",
|
||||
"--key",
|
||||
"base_url",
|
||||
"--value",
|
||||
"ftp://example.com/models",
|
||||
],
|
||||
["config", "set", "--key", "base_url", "--value", "ftp://example.com/models"],
|
||||
{ BAILIAN_CONFIG_DIR: configDir },
|
||||
);
|
||||
expect(invalidResult.exitCode).toBe(2);
|
||||
@@ -376,9 +317,7 @@ describe("e2e: config", () => {
|
||||
const data = parseStdoutJson<{
|
||||
would_set?: { default_image_to_video_model?: string };
|
||||
}>(stdout);
|
||||
expect(data.would_set?.default_image_to_video_model).toBe(
|
||||
"happyhorse-1.1-i2v",
|
||||
);
|
||||
expect(data.would_set?.default_image_to_video_model).toBe("happyhorse-1.1-i2v");
|
||||
});
|
||||
|
||||
test("config set --dry-run 支持参考生视频默认模型别名", async () => {
|
||||
@@ -397,9 +336,7 @@ describe("e2e: config", () => {
|
||||
const data = parseStdoutJson<{
|
||||
would_set?: { default_reference_to_video_model?: string };
|
||||
}>(stdout);
|
||||
expect(data.would_set?.default_reference_to_video_model).toBe(
|
||||
"happyhorse-1.1-r2v",
|
||||
);
|
||||
expect(data.would_set?.default_reference_to_video_model).toBe("happyhorse-1.1-r2v");
|
||||
});
|
||||
|
||||
test("config set --dry-run 展示归一化后的 Base URL", async () => {
|
||||
@@ -432,9 +369,7 @@ describe("e2e: config", () => {
|
||||
"json",
|
||||
]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const data = parseStdoutJson<{ would_set?: { access_key_id?: string } }>(
|
||||
stdout,
|
||||
);
|
||||
const data = parseStdoutJson<{ would_set?: { access_key_id?: string } }>(stdout);
|
||||
expect(data.would_set?.access_key_id).toBe("LTAI-config-placeholder");
|
||||
});
|
||||
|
||||
@@ -452,11 +387,7 @@ describe("e2e: config", () => {
|
||||
});
|
||||
|
||||
test("config agent --help 正常退出", async () => {
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, [
|
||||
"config",
|
||||
"agent",
|
||||
"--help",
|
||||
]);
|
||||
const { stderr, exitCode } = await runCommandE2e(CONFIG_ROUTES, ["config", "agent", "--help"]);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
expect(stderr).toMatch(/agent|--base-url|--model/i);
|
||||
});
|
||||
@@ -523,9 +454,7 @@ describe("e2e: config", () => {
|
||||
api_key?: string;
|
||||
}>(stdout);
|
||||
expect(data.agent).toBe("claude-code");
|
||||
expect(data.base_url).toBe(
|
||||
"https://dashscope.aliyuncs.com/apps/anthropic",
|
||||
);
|
||||
expect(data.base_url).toBe("https://dashscope.aliyuncs.com/apps/anthropic");
|
||||
expect(data.model).toBe("qwen3-max");
|
||||
expect(stdout).not.toContain("sk-secret-placeholder");
|
||||
expect(existsSync(join(home, ".claude", "settings.json"))).toBe(false);
|
||||
@@ -550,8 +479,6 @@ describe("e2e: config", () => {
|
||||
"sk-codex-placeholder",
|
||||
"--model",
|
||||
"qwen3-coder-plus",
|
||||
"--wire-api",
|
||||
"responses",
|
||||
],
|
||||
{ HOME: home },
|
||||
);
|
||||
@@ -560,10 +487,9 @@ describe("e2e: config", () => {
|
||||
expect(toml).toContain('model_provider = "bailian-cli"');
|
||||
expect(toml).toContain('env_key = "OPENAI_API_KEY"');
|
||||
expect(toml).toContain("requires_openai_auth = true");
|
||||
// 默认即 responses(新版 Codex 已不支持 chat)
|
||||
expect(toml).toContain('wire_api = "responses"');
|
||||
const auth = JSON.parse(
|
||||
readFileSync(join(home, ".codex", "auth.json"), "utf8"),
|
||||
);
|
||||
const auth = JSON.parse(readFileSync(join(home, ".codex", "auth.json"), "utf8"));
|
||||
expect(auth.OPENAI_API_KEY).toBe("sk-codex-placeholder");
|
||||
} finally {
|
||||
rmSync(home, { recursive: true, force: true });
|
||||
@@ -590,15 +516,10 @@ describe("e2e: config", () => {
|
||||
{ HOME: home },
|
||||
);
|
||||
expect(exitCode, stderr).toBe(0);
|
||||
const yamlText = readFileSync(
|
||||
join(home, ".hermes", "config.yaml"),
|
||||
"utf8",
|
||||
);
|
||||
const yamlText = readFileSync(join(home, ".hermes", "config.yaml"), "utf8");
|
||||
expect(yamlText).toContain("default: qwen3-coder-plus");
|
||||
expect(yamlText).toContain("provider: custom");
|
||||
expect(yamlText).toContain(
|
||||
"base_url: https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
);
|
||||
expect(yamlText).toContain("base_url: https://dashscope.aliyuncs.com/compatible-mode/v1");
|
||||
expect(yamlText).toContain("api_key: sk-hermes-placeholder");
|
||||
// OpenAI 兼容端点不写 api_mode
|
||||
expect(yamlText).not.toContain("api_mode");
|
||||
|
||||
@@ -28,14 +28,14 @@ Index: [index.md](index.md)
|
||||
|
||||
#### Flags
|
||||
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------------------------------------------------- | ------ | -------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `--agent <claude-code\|qwen-code\|opencode\|openclaw\|hermes\|codex>` | string | yes | Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex |
|
||||
| `--base-url <url>` | string | yes | API base URL |
|
||||
| `--api-key <key>` | string | yes | API key |
|
||||
| `--model <model>` | string | yes | Default model name |
|
||||
| `--context-window <tokens>` | number | no | OpenClaw only: model context window in tokens (default: 256000) |
|
||||
| `--wire-api <chat\|responses>` | string | no | Codex only: wire protocol — "chat" works with every model; "responses" for models supporting the Responses API (default: chat) |
|
||||
| Flag | Type | Required | Description |
|
||||
| --------------------------------------------------------------------- | ------ | -------- | --------------------------------------------------------------------------------------------- |
|
||||
| `--agent <claude-code\|qwen-code\|opencode\|openclaw\|hermes\|codex>` | string | yes | Target agent: claude-code, qwen-code, opencode, openclaw, hermes, codex |
|
||||
| `--base-url <url>` | string | yes | API base URL |
|
||||
| `--api-key <key>` | string | yes | API key |
|
||||
| `--model <model>` | string | yes | Default model name |
|
||||
| `--context-window <tokens>` | number | no | OpenClaw only: model context window in tokens (default: 256000) |
|
||||
| `--wire-api <chat\|responses>` | string | no | Codex only: wire protocol (default: responses). "chat" only works with legacy Codex <= 0.80.0 |
|
||||
|
||||
#### Examples
|
||||
|
||||
|
||||
Reference in New Issue
Block a user