mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 20:00:47 +00:00
- Allow selecting multiple models for 9router provider - Merge models instead of overwriting (backwards-compatible) - Support setting active model via click or Apply - Click active model to clear default (model = '') - Remove individual models via X button (persists to config) - Add PATCH endpoint for clearing active model - Update GET to return normalized model list - Preserve subagent model configuration for explorer subagent Made-with: Cursor
260 lines
8.3 KiB
JavaScript
260 lines
8.3 KiB
JavaScript
"use server";
|
|
|
|
import { NextResponse } from "next/server";
|
|
import { exec } from "child_process";
|
|
import { promisify } from "util";
|
|
import fs from "fs/promises";
|
|
import path from "path";
|
|
import os from "os";
|
|
|
|
const execAsync = promisify(exec);
|
|
|
|
const getConfigDir = () => path.join(os.homedir(), ".config", "opencode");
|
|
const getConfigPath = () => path.join(getConfigDir(), "opencode.json");
|
|
|
|
// Check if opencode CLI is installed (via which/where or config file exists)
|
|
const checkOpenCodeInstalled = async () => {
|
|
try {
|
|
const isWindows = os.platform() === "win32";
|
|
const command = isWindows ? "where opencode" : "which opencode";
|
|
const env = isWindows
|
|
? { ...process.env, PATH: `${process.env.APPDATA}\\npm;${process.env.PATH}` }
|
|
: process.env;
|
|
await execAsync(command, { windowsHide: true, env });
|
|
return true;
|
|
} catch {
|
|
try {
|
|
await fs.access(getConfigPath());
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
};
|
|
|
|
const readConfig = async () => {
|
|
try {
|
|
const content = await fs.readFile(getConfigPath(), "utf-8");
|
|
return JSON.parse(content);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") return null;
|
|
throw error;
|
|
}
|
|
};
|
|
|
|
const has9RouterConfig = (config) => {
|
|
if (!config?.provider) return false;
|
|
return !!config.provider["9router"];
|
|
};
|
|
|
|
// GET - Check opencode CLI and read current settings
|
|
export async function GET() {
|
|
try {
|
|
const isInstalled = await checkOpenCodeInstalled();
|
|
|
|
if (!isInstalled) {
|
|
return NextResponse.json({
|
|
installed: false,
|
|
config: null,
|
|
message: "OpenCode CLI is not installed",
|
|
});
|
|
}
|
|
|
|
const config = await readConfig();
|
|
const providerConfig = config?.provider?.["9router"];
|
|
const modelMap = providerConfig?.models || {};
|
|
|
|
return NextResponse.json({
|
|
installed: true,
|
|
config,
|
|
has9Router: has9RouterConfig(config),
|
|
configPath: getConfigPath(),
|
|
opencode: {
|
|
models: Object.keys(modelMap),
|
|
activeModel: config?.model?.startsWith("9router/") ? config.model.replace(/^9router\//, "") : null,
|
|
baseURL: providerConfig?.options?.baseURL || null,
|
|
},
|
|
});
|
|
} catch (error) {
|
|
console.log("Error checking opencode settings:", error);
|
|
return NextResponse.json({ error: "Failed to check opencode settings" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// POST - Apply 9Router as openai-compatible provider (multi-model support)
|
|
export async function POST(request) {
|
|
try {
|
|
const { baseUrl, apiKey, model, models, activeModel, subagentModel } = await request.json();
|
|
|
|
// Accept either `model` (string, legacy) or `models` (array of strings)
|
|
const modelsArray = Array.isArray(models) ? models.slice() : (typeof model === "string" ? [model] : []);
|
|
|
|
if (!baseUrl || modelsArray.length === 0) {
|
|
return NextResponse.json({ error: "baseUrl and at least one model are required" }, { status: 400 });
|
|
}
|
|
|
|
const configDir = getConfigDir();
|
|
const configPath = getConfigPath();
|
|
|
|
await fs.mkdir(configDir, { recursive: true });
|
|
|
|
// Read existing config or start fresh
|
|
let config = {};
|
|
try {
|
|
const existing = await fs.readFile(configPath, "utf-8");
|
|
config = JSON.parse(existing);
|
|
} catch { /* No existing config */ }
|
|
|
|
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
|
|
const keyToUse = apiKey || "sk_9router";
|
|
const effectiveSubagentModel = subagentModel || modelsArray[0];
|
|
|
|
// Ensure provider object
|
|
if (!config.provider) config.provider = {};
|
|
|
|
// Preserve any existing 9router provider entry and its models
|
|
const existingProvider = config.provider["9router"] || { npm: "@ai-sdk/openai-compatible", options: {}, models: {} };
|
|
|
|
// Merge options (overwrite baseURL/apiKey)
|
|
existingProvider.options = {
|
|
...existingProvider.options,
|
|
baseURL: normalizedBaseUrl,
|
|
apiKey: keyToUse,
|
|
};
|
|
|
|
// Ensure models map exists
|
|
existingProvider.models = existingProvider.models || {};
|
|
|
|
// Add or update entries for all requested models
|
|
for (const m of modelsArray) {
|
|
if (!m || typeof m !== "string") continue;
|
|
existingProvider.models[m] = { name: m };
|
|
}
|
|
|
|
// Save merged provider back
|
|
config.provider["9router"] = existingProvider;
|
|
|
|
// Set the active model: prefer explicit activeModel, else first of modelsArray
|
|
// If activeModel is explicitly empty string, clear the model
|
|
if (activeModel === "") {
|
|
config.model = "";
|
|
} else {
|
|
const finalActive = activeModel || modelsArray[0];
|
|
if (finalActive) {
|
|
config.model = `9router/${finalActive}`;
|
|
}
|
|
}
|
|
|
|
// Add subagent configuration
|
|
if (!config.agent) config.agent = {};
|
|
config.agent.explorer = {
|
|
description: "Fast explorer subagent for codebase exploration",
|
|
mode: "subagent",
|
|
model: `9router/${effectiveSubagentModel}`,
|
|
};
|
|
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "OpenCode settings applied successfully!",
|
|
configPath,
|
|
});
|
|
} catch (error) {
|
|
console.log("Error applying opencode settings:", error);
|
|
return NextResponse.json({ error: "Failed to apply settings" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// PATCH - Update specific settings (e.g., clear active model)
|
|
export async function PATCH(request) {
|
|
try {
|
|
const { clearActiveModel } = await request.json();
|
|
const configPath = getConfigPath();
|
|
|
|
let config = {};
|
|
try {
|
|
const existing = await fs.readFile(configPath, "utf-8");
|
|
config = JSON.parse(existing);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return NextResponse.json({ success: true, message: "No config file found" });
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
if (clearActiveModel === true) {
|
|
// Clear active model but keep models in the list
|
|
if (config.model?.startsWith("9router/")) {
|
|
config.model = "";
|
|
}
|
|
}
|
|
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: "Settings updated",
|
|
});
|
|
} catch (error) {
|
|
console.log("Error patching opencode settings:", error);
|
|
return NextResponse.json({ error: "Failed to patch settings" }, { status: 500 });
|
|
}
|
|
}
|
|
|
|
// DELETE - Remove 9Router provider or specific models from config
|
|
export async function DELETE(request) {
|
|
try {
|
|
const { searchParams } = new URL(request.url);
|
|
const modelToRemove = searchParams.get("model");
|
|
const configPath = getConfigPath();
|
|
|
|
let config = {};
|
|
try {
|
|
const existing = await fs.readFile(configPath, "utf-8");
|
|
config = JSON.parse(existing);
|
|
} catch (error) {
|
|
if (error.code === "ENOENT") {
|
|
return NextResponse.json({ success: true, message: "No config file to reset" });
|
|
}
|
|
throw error;
|
|
}
|
|
|
|
// If specific model provided, remove just that model
|
|
if (modelToRemove && config.provider?.["9router"]?.models) {
|
|
delete config.provider["9router"].models[modelToRemove];
|
|
|
|
// If no models left, remove the provider
|
|
if (Object.keys(config.provider["9router"].models).length === 0) {
|
|
delete config.provider["9router"];
|
|
if (config.model?.startsWith("9router/")) delete config.model;
|
|
} else if (config.model === `9router/${modelToRemove}`) {
|
|
// If removed model was active, switch to first remaining model
|
|
const remainingModels = Object.keys(config.provider["9router"].models);
|
|
config.model = `9router/${remainingModels[0]}`;
|
|
}
|
|
} else {
|
|
// No specific model - remove entire 9router provider
|
|
if (config.provider) delete config.provider["9router"];
|
|
if (config.model?.startsWith("9router/")) delete config.model;
|
|
}
|
|
|
|
// Remove subagent configuration
|
|
if (config.agent?.explorer?.model?.startsWith("9router/")) {
|
|
delete config.agent.explorer;
|
|
// Clean up empty agent object
|
|
if (Object.keys(config.agent).length === 0) delete config.agent;
|
|
}
|
|
|
|
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
|
|
|
|
return NextResponse.json({
|
|
success: true,
|
|
message: modelToRemove ? `Model "${modelToRemove}" removed` : "9Router settings removed from OpenCode",
|
|
});
|
|
} catch (error) {
|
|
console.log("Error resetting opencode settings:", error);
|
|
return NextResponse.json({ error: "Failed to reset opencode settings" }, { status: 500 });
|
|
}
|
|
}
|