feat(cli): add direct config save for Qwen Code (#1394)
Integrated into release/v3.6.9
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
Reference in New Issue
Block a user