feat(cli): add direct config save for Qwen Code (#1394)

Integrated into release/v3.6.9
This commit is contained in:
Benson K B
2026-04-18 19:29:11 +05:30
committed by GitHub
parent 891189bbf3
commit 4a560c0b1c
4 changed files with 171 additions and 17 deletions
@@ -153,7 +153,7 @@ export default function DefaultToolCard({
};
// Check if this tool supports direct config file write
const supportsDirectSave = ["continue", "opencode"].includes(toolId);
const supportsDirectSave = ["continue", "opencode", "qwen"].includes(toolId);
const renderApiKeySelector = () => {
return (
@@ -45,6 +45,8 @@ export async function POST(request, { params }) {
// (#524) OpenCode config was never saved because only 'continue' was handled here.
// opencode reads ~/.config/opencode/config.toml — write the OmniRoute settings there.
return await saveOpenCodeConfig({ baseUrl, apiKey, model });
case "qwen":
return await saveQwenConfig({ baseUrl, apiKey, model });
default:
return NextResponse.json(
{ error: `Direct config save not supported for: ${toolId}` },
@@ -173,3 +175,63 @@ async function saveOpenCodeConfig({ baseUrl, apiKey, model }) {
configPath,
});
}
/**
* Save Qwen Code config to ~/.qwen/settings.json
* Writes the modelProviders.openai entry with OmniRoute as the provider.
* Merges with existing config to preserve other providers.
*/
async function saveQwenConfig({ baseUrl, apiKey, model }) {
const home = os.homedir();
const configPath = path.join(home, ".qwen", "settings.json");
const configDir = path.dirname(configPath);
await fs.mkdir(configDir, { recursive: true });
const normalizedBaseUrl = String(baseUrl || "").trim().replace(/\/+$/, "");
// Read existing config to preserve other provider entries
let existingConfig: Record<string, any> = {};
try {
const raw = await fs.readFile(configPath, "utf-8");
existingConfig = JSON.parse(raw);
} catch {
// File doesn't exist or invalid JSON
}
// Build OmniRoute openai provider entry
const omnirouteEntry = {
id: "omniroute",
name: "OmniRoute",
envKey: "OPENAI_API_KEY",
baseUrl: normalizedBaseUrl,
apiKey: apiKey || "sk_omniroute",
generationConfig: {
defaultModel: model || "auto",
},
};
// Ensure modelProviders.openai array exists
if (!existingConfig.modelProviders) existingConfig.modelProviders = {};
if (!existingConfig.modelProviders.openai) existingConfig.modelProviders.openai = [];
const providers = existingConfig.modelProviders.openai;
// Replace OmniRoute entry if already present, otherwise add it
const existingIdx = providers.findIndex(
(p: any) => p && (p.baseUrl === normalizedBaseUrl || p.id === "omniroute")
);
if (existingIdx >= 0) {
providers[existingIdx] = omnirouteEntry;
} else {
providers.push(omnirouteEntry);
}
await fs.writeFile(configPath, JSON.stringify(existingConfig, null, 2), "utf-8");
return NextResponse.json({
success: true,
message: `Qwen Code config saved to ${configPath}`,
configPath,
});
}
+32 -16
View File
@@ -358,39 +358,55 @@ amp --model "{{model}}"
color: "#10B981",
description: "Alibaba Qwen Code CLI — OpenAI-compatible endpoint",
docsUrl: "https://qwenlm.github.io/qwen-code-docs/",
configType: "custom",
configType: "guide",
defaultCommand: "qwen",
notes: [
{
type: "info",
text: "Qwen Code supports custom OpenAI-compatible API endpoints via environment variables or settings.json.",
text: "Qwen Code supports custom OpenAI-compatible API endpoints via modelProviders in settings.json.",
},
{
type: "warning",
text: "Config path: Linux/macOS ~/.qwen/ • Windows %USERPROFILE%\\.qwen\\",
text: "Config path: Linux/macOS ~/.qwen/settings.json • Windows %USERPROFILE%\\.qwen\\settings.json",
},
],
modelAliases: ["default", "claude-sonnet", "claude-opus", "gemini-pro", "gemini-flash"],
defaultModels: [
{
id: "default",
name: "Default Model",
alias: "default",
envKey: "OPENAI_MODEL",
defaultValue: "auto",
},
],
guideSteps: [
{ step: 1, title: "Install Qwen Code", desc: "npm install -g @qwen-code/qwen-code" },
{ step: 2, title: "API Key", type: "apiKeySelector" },
{ step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
{ step: 4, title: "Select Model", type: "modelSelector" },
{
step: 4,
title: "Configure Settings",
desc: "Add to your ~/.qwen/.env file or settings.json env field:",
step: 5,
title: "Save Config",
desc: "Click Save Config below to write your settings.json automatically.",
},
],
codeBlock: {
language: "bash",
code: `# ~/.qwen/.env
OPENAI_API_KEY="{{apiKey}}"
OPENAI_BASE_URL="{{baseUrl}}"
OPENAI_MODEL="auto"
# Or add to settings.json:
# "env": {
# "OPENAI_API_KEY": "{{apiKey}}",
# "OPENAI_BASE_URL": "{{baseUrl}}"
# }`,
language: "json",
code: `# ~/.qwen/settings.json
{
"modelProviders": {
"openai": [{
"id": "omniroute",
"name": "OmniRoute",
"envKey": "OPENAI_API_KEY",
"baseUrl": "{{baseUrl}}",
"generationConfig": {
"defaultModel": "{{model}}"
}
}]
}
}`,
},
},
// HIDDEN: gemini-cli
+76
View File
@@ -0,0 +1,76 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { NextResponse } from "next/server";
const guideSettingsRoute =
await import("../../src/app/api/cli-tools/guide-settings/[toolId]/route.ts");
const DUMMY_HOME = path.join(os.tmpdir(), "omniroute-qwen-test-" + Date.now());
const QWEN_CONFIG_PATH = path.join(DUMMY_HOME, ".qwen", "settings.json");
function buildRequest(body: any) {
return new Request("http://localhost/api/cli-tools/guide-settings/qwen", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
}
test.beforeEach(async () => {
// Mock os.homedir to return our dummy path
os.homedir = () => DUMMY_HOME;
await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true }).catch(() => {});
});
test.afterEach(async () => {
await fs.rm(DUMMY_HOME, { recursive: true, force: true }).catch(() => {});
});
test("guide-settings POST creates new qwen settings.json if it doesn't exist", async () => {
const req = buildRequest({ baseUrl: "http://my-omni", apiKey: "sk-123", model: "qwen-max" });
const response = (await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } })) as Response;
const data = await response.json();
assert.equal(response.status, 200, "Response should be OK");
assert.equal(data.success, true);
const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8"));
assert.ok(content.modelProviders.openai);
const omniProvider = content.modelProviders.openai.find((p: any) => p.id === "omniroute");
assert.ok(omniProvider);
assert.equal(omniProvider.baseUrl, "http://my-omni");
assert.equal(omniProvider.apiKey, "sk-123");
assert.equal(omniProvider.generationConfig.defaultModel, "qwen-max");
});
test("guide-settings POST merges into existing qwen settings.json", async () => {
await fs.mkdir(path.dirname(QWEN_CONFIG_PATH), { recursive: true });
await fs.writeFile(
QWEN_CONFIG_PATH,
JSON.stringify({
modelProviders: {
openai: [{ id: "other", baseUrl: "https://other" }],
},
}),
"utf-8"
);
const req = buildRequest({ baseUrl: "http://my-omni", apiKey: "sk-123", model: "auto" });
const response = (await guideSettingsRoute.POST(req, { params: { toolId: "qwen" } })) as Response;
assert.equal(response.status, 200);
const content = JSON.parse(await fs.readFile(QWEN_CONFIG_PATH, "utf-8"));
assert.equal(content.modelProviders.openai.length, 2);
const otherProvider = content.modelProviders.openai.find((p: any) => p.id === "other");
assert.ok(otherProvider);
assert.equal(otherProvider.baseUrl, "https://other");
const omniProvider = content.modelProviders.openai.find((p: any) => p.id === "omniroute");
assert.ok(omniProvider);
assert.equal(omniProvider.generationConfig.defaultModel, "auto"); // default fallback
});