Skip to content

Commit 74c902f

Browse files
committed
feat(qwen-code): add warnings for environment variable conflicts and enhance model entry handling
- Introduced warnings for cases where the environment variable overrides the settings file. - Updated model entry handling to prevent overwriting existing entries with the same ID. - Enhanced the display name for bailian-cli model entries to include the model name. - Updated WriteSummary interface to include warnings for non-fatal issues.
1 parent fac2b2d commit 74c902f

4 files changed

Lines changed: 135 additions & 18 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ export default defineCommand({
6666
emitBare(`${agentDef.label} configured successfully.`);
6767
for (const path of summary.paths) emitBare(` Written: ${path}`);
6868
emitBare(` ${summary.nextStep}`);
69+
for (const warning of summary.warnings ?? []) {
70+
process.stderr.write(`Warning: ${warning}\n`);
71+
}
6972
}
7073
},
7174
});

packages/commands/src/commands/config/agent/writers/qwen-code.ts

Lines changed: 62 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,58 +4,106 @@ import { backup, readJson, writeJsonAtomic, isAnthropicEndpoint, type AgentDef }
44

55
const ENV_KEY = "BAILIAN_CLI_API_KEY";
66

7+
function displayName(model: string): string {
8+
return `${model} (bailian-cli)`;
9+
}
10+
11+
/** Entries we previously wrote, or still own via envKey. */
12+
function isBailianCliEntry(entry: Record<string, unknown>): boolean {
13+
return entry.envKey === ENV_KEY || entry.name === "bailian-cli";
14+
}
15+
716
/**
817
* Qwen Code keys `modelProviders` and `security.auth.selectedType` by the SDK
918
* protocol (an AuthType string), not by a free-form provider id — the runtime
10-
* resolver indexes credentials/defaults by protocol. The `bailian-cli` brand
11-
* therefore lives in the model entry `name` and the env var name.
19+
* resolver indexes credentials/defaults by protocol. Ownership is tracked via
20+
* `envKey` (`BAILIAN_CLI_API_KEY`) and a display `name` suffix `(bailian-cli)`.
21+
*
22+
* Qwen Code does not support duplicate model `id`s (only the first loads), so
23+
* we must never overwrite a pre-existing Token Plan / third-party entry that
24+
* shares the same id.
1225
*/
1326
export default {
1427
label: "Qwen Code",
1528
write({ baseUrl, apiKey, model }) {
1629
const settingsPath = join(homedir(), ".qwen", "settings.json");
1730
const protocol = isAnthropicEndpoint(baseUrl) ? "anthropic" : "openai";
31+
const warnings: string[] = [];
1832

1933
backup(settingsPath);
2034
const settings = readJson(settingsPath);
2135

2236
// env — API key read by the provider entry's envKey.
37+
// Qwen Code treats settings.json `env` as lowest priority; a process/shell
38+
// value for the same key wins and can make the first launch fail.
2339
const env = (settings.env ?? {}) as Record<string, string>;
2440
env[ENV_KEY] = apiKey;
2541
settings.env = env;
2642

27-
// modelProviders[<protocol>] — upsert the bailian-cli model entry.
43+
const processEnvValue = process.env[ENV_KEY];
44+
if (processEnvValue !== undefined && processEnvValue !== apiKey) {
45+
warnings.push(
46+
`Shell/environment ${ENV_KEY} is set and overrides settings.json. ` +
47+
`Unset it (e.g. \`unset ${ENV_KEY}\`) so the key written here takes effect.`,
48+
);
49+
}
50+
51+
// modelProviders[<protocol>] — upsert only bailian-cli-owned entries.
2852
const providers = (settings.modelProviders ?? {}) as Record<
2953
string,
3054
Array<Record<string, unknown>>
3155
>;
3256
const entries = (providers[protocol] ?? []) as Array<Record<string, unknown>>;
33-
const existing = entries.find(
34-
(entry) => entry.id === model && (entry.baseUrl ?? "") === baseUrl,
35-
);
36-
if (existing) {
37-
existing.name = "bailian-cli";
38-
existing.baseUrl = baseUrl;
39-
existing.envKey = ENV_KEY;
57+
const owned = entries.find((entry) => isBailianCliEntry(entry) && entry.id === model);
58+
const conflicting = entries.find((entry) => !isBailianCliEntry(entry) && entry.id === model);
59+
60+
if (owned) {
61+
owned.name = displayName(model);
62+
owned.baseUrl = baseUrl;
63+
owned.envKey = ENV_KEY;
64+
} else if (conflicting) {
65+
const existingName =
66+
typeof conflicting.name === "string" && conflicting.name.length > 0
67+
? conflicting.name
68+
: String(conflicting.id);
69+
warnings.push(
70+
`Model id "${model}" already exists as "${existingName}"; left unchanged ` +
71+
`(Qwen Code loads only the first entry per id). Remove or rename that ` +
72+
`entry if you want bailian-cli to own this model.`,
73+
);
4074
} else {
41-
entries.push({ id: model, name: "bailian-cli", baseUrl, envKey: ENV_KEY });
75+
entries.push({
76+
id: model,
77+
name: displayName(model),
78+
baseUrl,
79+
envKey: ENV_KEY,
80+
});
4281
}
4382
providers[protocol] = entries;
4483
settings.modelProviders = providers;
4584

46-
// security.auth — select the protocol and carry the OpenAI-compatible creds.
85+
// security.auth — select protocol only. Prefer modelProviders + envKey for
86+
// credentials; apiKey/baseUrl on auth are deprecated in Qwen Code.
4787
const security = (settings.security ?? {}) as Record<string, unknown>;
48-
security.auth = { selectedType: protocol, apiKey, baseUrl };
88+
const previousAuth =
89+
security.auth && typeof security.auth === "object"
90+
? (security.auth as Record<string, unknown>)
91+
: {};
92+
security.auth = { ...previousAuth, selectedType: protocol };
93+
// Drop deprecated inline creds so they cannot disagree with envKey lookup.
94+
delete (security.auth as Record<string, unknown>).apiKey;
95+
delete (security.auth as Record<string, unknown>).baseUrl;
4996
settings.security = security;
5097

51-
// model — active model, disambiguated by baseUrl.
98+
// model — active model id (disambiguated by baseUrl when supported).
5299
settings.model = { name: model, baseUrl };
53100

54101
writeJsonAtomic(settingsPath, settings);
55102

56103
return {
57104
paths: [settingsPath],
58105
nextStep: "Run `qwen` to start using Qwen Code with DashScope.",
106+
warnings: warnings.length > 0 ? warnings : undefined,
59107
};
60108
},
61109
} satisfies AgentDef;

packages/commands/src/commands/config/agent/writers/utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export interface WriteParams {
1212
export interface WriteSummary {
1313
paths: string[];
1414
nextStep: string;
15+
/** Non-fatal issues the command should surface to the user. */
16+
warnings?: string[];
1517
}
1618

1719
/** An agent configuration writer: a human label plus a `write` that applies it. */

packages/commands/tests/config-agent-writers.test.ts

Lines changed: 68 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -75,14 +75,14 @@ describe("config agent writers", () => {
7575
const settings = readJsonAt(".qwen", "settings.json");
7676
const security = settings.security as { auth: Record<string, string> };
7777
expect(security.auth.selectedType).toBe("openai");
78-
expect(security.auth.apiKey).toBe("sk-q");
79-
expect(security.auth.baseUrl).toBe(OAI_URL);
78+
expect(security.auth.apiKey).toBeUndefined();
79+
expect(security.auth.baseUrl).toBeUndefined();
8080
expect((settings.env as Record<string, string>).BAILIAN_CLI_API_KEY).toBe("sk-q");
8181
expect((settings.model as Record<string, string>).name).toBe("qwen3-coder-plus");
8282
const providers = settings.modelProviders as Record<string, Array<Record<string, unknown>>>;
8383
expect(providers.openai[0]).toMatchObject({
8484
id: "qwen3-coder-plus",
85-
name: "bailian-cli",
85+
name: "qwen3-coder-plus (bailian-cli)",
8686
baseUrl: OAI_URL,
8787
envKey: "BAILIAN_CLI_API_KEY",
8888
});
@@ -99,12 +99,76 @@ describe("config agent writers", () => {
9999
expect(providers.openai).toBeUndefined();
100100
});
101101

102-
test("qwen-code 对相同 id+baseUrl 的 provider 项做 upsert 而非追加", () => {
102+
test("qwen-code 对自有 provider 项按 id upsert 而非追加", () => {
103103
qwenCode.write({ baseUrl: OAI_URL, apiKey: "sk-1", model: "qwen3-coder-plus" });
104104
qwenCode.write({ baseUrl: OAI_URL, apiKey: "sk-2", model: "qwen3-coder-plus" });
105105
const settings = readJsonAt(".qwen", "settings.json");
106106
const openaiEntries = (settings.modelProviders as Record<string, unknown[]>).openai;
107107
expect(openaiEntries).toHaveLength(1);
108+
expect((settings.env as Record<string, string>).BAILIAN_CLI_API_KEY).toBe("sk-2");
109+
});
110+
111+
test("qwen-code 不劫持已有 Token Plan 同 id 条目的 name/envKey", () => {
112+
mkdirSync(join(home, ".qwen"), { recursive: true });
113+
writeFileSync(
114+
join(home, ".qwen", "settings.json"),
115+
JSON.stringify({
116+
env: { BAILIAN_TOKEN_PLAN_API_KEY: "sk-token-plan" },
117+
modelProviders: {
118+
openai: [
119+
{
120+
id: "qwen3.8-max-preview",
121+
name: "[Token Plan 个人版] qwen3.8-max-preview",
122+
baseUrl: "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1",
123+
envKey: "BAILIAN_TOKEN_PLAN_API_KEY",
124+
generationConfig: { extra_body: { enable_thinking: true } },
125+
},
126+
],
127+
},
128+
}),
129+
);
130+
131+
const tokenPlanUrl = "https://token-plan.cn-beijing.maas.aliyuncs.com/compatible-mode/v1";
132+
const summary = qwenCode.write({
133+
baseUrl: tokenPlanUrl,
134+
apiKey: "sk-bailian",
135+
model: "qwen3.8-max-preview",
136+
});
137+
138+
const settings = readJsonAt(".qwen", "settings.json");
139+
const openaiEntries = (
140+
settings.modelProviders as Record<string, Array<Record<string, unknown>>>
141+
).openai;
142+
expect(openaiEntries).toHaveLength(1);
143+
expect(openaiEntries[0]).toMatchObject({
144+
id: "qwen3.8-max-preview",
145+
name: "[Token Plan 个人版] qwen3.8-max-preview",
146+
envKey: "BAILIAN_TOKEN_PLAN_API_KEY",
147+
generationConfig: { extra_body: { enable_thinking: true } },
148+
});
149+
expect((settings.env as Record<string, string>).BAILIAN_TOKEN_PLAN_API_KEY).toBe(
150+
"sk-token-plan",
151+
);
152+
expect((settings.env as Record<string, string>).BAILIAN_CLI_API_KEY).toBe("sk-bailian");
153+
expect(summary.warnings?.some((warning) => warning.includes("already exists"))).toBe(true);
154+
});
155+
156+
test("qwen-code 在进程环境变量覆盖 settings.env 时给出警告", () => {
157+
const previous = process.env.BAILIAN_CLI_API_KEY;
158+
process.env.BAILIAN_CLI_API_KEY = "sk-from-shell";
159+
try {
160+
const summary = qwenCode.write({
161+
baseUrl: OAI_URL,
162+
apiKey: "sk-from-settings",
163+
model: "qwen3-coder-plus",
164+
});
165+
expect(summary.warnings?.some((warning) => warning.includes("overrides settings.json"))).toBe(
166+
true,
167+
);
168+
} finally {
169+
if (previous === undefined) delete process.env.BAILIAN_CLI_API_KEY;
170+
else process.env.BAILIAN_CLI_API_KEY = previous;
171+
}
108172
});
109173

110174
test("opencode 按端点选 npm,含 setCacheKey,合并保留其它 provider", () => {

0 commit comments

Comments
 (0)