@lazyingart/agintiflow 0.14.1 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -46,13 +46,15 @@ aginti --list-profiles
46
46
  aginti --sandbox-status
47
47
  ```
48
48
 
49
- On first interactive use, if no DeepSeek key is detected, `aginti` asks you to paste it and saves it to the project-local ignored file `.aginti/.env` with `0600` permissions. You can also set it explicitly:
49
+ On first interactive use, if no main model key is detected, `aginti` opens an auth wizard. Use Up/Down to choose DeepSeek, OpenAI, or Qwen, paste the key, and press Enter to save it to the project-local ignored file `.aginti/.env` with `0600` permissions. The wizard then offers the optional auxiliary image key; press Esc to skip. You can rerun it even when keys already exist:
50
50
 
51
51
  ```bash
52
- aginti login deepseek
53
- # inside chat, use /login or /auth
52
+ aginti auth
53
+ aginti auth openai
54
+ # inside chat, use /login or /auth for the same wizard
54
55
  # or non-interactively:
55
56
  printf '%s' "$DEEPSEEK_API_KEY" | aginti keys set deepseek --stdin
57
+ printf '%s' "$QWEN_API_KEY" | aginti keys set qwen --stdin
56
58
 
57
59
  # optional image-generation auxiliary skill:
58
60
  aginti login grsai
@@ -269,7 +271,7 @@ Defaults:
269
271
  | `smart` | DeepSeek | Fast for normal tasks, pro for complex tasks | `AGENT_ROUTING_MODE=smart` |
270
272
  | `fast` | DeepSeek | `deepseek-v4-flash` | `DEEPSEEK_FAST_MODEL` |
271
273
  | `complex` | DeepSeek | `deepseek-v4-pro` | `DEEPSEEK_PRO_MODEL` |
272
- | `manual` | DeepSeek/OpenAI | user supplied | `AGENT_PROVIDER`, `LLM_MODEL` |
274
+ | `manual` | DeepSeek/OpenAI/Qwen | user supplied | `AGENT_PROVIDER`, `LLM_MODEL` |
273
275
 
274
276
  Provider credentials:
275
277
 
@@ -277,6 +279,7 @@ Provider credentials:
277
279
  | --- | --- | --- |
278
280
  | OpenAI | `OPENAI_API_KEY` | `https://api.openai.com/v1` |
279
281
  | DeepSeek | `DEEPSEEK_API_KEY` | `https://api.deepseek.com/v1` |
282
+ | Qwen | `QWEN_API_KEY` | `QWEN_BASE_URL` or DashScope compatible mode |
280
283
 
281
284
  Project-local credentials can be stored without committing secrets:
282
285
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.14.1",
3
+ "version": "0.15.0",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a resumable Playwright website-control agent with OpenAI-compatible tool calling.",
6
6
  "license": "Apache-2.0",
@@ -42,6 +42,7 @@
42
42
  "scripts/setup-agent-toolchain-docker.sh",
43
43
  "scripts/real-deepseek-capabilities.js",
44
44
  "scripts/smoke-auxiliary-tools.js",
45
+ "scripts/smoke-auth.js",
45
46
  "scripts/smoke-cli-chat.js",
46
47
  "scripts/smoke-coding-tools.js",
47
48
  "scripts/smoke-capabilities.js",
@@ -65,13 +66,14 @@
65
66
  "setup:toolchain-docker": "scripts/setup-agent-toolchain-docker.sh",
66
67
  "smoke:coding-tools": "node scripts/smoke-coding-tools.js",
67
68
  "smoke:auxiliary-tools": "node scripts/smoke-auxiliary-tools.js",
69
+ "smoke:auth": "node scripts/smoke-auth.js",
68
70
  "smoke:cli-chat": "node scripts/smoke-cli-chat.js",
69
71
  "smoke:skills": "node scripts/smoke-skills.js",
70
72
  "smoke:toolchain-docker": "node scripts/smoke-toolchain-docker.js",
71
73
  "smoke:inbox": "node scripts/smoke-inbox.js",
72
74
  "smoke:web-api": "node scripts/smoke-web-api.js",
73
75
  "real:deepseek": "node scripts/real-deepseek-capabilities.js",
74
- "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
76
+ "test": "npm run check && npm run smoke:web-api && npm run smoke:coding-tools && npm run smoke:auxiliary-tools && npm run smoke:auth && npm run smoke:capabilities && npm run smoke:skills && npm run smoke:cli-chat && npm run smoke:inbox",
75
77
  "pack:dry-run": "npm pack --dry-run",
76
78
  "smoke:capabilities": "node scripts/smoke-capabilities.js"
77
79
  },
package/public/app.js CHANGED
@@ -22,9 +22,9 @@ const translations = {
22
22
  projectStatusTitle: "Project folder",
23
23
  setupTitle: "Provider setup",
24
24
  setupHelp:
25
- "DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.",
25
+ "DeepSeek/OpenAI/Qwen keys are missing. Use mock mode, export an env var, or save a project-local model key.",
26
26
  setupEnvHelp:
27
- "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
27
+ "Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, LLM_API_KEY, and optional GRSAI for image generation. Mock mode remains available for local tests.",
28
28
  setupProviderLabel: "Provider",
29
29
  setupKeyLabel: "API key",
30
30
  saveKeyButton: "Save local key",
@@ -714,6 +714,7 @@ const ariaLabelNodes = [...document.querySelectorAll("[data-i18n-aria-label]")];
714
714
  const defaults = {
715
715
  openai: "gpt-5.4-mini",
716
716
  deepseek: "deepseek-v4-flash",
717
+ qwen: "qwen-plus",
717
718
  mock: "mock-agent",
718
719
  };
719
720
 
@@ -783,12 +784,14 @@ function renderKeyStatus(status = lastKeyStatus) {
783
784
  if (!status) return;
784
785
  keyStatusEl.textContent = `${t("keysLabel")}: OpenAI ${
785
786
  status.openai ? t("availableLabel") : t("missingLabel")
786
- } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · GRS AI ${
787
+ } · DeepSeek ${status.deepseek ? t("availableLabel") : t("missingLabel")} · Qwen ${
788
+ status.qwen ? t("availableLabel") : t("missingLabel")
789
+ } · GRS AI ${
787
790
  status.grsai ? t("availableLabel") : t("missingLabel")
788
791
  } · ${t("mockLabel")} ${
789
792
  status.mock ? t("availableLabel") : t("missingLabel")
790
793
  }`;
791
- if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek);
794
+ if (setupCardEl) setupCardEl.hidden = Boolean(status.openai || status.deepseek || status.qwen);
792
795
  }
793
796
 
794
797
  function renderProjectStatus(info = projectInfo) {
@@ -2278,6 +2281,7 @@ providerField.addEventListener("change", () => {
2278
2281
  !modelField.value.trim() ||
2279
2282
  modelField.value === defaults.openai ||
2280
2283
  modelField.value === defaults.deepseek ||
2284
+ modelField.value === defaults.qwen ||
2281
2285
  modelField.value === defaults.mock
2282
2286
  ) {
2283
2287
  modelField.value = defaults[providerField.value] || "";
@@ -2513,6 +2517,7 @@ async function loadConfig() {
2513
2517
  taskProfiles = data.taskProfiles || [];
2514
2518
  projectInfo = data.project || null;
2515
2519
  defaults.openai = data.defaults?.openai?.model || defaults.openai;
2520
+ defaults.qwen = data.defaults?.qwen?.model || defaults.qwen;
2516
2521
  defaults.deepseek = routingPresets.fast?.model || data.defaults?.deepseek?.model || defaults.deepseek;
2517
2522
  defaults.mock = data.defaults?.mock?.model || defaults.mock;
2518
2523
 
package/public/index.html CHANGED
@@ -48,7 +48,7 @@
48
48
  DeepSeek/OpenAI keys are missing. Use mock mode, export an env var, or save a project-local DeepSeek key.
49
49
  </p>
50
50
  <p class="subtle" data-i18n="setupEnvHelp">
51
- Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
51
+ Env vars: DEEPSEEK_API_KEY, OPENAI_API_KEY, QWEN_API_KEY, or LLM_API_KEY. Mock mode remains available for local tests.
52
52
  </p>
53
53
  </div>
54
54
  <div class="grid">
@@ -57,6 +57,7 @@
57
57
  <select id="setup-provider">
58
58
  <option value="deepseek">DeepSeek</option>
59
59
  <option value="openai">OpenAI</option>
60
+ <option value="qwen">Qwen</option>
60
61
  <option value="grsai">GRS AI image</option>
61
62
  </select>
62
63
  </label>
@@ -88,6 +89,7 @@
88
89
  <select id="provider" name="provider">
89
90
  <option value="deepseek">DeepSeek</option>
90
91
  <option value="openai">OpenAI</option>
92
+ <option value="qwen">Qwen</option>
91
93
  <option value="mock" data-i18n="mockProviderOption">Mock local</option>
92
94
  </select>
93
95
  </label>
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { normalizeAuthProvider } from "../src/auth-onboarding.js";
8
+ import { getProviderDefaults } from "../src/model-routing.js";
9
+ import { providerKeyStatus, setProviderKey } from "../src/project.js";
10
+
11
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
12
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-auth-"));
13
+
14
+ function assert(condition, message) {
15
+ if (!condition) throw new Error(message);
16
+ }
17
+
18
+ async function runCli(args, stdin = "") {
19
+ return new Promise((resolve, reject) => {
20
+ const child = spawn(process.execPath, [path.join(repoRoot, "bin/aginti-cli.js"), ...args], {
21
+ cwd: tempRoot,
22
+ stdio: ["pipe", "pipe", "pipe"],
23
+ env: {
24
+ ...process.env,
25
+ AGINTIFLOW_RUNTIME_DIR: "",
26
+ },
27
+ });
28
+ let stdout = "";
29
+ let stderr = "";
30
+ const timer = setTimeout(() => {
31
+ child.kill("SIGTERM");
32
+ reject(new Error("auth smoke command timed out"));
33
+ }, 12000);
34
+ child.stdout.on("data", (chunk) => {
35
+ stdout += String(chunk);
36
+ });
37
+ child.stderr.on("data", (chunk) => {
38
+ stderr += String(chunk);
39
+ });
40
+ child.on("error", (error) => {
41
+ clearTimeout(timer);
42
+ reject(error);
43
+ });
44
+ child.on("close", (code) => {
45
+ clearTimeout(timer);
46
+ if (code === 0) resolve(stdout);
47
+ else reject(new Error(`auth smoke command failed ${code}\n${stdout}\n${stderr}`));
48
+ });
49
+ child.stdin.end(stdin);
50
+ });
51
+ }
52
+
53
+ try {
54
+ assert(normalizeAuthProvider("auxilliary") === "grsai", "auxilliary alias did not normalize to grsai");
55
+ assert(normalizeAuthProvider("qwen") === "qwen", "qwen provider did not normalize");
56
+ const qwenDefaults = getProviderDefaults("qwen");
57
+ assert(qwenDefaults.provider === "qwen" && qwenDefaults.model, "qwen provider defaults are not available");
58
+
59
+ await setProviderKey(tempRoot, "qwen", "test-qwen-key");
60
+ let status = providerKeyStatus(tempRoot);
61
+ assert(status.qwen, "qwen key status was not detected");
62
+ assert(status.envVars.qwen.includes("QWEN_API_KEY"), "qwen env var name was not reported");
63
+
64
+ await runCli(["keys", "set", "openai", "--stdin"], "test-openai-key");
65
+ await runCli(["keys", "set", "grsai", "--stdin"], "test-grsai-key");
66
+ status = providerKeyStatus(tempRoot);
67
+ assert(status.openai && status.grsai && status.qwen, "stored auth keys were not detected");
68
+
69
+ const keysOutput = await runCli(["keys", "status"]);
70
+ assert(keysOutput.includes("qwen=available"), "keys status did not include qwen");
71
+ assert(!keysOutput.includes("test-openai-key") && !keysOutput.includes("test-qwen-key"), "keys status leaked a raw key");
72
+
73
+ console.log(
74
+ JSON.stringify(
75
+ {
76
+ ok: true,
77
+ projectRoot: tempRoot,
78
+ checks: ["normalize-auth-provider", "qwen-defaults", "qwen-key-status", "cli-key-status-redacted"],
79
+ },
80
+ null,
81
+ 2
82
+ )
83
+ );
84
+ } finally {
85
+ await fs.rm(tempRoot, { recursive: true, force: true });
86
+ }
@@ -37,6 +37,7 @@ try {
37
37
  assert(capabilities.project.instructionsPresent, "capabilities did not report AGINTI.md");
38
38
  assert(capabilities.project.sharedSessionFolder, "capabilities did not report shared session folder");
39
39
  assert(capabilities.keys?.mock === true, "capabilities did not report mock availability");
40
+ assert(typeof capabilities.keys?.qwen === "boolean", "capabilities did not report qwen key status");
40
41
  assert(
41
42
  capabilities.checks.some((check) => check.name === "npm-prefix-test-policy" && check.ok),
42
43
  "npm --prefix test policy is not allowed"
@@ -87,6 +87,7 @@ try {
87
87
 
88
88
  const keyStatus = await fetchJson("/api/keys/status");
89
89
  if (typeof keyStatus.keyStatus?.deepseek !== "boolean") throw new Error("key status endpoint is invalid");
90
+ if (typeof keyStatus.keyStatus?.qwen !== "boolean") throw new Error("qwen key status is missing");
90
91
  if ("localEnvPath" in keyStatus.keyStatus) throw new Error("key status leaked a local env path");
91
92
  const capabilities = await fetchJson("/api/capabilities");
92
93
  if (capabilities.project?.root !== runtimeDir || !Array.isArray(capabilities.checks)) {
@@ -106,6 +107,14 @@ try {
106
107
  if (!savedKey.ok || !savedKey.keyStatus?.deepseek || "apiKey" in savedKey || "key" in savedKey) {
107
108
  throw new Error("local key save endpoint returned invalid or sensitive data");
108
109
  }
110
+ const savedQwenKey = await fetchJson("/api/keys/qwen", {
111
+ method: "POST",
112
+ headers: { "Content-Type": "application/json" },
113
+ body: JSON.stringify({ apiKey: "test-qwen-key-not-real" }),
114
+ });
115
+ if (!savedQwenKey.ok || !savedQwenKey.keyStatus?.qwen || "apiKey" in savedQwenKey || "key" in savedQwenKey) {
116
+ throw new Error("qwen local key save endpoint returned invalid or sensitive data");
117
+ }
109
118
 
110
119
  const status = await fetchJson("/api/sandbox/status");
111
120
  if (!status.status?.workspaceReadable) throw new Error("sandbox status did not report a readable workspace");
@@ -1,8 +1,60 @@
1
1
  import readline from "node:readline/promises";
2
+ import { emitKeypressEvents } from "node:readline";
2
3
  import { stdin as input, stdout as output } from "node:process";
3
4
  import { Writable } from "node:stream";
4
5
  import { providerKeyStatus, setProviderKey } from "./project.js";
5
6
 
7
+ export const MAIN_AUTH_PROVIDERS = [
8
+ {
9
+ id: "deepseek",
10
+ label: "DeepSeek",
11
+ keyName: "DEEPSEEK_API_KEY",
12
+ description: "default fast/pro route",
13
+ },
14
+ {
15
+ id: "openai",
16
+ label: "OpenAI",
17
+ keyName: "OPENAI_API_KEY",
18
+ description: "OpenAI-compatible fallback",
19
+ },
20
+ {
21
+ id: "qwen",
22
+ label: "Qwen",
23
+ keyName: "QWEN_API_KEY",
24
+ description: "Qwen OpenAI-compatible route",
25
+ },
26
+ ];
27
+
28
+ const AUXILIARY_AUTH_PROVIDER = {
29
+ id: "grsai",
30
+ label: "GRS AI / Nano Banana",
31
+ keyName: "GRSAI",
32
+ description: "optional image generation",
33
+ };
34
+
35
+ const AUTH_ALIASES = {
36
+ auxiliary: "grsai",
37
+ auxilliary: "grsai",
38
+ image: "grsai",
39
+ imagegen: "grsai",
40
+ grs: "grsai",
41
+ grsai: "grsai",
42
+ deepseek: "deepseek",
43
+ ds: "deepseek",
44
+ openai: "openai",
45
+ qwen: "qwen",
46
+ };
47
+
48
+ export function normalizeAuthProvider(provider = "", fallback = "deepseek") {
49
+ const normalized = AUTH_ALIASES[String(provider || "").trim().toLowerCase()] || String(provider || "").trim().toLowerCase();
50
+ return ["deepseek", "openai", "qwen", "grsai"].includes(normalized) ? normalized : fallback;
51
+ }
52
+
53
+ function providerLabel(provider = "") {
54
+ const match = [...MAIN_AUTH_PROVIDERS, AUXILIARY_AUTH_PROVIDER].find((item) => item.id === provider);
55
+ return match?.label || provider;
56
+ }
57
+
6
58
  class MutedWritable extends Writable {
7
59
  constructor(target) {
8
60
  super();
@@ -16,8 +68,55 @@ class MutedWritable extends Writable {
16
68
  }
17
69
  }
18
70
 
19
- export async function promptHidden(promptText) {
20
- if (!input.isTTY || !output.isTTY) return "";
71
+ export async function promptSecret(promptText, { allowEscape = true } = {}) {
72
+ if (!input.isTTY || !output.isTTY) return { value: "", skipped: true };
73
+
74
+ if (typeof input.setRawMode === "function") {
75
+ return new Promise((resolve, reject) => {
76
+ emitKeypressEvents(input);
77
+ const wasRaw = Boolean(input.isRaw);
78
+ let value = "";
79
+
80
+ const cleanup = () => {
81
+ input.off("keypress", handler);
82
+ if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
83
+ input.pause();
84
+ };
85
+
86
+ const finish = (result) => {
87
+ cleanup();
88
+ output.write("\n");
89
+ resolve(result);
90
+ };
91
+
92
+ const handler = (str = "", key = {}) => {
93
+ if (key.ctrl && key.name === "c") {
94
+ cleanup();
95
+ reject(Object.assign(new Error("Interrupted by ctrl-c."), { name: "AbortError", code: "ABORT_ERR" }));
96
+ return;
97
+ }
98
+ if (allowEscape && key.name === "escape") {
99
+ finish({ value: "", skipped: true });
100
+ return;
101
+ }
102
+ if (key.name === "return" || key.name === "enter" || key.sequence === "\r" || str === "\r") {
103
+ finish({ value: value.trim(), skipped: !value.trim() });
104
+ return;
105
+ }
106
+ if (key.name === "backspace" || key.name === "delete") {
107
+ value = value.slice(0, -1);
108
+ return;
109
+ }
110
+ if (key.ctrl || key.meta || key.sequence?.startsWith("\x1b")) return;
111
+ if (str) value += str.replace(/\r|\n/g, "");
112
+ };
113
+
114
+ output.write(promptText);
115
+ input.resume();
116
+ input.setRawMode(true);
117
+ input.on("keypress", handler);
118
+ });
119
+ }
21
120
 
22
121
  const mutedOutput = new MutedWritable(output);
23
122
  const rl = readline.createInterface({
@@ -31,19 +130,132 @@ export async function promptHidden(promptText) {
31
130
  mutedOutput.muted = true;
32
131
  const value = await rl.question("");
33
132
  output.write("\n");
34
- return String(value || "").trim();
133
+ const text = String(value || "").trim();
134
+ return { value: text, skipped: !text };
35
135
  } finally {
36
136
  mutedOutput.muted = false;
37
137
  rl.close();
38
138
  }
39
139
  }
40
140
 
141
+ export async function promptHidden(promptText) {
142
+ const result = await promptSecret(promptText, { allowEscape: true });
143
+ return typeof result === "string" ? result : result.value || "";
144
+ }
145
+
146
+ function renderProviderPicker({ providers, selected, title, status }) {
147
+ const lines = [
148
+ `\n${title}`,
149
+ "Use Up/Down to choose, Enter to confirm, Esc to go back/skip.",
150
+ `Current key status: ${status}`,
151
+ "",
152
+ ...providers.map((provider, index) => {
153
+ const cursor = index === selected ? ">" : " ";
154
+ return `${cursor} ${provider.label.padEnd(10)} ${provider.keyName.padEnd(16)} ${provider.description}`;
155
+ }),
156
+ ];
157
+ return lines.join("\n");
158
+ }
159
+
160
+ export async function chooseAuthProvider({
161
+ providers = MAIN_AUTH_PROVIDERS,
162
+ title = "Choose main model API key",
163
+ initialProvider = "deepseek",
164
+ projectRoot = process.cwd(),
165
+ } = {}) {
166
+ if (!input.isTTY || !output.isTTY || typeof input.setRawMode !== "function") {
167
+ return normalizeAuthProvider(initialProvider, providers[0]?.id || "deepseek");
168
+ }
169
+
170
+ return new Promise((resolve, reject) => {
171
+ emitKeypressEvents(input);
172
+ const wasRaw = Boolean(input.isRaw);
173
+ const status = providerKeyStatus(projectRoot);
174
+ let selected = Math.max(
175
+ providers.findIndex((provider) => provider.id === normalizeAuthProvider(initialProvider, providers[0]?.id)),
176
+ 0
177
+ );
178
+ let renderedLines = 0;
179
+
180
+ const cleanup = () => {
181
+ input.off("keypress", handler);
182
+ if (typeof input.setRawMode === "function") input.setRawMode(wasRaw);
183
+ input.pause();
184
+ output.write("\x1b[?25h");
185
+ };
186
+
187
+ const clear = () => {
188
+ if (renderedLines <= 0) return;
189
+ output.write(`\x1b[${renderedLines - 1}A`);
190
+ for (let index = 0; index < renderedLines; index += 1) {
191
+ output.write("\r\x1b[2K");
192
+ if (index < renderedLines - 1) output.write("\x1b[1B");
193
+ }
194
+ output.write(`\x1b[${renderedLines - 1}A\r`);
195
+ };
196
+
197
+ const render = () => {
198
+ clear();
199
+ const providerStatus = providers
200
+ .map((provider) => `${provider.label}=${status[provider.id] ? "available" : "missing"}`)
201
+ .join(" · ");
202
+ const text = renderProviderPicker({
203
+ providers,
204
+ selected,
205
+ title,
206
+ status: providerStatus,
207
+ });
208
+ const lines = text.split("\n");
209
+ renderedLines = lines.length;
210
+ output.write(`\x1b[?25l${text}`);
211
+ };
212
+
213
+ const finish = (value) => {
214
+ clear();
215
+ cleanup();
216
+ resolve(value);
217
+ };
218
+
219
+ const handler = (_str = "", key = {}) => {
220
+ if (key.ctrl && key.name === "c") {
221
+ clear();
222
+ cleanup();
223
+ reject(Object.assign(new Error("Interrupted by ctrl-c."), { name: "AbortError", code: "ABORT_ERR" }));
224
+ return;
225
+ }
226
+ if (key.name === "escape") {
227
+ finish("");
228
+ return;
229
+ }
230
+ if (key.name === "up") {
231
+ selected = (selected - 1 + providers.length) % providers.length;
232
+ render();
233
+ return;
234
+ }
235
+ if (key.name === "down") {
236
+ selected = (selected + 1) % providers.length;
237
+ render();
238
+ return;
239
+ }
240
+ if (key.name === "return" || key.name === "enter" || key.sequence === "\r") {
241
+ finish(providers[selected].id);
242
+ }
243
+ };
244
+
245
+ input.resume();
246
+ input.setRawMode(true);
247
+ input.on("keypress", handler);
248
+ render();
249
+ });
250
+ }
251
+
41
252
  export function shouldPromptForDeepSeek(args = {}, projectRoot = process.cwd()) {
42
253
  const provider = String(args.provider || "").toLowerCase();
43
- if (provider === "mock" || provider === "openai") return false;
254
+ if (provider === "mock" || provider === "openai" || provider === "qwen") return false;
44
255
  if (process.env.AGINTIFLOW_NO_AUTH_PROMPT === "1") return false;
45
256
  if (!input.isTTY || !output.isTTY) return false;
46
- return !providerKeyStatus(projectRoot).deepseek;
257
+ const status = providerKeyStatus(projectRoot);
258
+ return !status.deepseek && !status.openai && !status.qwen;
47
259
  }
48
260
 
49
261
  export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), options = {}) {
@@ -60,3 +272,60 @@ export async function promptAndSaveDeepSeekKey(projectRoot = process.cwd(), opti
60
272
  path: result.path,
61
273
  };
62
274
  }
275
+
276
+ export async function runAuthWizard(projectRoot = process.cwd(), options = {}) {
277
+ const status = providerKeyStatus(projectRoot);
278
+ const initialProvider = normalizeAuthProvider(options.provider || options.initialProvider || "deepseek", "deepseek");
279
+ const directProvider =
280
+ options.provider && ["deepseek", "openai", "qwen", "grsai"].includes(normalizeAuthProvider(options.provider, ""))
281
+ ? normalizeAuthProvider(options.provider)
282
+ : "";
283
+ const mainProvider =
284
+ directProvider === "grsai"
285
+ ? ""
286
+ : directProvider ||
287
+ (await chooseAuthProvider({
288
+ projectRoot,
289
+ initialProvider,
290
+ title: "Choose the main model API key to save",
291
+ }));
292
+
293
+ const saved = [];
294
+ const skipped = [];
295
+
296
+ if (mainProvider) {
297
+ const current = status[mainProvider] ? "currently available; paste a new key to replace, or Esc to keep existing" : "missing";
298
+ const prompt = `${providerLabel(mainProvider)} main API key (${current}) [hidden]: `;
299
+ const secret = await promptSecret(prompt, { allowEscape: true });
300
+ if (secret.value) {
301
+ const result = await setProviderKey(projectRoot, mainProvider, secret.value);
302
+ saved.push(result);
303
+ } else {
304
+ skipped.push({ provider: mainProvider, reason: secret.skipped ? "skipped" : "empty" });
305
+ }
306
+ } else {
307
+ skipped.push({ provider: "main", reason: "skipped" });
308
+ }
309
+
310
+ if (options.includeAuxiliary !== false && directProvider !== "grsai") {
311
+ const auxStatus = providerKeyStatus(projectRoot);
312
+ const current = auxStatus.grsai ? "currently available; paste a new key to replace, or Esc to skip" : "optional; paste key or Esc to skip";
313
+ const secret = await promptSecret(`${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key (${current}) [hidden]: `, {
314
+ allowEscape: true,
315
+ });
316
+ if (secret.value) {
317
+ const result = await setProviderKey(projectRoot, "grsai", secret.value);
318
+ saved.push(result);
319
+ } else {
320
+ skipped.push({ provider: "grsai", reason: "skipped" });
321
+ }
322
+ } else if (directProvider === "grsai") {
323
+ const secret = await promptSecret(`${AUXILIARY_AUTH_PROVIDER.label} auxiliary image key [hidden]: `, {
324
+ allowEscape: true,
325
+ });
326
+ if (secret.value) saved.push(await setProviderKey(projectRoot, "grsai", secret.value));
327
+ else skipped.push({ provider: "grsai", reason: "skipped" });
328
+ }
329
+
330
+ return { saved, skipped };
331
+ }
@@ -147,6 +147,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
147
147
  capability("docker", Boolean(dockerStatus?.dockerAvailable), dockerStatus || {}),
148
148
  capability("deepseek-key", keyStatus.deepseek, { envVars: keyStatus.envVars.deepseek }),
149
149
  capability("openai-key", keyStatus.openai, { envVars: keyStatus.envVars.openai }),
150
+ capability("qwen-key", keyStatus.qwen, { envVars: keyStatus.envVars.qwen }),
150
151
  capability("grsai-key", keyStatus.grsai, {
151
152
  envVars: keyStatus.envVars.grsai,
152
153
  setup: "Optional for image generation. Run `aginti login grsai` or use `/auxilliary grsai` in chat.",
@@ -202,6 +203,7 @@ export async function buildCapabilityReport(projectRoot, packageVersion, config)
202
203
  keys: {
203
204
  deepseek: keyStatus.deepseek,
204
205
  openai: keyStatus.openai,
206
+ qwen: keyStatus.qwen,
205
207
  grsai: keyStatus.grsai,
206
208
  mock: true,
207
209
  localEnv: keyStatus.localEnv,
@@ -266,7 +268,9 @@ export function printCapabilityReport(report) {
266
268
  console.log(
267
269
  `keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
268
270
  report.keys.openai ? "available" : "missing"
269
- } grsai=${report.keys.grsai ? "available" : "missing"} mock=available localEnv=${report.keys.localEnv}`
271
+ } qwen=${report.keys.qwen ? "available" : "missing"} grsai=${
272
+ report.keys.grsai ? "available" : "missing"
273
+ } mock=available localEnv=${report.keys.localEnv}`
270
274
  );
271
275
  for (const check of report.checks) {
272
276
  const suffix = check.version ? ` ${check.version}` : check.reason ? ` ${check.reason}` : check.hint ? ` ${check.hint}` : "";
package/src/cli.js CHANGED
@@ -16,7 +16,7 @@ import {
16
16
  } from "./project.js";
17
17
  import { listTaskProfiles } from "./task-profiles.js";
18
18
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
19
- import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
19
+ import { normalizeAuthProvider, promptHidden, runAuthWizard, shouldPromptForDeepSeek } from "./auth-onboarding.js";
20
20
  import { listSkills, selectSkillsForGoal } from "./skill-library.js";
21
21
  import fs from "node:fs/promises";
22
22
  import path from "node:path";
@@ -267,13 +267,14 @@ export function parseArgs(argv) {
267
267
 
268
268
  function printUsage() {
269
269
  console.log(
270
- 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti skills [query] OR aginti login deepseek|openai|grsai OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--list-skills] [--sandbox-status|--sandbox-preflight] "your task"'
270
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|grsai] OR aginti login [deepseek|openai|qwen|grsai] OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|mock] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex] [--list-skills] [--sandbox-status|--sandbox-preflight] "your task"'
271
271
  );
272
272
  }
273
273
 
274
274
  function providerLabel(provider) {
275
275
  const normalized = String(provider || "").toLowerCase();
276
276
  if (normalized === "openai") return "OpenAI";
277
+ if (normalized === "qwen") return "Qwen";
277
278
  if (normalized === "grsai" || normalized === "auxiliary" || normalized === "auxilliary") return "GRSAI";
278
279
  return "DeepSeek";
279
280
  }
@@ -355,7 +356,9 @@ function printDoctorReport(report) {
355
356
  console.log(
356
357
  `keys: deepseek=${report.keys.deepseek ? "available" : "missing"} openai=${
357
358
  report.keys.openai ? "available" : "missing"
358
- } grsai=${report.keys.grsai ? "available" : "missing"} mock=available localEnv=${report.project.localEnvPresent}`
359
+ } qwen=${report.keys.qwen ? "available" : "missing"} grsai=${
360
+ report.keys.grsai ? "available" : "missing"
361
+ } mock=available localEnv=${report.project.localEnvPresent}`
359
362
  );
360
363
  console.log(
361
364
  `sandbox=${report.sandbox?.sandboxMode || "unknown"} docker=${
@@ -377,16 +380,14 @@ async function readStdin() {
377
380
 
378
381
  async function ensureDeepSeekKeyForOneShot(args) {
379
382
  if (!shouldPromptForDeepSeek(args, process.cwd())) return true;
380
- console.log("DeepSeek API key is not configured for this project.");
381
- console.log("Paste it once to save it in `.aginti/.env` with 0600 permissions, or press Enter to cancel.");
382
- const result = await promptAndSaveDeepSeekKey(process.cwd(), {
383
- promptText: "DeepSeek API key: ",
384
- });
385
- if (result.saved) {
386
- console.log(`saved ${result.keyName} to project-local ignored env`);
383
+ console.log("No main model API key is configured for this project.");
384
+ console.log("Choose DeepSeek, OpenAI, or Qwen, then paste a key to save in `.aginti/.env` with 0600 permissions.");
385
+ const result = await runAuthWizard(process.cwd(), { provider: args.provider || "" });
386
+ printAuthWizardResult(result);
387
+ if (result.saved.some((item) => item.provider !== "grsai")) {
387
388
  return true;
388
389
  }
389
- console.error("No DeepSeek key saved. Run `aginti login deepseek` later, or use `--provider mock` for local tests.");
390
+ console.error("No main key saved. Run `aginti auth` later, or use `--provider mock` for local tests.");
390
391
  return false;
391
392
  }
392
393
 
@@ -397,9 +398,9 @@ async function handleKeyCommand(argv) {
397
398
  console.log(
398
399
  `keys: deepseek=${status.deepseek ? "available" : "missing"} openai=${
399
400
  status.openai ? "available" : "missing"
400
- } grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
401
+ } qwen=${status.qwen ? "available" : "missing"} grsai=${status.grsai ? "available" : "missing"} mock=available localEnv=${status.localEnv}`
401
402
  );
402
- console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; image=GRSAI or GRSAI_API_KEY");
403
+ console.log("env vars: DeepSeek=DEEPSEEK_API_KEY or LLM_API_KEY; OpenAI=OPENAI_API_KEY or LLM_API_KEY; Qwen=QWEN_API_KEY; image=GRSAI or GRSAI_API_KEY");
403
404
  return;
404
405
  }
405
406
 
@@ -415,10 +416,22 @@ async function handleKeyCommand(argv) {
415
416
  return;
416
417
  }
417
418
 
418
- console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|grsai [--stdin]");
419
+ console.error("Usage: aginti keys status OR aginti keys set deepseek|openai|qwen|grsai [--stdin]");
419
420
  process.exit(1);
420
421
  }
421
422
 
423
+ function printAuthWizardResult(result) {
424
+ if (result.saved.length > 0) {
425
+ for (const item of result.saved) {
426
+ console.log(`saved ${item.provider} key to project-local ignored env (${item.keyName})`);
427
+ }
428
+ }
429
+ if (result.saved.length === 0) console.log("No key saved.");
430
+ if (result.skipped.length > 0) {
431
+ console.log(`skipped: ${result.skipped.map((item) => item.provider).join(", ")}`);
432
+ }
433
+ }
434
+
422
435
  async function handleSessionsCommand(argv) {
423
436
  const [verb = "list", sessionId = ""] = argv;
424
437
  if (verb === "list") {
@@ -532,16 +545,22 @@ export async function main(argv = process.argv.slice(2)) {
532
545
  return;
533
546
  }
534
547
 
535
- if (argv[0] === "login") {
536
- const provider = argv[1] || "deepseek";
548
+ if (argv[0] === "auth" || argv[0] === "login") {
549
+ const provider = normalizeAuthProvider(argv[1] || "", "");
550
+ if (argv[0] === "auth" || (!provider && process.stdin.isTTY)) {
551
+ const result = await runAuthWizard(process.cwd(), { provider });
552
+ printAuthWizardResult(result);
553
+ return;
554
+ }
555
+ const target = provider || "deepseek";
537
556
  const key = argv.includes("--stdin") || !process.stdin.isTTY
538
557
  ? await readStdin()
539
- : await promptHidden(`${providerLabel(provider)} API key/token: `);
558
+ : await promptHidden(`${providerLabel(target)} API key/token: `);
540
559
  if (!key) {
541
560
  console.error("No key saved.");
542
561
  process.exit(1);
543
562
  }
544
- const result = await setProviderKey(process.cwd(), provider, key);
563
+ const result = await setProviderKey(process.cwd(), target, key);
545
564
  console.log(`saved ${result.provider} key to project-local ignored env (${result.keyName})`);
546
565
  return;
547
566
  }
@@ -3,11 +3,11 @@ import { emitKeypressEvents } from "node:readline";
3
3
  import { stdin as input, stdout as output } from "node:process";
4
4
  import { runAgent } from "./agent-runner.js";
5
5
  import { loadConfig } from "./config.js";
6
- import { initProject, listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions, setProviderKey } from "./project.js";
6
+ import { initProject, listProjectSessions, projectPaths, providerKeyStatus, readProjectInstructions } from "./project.js";
7
7
  import { normalizePackageInstallPolicy, normalizeSandboxMode } from "./command-policy.js";
8
8
  import { defaultMaxStepsForProfile, normalizeTaskProfile } from "./task-profiles.js";
9
9
  import { recommendedMaxStepsForTask } from "./engineering-guidance.js";
10
- import { promptAndSaveDeepSeekKey, promptHidden, shouldPromptForDeepSeek } from "./auth-onboarding.js";
10
+ import { normalizeAuthProvider, runAuthWizard, shouldPromptForDeepSeek } from "./auth-onboarding.js";
11
11
  import { SessionStore } from "./session-store.js";
12
12
  import { listSkills, selectSkillsForGoal } from "./skill-library.js";
13
13
 
@@ -474,8 +474,8 @@ function printHelp() {
474
474
  "Commands:",
475
475
  " /help Show this help.",
476
476
  " /status Show active route, workspace, sandbox, and session.",
477
- " /login [deepseek|openai|grsai] Paste and save a project-local API key.",
478
- " /auth [deepseek|openai|grsai] Alias for /login.",
477
+ " /login [deepseek|openai|qwen|grsai] Pick, paste, and save project-local API keys.",
478
+ " /auth [deepseek|openai|qwen|grsai] Alias for /login.",
479
479
  " /instructions Show AGINTI.md project instructions status.",
480
480
  " /memory Alias for /instructions.",
481
481
  " /auxilliary [status|grsai|on|off|image]",
@@ -488,7 +488,7 @@ function printHelp() {
488
488
  " /web-search on|off Enable or disable the web_search tool.",
489
489
  " /scouts on|off|<1-10> Enable parallel DeepSeek scouts and set scout count.",
490
490
  " /routing <mode> Set routing: smart, fast, complex, manual.",
491
- " /provider <name> Set provider: deepseek, openai, mock.",
491
+ " /provider <name> Set provider: deepseek, openai, qwen, mock.",
492
492
  " /model <name> Set an explicit model, or /model auto.",
493
493
  " /docker on Use docker-workspace with approved package installs.",
494
494
  " /docker off Use host shell policy.",
@@ -1440,48 +1440,46 @@ async function maybeOnboardDeepSeekKey(state) {
1440
1440
 
1441
1441
  printAgentMessage(
1442
1442
  [
1443
- "DeepSeek API key is not configured for this project.",
1444
- "Paste it once to save it in `.aginti/.env` with 0600 permissions, or press Enter to continue in mock mode.",
1443
+ "No main model API key is configured for this project.",
1444
+ "Choose DeepSeek, OpenAI, or Qwen, then paste a key to save in `.aginti/.env` with 0600 permissions.",
1445
+ "After that, you can optionally paste the auxiliary image key. Press Esc to skip.",
1445
1446
  ].join("\n")
1446
1447
  );
1447
- const result = await promptAndSaveDeepSeekKey(process.cwd(), {
1448
- promptText: "DeepSeek API key: ",
1449
- });
1450
- if (result.saved) {
1451
- printAgentMessage(`Saved ${result.keyName} to project-local ignored env.`);
1448
+ const result = await runAuthWizard(process.cwd(), { provider: state.provider || "", includeAuxiliary: true });
1449
+ applyAuthWizardResult(result, state);
1450
+ if (result.saved.some((item) => item.provider !== "grsai")) {
1452
1451
  return;
1453
1452
  }
1454
1453
 
1455
1454
  state.provider = "mock";
1456
1455
  state.routingMode = "manual";
1457
1456
  state.model = "mock-agent";
1458
- printAgentMessage("No key saved. Continuing in local mock mode. Use `/provider deepseek` after running `aginti login deepseek`.");
1459
- }
1460
-
1461
- async function promptAndSaveProviderKey(provider = "deepseek", state = null) {
1462
- const aliases = { auxiliary: "grsai", auxilliary: "grsai", image: "grsai", imagegen: "grsai" };
1463
- const candidate = aliases[String(provider || "").toLowerCase()] || String(provider || "").toLowerCase();
1464
- const normalized = ["openai", "deepseek", "grsai"].includes(candidate)
1465
- ? String(provider || "").toLowerCase()
1466
- : "deepseek";
1467
- const canonical = aliases[normalized] || normalized;
1468
- const labelText = canonical === "openai" ? "OpenAI" : canonical === "grsai" ? "GRSAI" : "DeepSeek";
1469
- const key = await promptHidden(`${labelText} API key/token (paste, Enter to save): `);
1470
- if (!key) {
1471
- printAgentMessage("No key saved.");
1472
- return;
1473
- }
1457
+ printAgentMessage("No main key saved. Continuing in local mock mode. Use `/auth` later to save DeepSeek, OpenAI, or Qwen.");
1458
+ }
1474
1459
 
1475
- const result = await setProviderKey(process.cwd(), canonical, key);
1460
+ function applyAuthWizardResult(result, state = null) {
1476
1461
  if (state) {
1477
- if (canonical !== "grsai") state.provider = canonical;
1462
+ const main = result.saved.find((item) => item.provider !== "grsai");
1463
+ if (main) state.provider = main.provider;
1478
1464
  if (state.routingMode === "manual" && state.model === "mock-agent") {
1479
1465
  state.routingMode = "smart";
1480
1466
  state.model = "";
1481
1467
  }
1482
- if (canonical === "grsai") state.allowAuxiliaryTools = true;
1468
+ if (result.saved.some((item) => item.provider === "grsai")) state.allowAuxiliaryTools = true;
1469
+ }
1470
+ if (result.saved.length > 0) {
1471
+ printAgentMessage(
1472
+ result.saved.map((item) => `Saved ${item.keyName} to project-local ignored env. Raw key was not printed.`).join("\n")
1473
+ );
1474
+ } else {
1475
+ printAgentMessage("No key saved.");
1483
1476
  }
1484
- printAgentMessage(`Saved ${result.keyName} to project-local ignored env. Raw key was not printed.`);
1477
+ }
1478
+
1479
+ async function promptAndSaveProviderKey(provider = "", state = null) {
1480
+ const canonical = normalizeAuthProvider(provider || "", "");
1481
+ const result = await runAuthWizard(process.cwd(), { provider: canonical, includeAuxiliary: canonical !== "grsai" });
1482
+ applyAuthWizardResult(result, state);
1485
1483
  }
1486
1484
 
1487
1485
  async function handleCommand(line, state, packageDir) {
@@ -1499,7 +1497,7 @@ async function handleCommand(line, state, packageDir) {
1499
1497
  printSystemLine(
1500
1498
  `keys deepseek=${keys.deepseek ? "available" : "missing"} openai=${keys.openai ? "available" : "missing"} grsai=${
1501
1499
  keys.grsai ? "available" : "missing"
1502
- }`
1500
+ } qwen=${keys.qwen ? "available" : "missing"}`
1503
1501
  );
1504
1502
  return true;
1505
1503
  }
@@ -88,6 +88,15 @@ export function getProviderDefaults(provider = "deepseek") {
88
88
  };
89
89
  }
90
90
 
91
+ if (provider === "qwen") {
92
+ return {
93
+ provider: "qwen",
94
+ apiKey: process.env.QWEN_API_KEY || "",
95
+ baseURL: process.env.QWEN_BASE_URL || process.env.LLM_BASE_URL || "https://dashscope.aliyuncs.com/compatible-mode/v1",
96
+ model: process.env.QWEN_DEFAULT_MODEL || process.env.LLM_MODEL || "qwen-plus",
97
+ };
98
+ }
99
+
91
100
  return {
92
101
  provider: "deepseek",
93
102
  apiKey: process.env.LLM_API_KEY || process.env.DEEPSEEK_API_KEY || "",
package/src/project.js CHANGED
@@ -252,6 +252,7 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
252
252
  return {
253
253
  openai: Boolean(process.env.OPENAI_API_KEY || process.env.LLM_API_KEY),
254
254
  deepseek: Boolean(process.env.DEEPSEEK_API_KEY || process.env.LLM_API_KEY),
255
+ qwen: Boolean(process.env.QWEN_API_KEY),
255
256
  grsai: Boolean(process.env.GRSAI || process.env.GRSAI_API_KEY),
256
257
  mock: true,
257
258
  localEnv: env.loaded,
@@ -259,6 +260,7 @@ export function providerKeyStatus(projectRoot = process.cwd()) {
259
260
  envVars: {
260
261
  openai: ["OPENAI_API_KEY", "LLM_API_KEY"],
261
262
  deepseek: ["DEEPSEEK_API_KEY", "LLM_API_KEY"],
263
+ qwen: ["QWEN_API_KEY"],
262
264
  grsai: ["GRSAI", "GRSAI_API_KEY"],
263
265
  },
264
266
  };
@@ -274,9 +276,15 @@ export async function setProviderKey(projectRoot, provider, value) {
274
276
  };
275
277
  const canonicalProvider = aliases[normalizedProvider] || normalizedProvider;
276
278
  const keyName =
277
- canonicalProvider === "openai" ? "OPENAI_API_KEY" : canonicalProvider === "grsai" ? "GRSAI" : "DEEPSEEK_API_KEY";
278
- if (!["deepseek", "openai", "grsai"].includes(canonicalProvider)) {
279
- throw new Error("Provider must be deepseek, openai, or grsai.");
279
+ canonicalProvider === "openai"
280
+ ? "OPENAI_API_KEY"
281
+ : canonicalProvider === "qwen"
282
+ ? "QWEN_API_KEY"
283
+ : canonicalProvider === "grsai"
284
+ ? "GRSAI"
285
+ : "DEEPSEEK_API_KEY";
286
+ if (!["deepseek", "openai", "qwen", "grsai"].includes(canonicalProvider)) {
287
+ throw new Error("Provider must be deepseek, openai, qwen, or grsai.");
280
288
  }
281
289
 
282
290
  const keyValue = String(value || "").trim();
@@ -412,6 +420,7 @@ export async function doctorReport(projectRoot, packageVersion, config) {
412
420
  keys: {
413
421
  openai: keyStatus.openai,
414
422
  deepseek: keyStatus.deepseek,
423
+ qwen: keyStatus.qwen,
415
424
  grsai: keyStatus.grsai,
416
425
  mock: true,
417
426
  },
package/web.js CHANGED
@@ -113,7 +113,7 @@ function serializeRun(run) {
113
113
  function normalizePreferencePayload(body = {}, current = db.getPreferences()) {
114
114
  const modelPresets = getModelPresets();
115
115
  const providerCandidate = body.provider || current.provider || "deepseek";
116
- const provider = ["openai", "deepseek", "mock"].includes(providerCandidate) ? providerCandidate : "deepseek";
116
+ const provider = ["openai", "deepseek", "qwen", "mock"].includes(providerCandidate) ? providerCandidate : "deepseek";
117
117
  const routingMode =
118
118
  provider === "mock" ? "manual" : normalizeRoutingMode(body.routingMode || current.routingMode || "smart");
119
119
  const providerDefaults = getProviderDefaults(provider);
@@ -195,6 +195,7 @@ function publicKeyStatus(projectRoot = baseDir) {
195
195
  return {
196
196
  openai: status.openai,
197
197
  deepseek: status.deepseek,
198
+ qwen: status.qwen,
198
199
  grsai: status.grsai,
199
200
  mock: true,
200
201
  localEnv: status.localEnv,
@@ -570,6 +571,7 @@ app.get("/api/config", async (_req, res) => {
570
571
  defaults: {
571
572
  openai: publicProviderDefault("openai"),
572
573
  deepseek: publicProviderDefault("deepseek"),
574
+ qwen: publicProviderDefault("qwen"),
573
575
  mock: publicProviderDefault("mock"),
574
576
  headless: true,
575
577
  maxSteps: 24,