feat: multi-model support for OpenCode CLI config with subagent integration

- 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
This commit is contained in:
decolua
2026-04-10 10:22:42 +07:00
parent 3c96e8d6d1
commit 1a25c6e3b5
2 changed files with 216 additions and 54 deletions
+108 -28
View File
@@ -61,12 +61,19 @@ export async function GET() {
}
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);
@@ -74,13 +81,16 @@ export async function GET() {
}
}
// POST - Apply 9Router as openai-compatible provider
// POST - Apply 9Router as openai-compatible provider (multi-model support)
export async function POST(request) {
try {
const { baseUrl, apiKey, model, subagentModel } = await request.json();
const { baseUrl, apiKey, model, models, activeModel, subagentModel } = await request.json();
if (!baseUrl || !model) {
return NextResponse.json({ error: "baseUrl and model are required" }, { status: 400 });
// 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();
@@ -97,24 +107,43 @@ export async function POST(request) {
const normalizedBaseUrl = baseUrl.endsWith("/v1") ? baseUrl : `${baseUrl}/v1`;
const keyToUse = apiKey || "sk_9router";
const effectiveSubagentModel = subagentModel || model;
const effectiveSubagentModel = subagentModel || modelsArray[0];
// Merge 9router provider
// Ensure provider object
if (!config.provider) config.provider = {};
config.provider["9router"] = {
npm: "@ai-sdk/openai-compatible",
options: {
baseURL: normalizedBaseUrl,
apiKey: keyToUse,
},
models: {
[model]: { name: model },
[effectiveSubagentModel]: { name: effectiveSubagentModel },
},
// 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,
};
// Set as active model
config.model = `9router/${model}`;
// 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 = {};
@@ -132,14 +161,52 @@ export async function POST(request) {
configPath,
});
} catch (error) {
console.log("Error updating opencode settings:", error);
return NextResponse.json({ error: "Failed to update opencode settings" }, { status: 500 });
console.log("Error applying opencode settings:", error);
return NextResponse.json({ error: "Failed to apply settings" }, { status: 500 });
}
}
// DELETE - Remove 9Router provider from config
export async function DELETE() {
// 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 = {};
@@ -153,11 +220,24 @@ export async function DELETE() {
throw error;
}
// Remove 9router provider
if (config.provider) delete config.provider["9router"];
// Reset model if it was pointing to 9router
if (config.model?.startsWith("9router/")) delete config.model;
// 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/")) {
@@ -170,7 +250,7 @@ export async function DELETE() {
return NextResponse.json({
success: true,
message: "9Router settings removed from OpenCode",
message: modelToRemove ? `Model "${modelToRemove}" removed` : "9Router settings removed from OpenCode",
});
} catch (error) {
console.log("Error resetting opencode settings:", error);