mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the logic for generate configiration for cli tools
This commit is contained in:
@@ -20,7 +20,7 @@ const withThinkingLevel = (model, thinkingLevel) => (
|
|||||||
model && thinkingLevel ? `${model}(${thinkingLevel})` : model
|
model && thinkingLevel ? `${model}(${thinkingLevel})` : model
|
||||||
);
|
);
|
||||||
|
|
||||||
function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, claudeThinking = {}, codexModel = "", codexThinking = "" }) {
|
function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, claudeThinking = {}, codexModel = "", codexThinking = "", opencodeModels = [], opencodeDefaultModel = "", coworkThinking = {} }) {
|
||||||
const endpoint = normalizeV1(baseUrl);
|
const endpoint = normalizeV1(baseUrl);
|
||||||
const selectedModels = models.length ? models : [DEFAULT_MODEL];
|
const selectedModels = models.length ? models : [DEFAULT_MODEL];
|
||||||
const model = selectedModels[0];
|
const model = selectedModels[0];
|
||||||
@@ -49,19 +49,26 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
{ filename: "~/.codex/auth.json", content: toJson({ auth_mode: "apikey", OPENAI_API_KEY: apiKey }) },
|
{ filename: "~/.codex/auth.json", content: toJson({ auth_mode: "apikey", OPENAI_API_KEY: apiKey }) },
|
||||||
];
|
];
|
||||||
case "opencode": {
|
case "opencode": {
|
||||||
const modelEntries = Object.fromEntries(selectedModels.map((id) => [id, {
|
// OpenCode identifies a default model as <provider-id>/<model-id>. Our
|
||||||
name: id,
|
// custom provider is "9router", while the 9Router model ID itself keeps
|
||||||
modalities: { input: ["text", "image"], output: ["text"] },
|
// its upstream provider prefix (for example, "cc/claude-sonnet-5").
|
||||||
}]));
|
const configuredModels = opencodeModels.length ? opencodeModels : [DEFAULT_MODEL];
|
||||||
|
const modelId = configuredModels.includes(opencodeDefaultModel)
|
||||||
|
? opencodeDefaultModel
|
||||||
|
: configuredModels[0];
|
||||||
return [{
|
return [{
|
||||||
filename: "~/.config/opencode/opencode.json",
|
filename: "~/.config/opencode/opencode.json",
|
||||||
content: toJson({
|
content: toJson({
|
||||||
provider: { "9router": {
|
$schema: "https://opencode.ai/config.json",
|
||||||
npm: "@ai-sdk/openai-compatible",
|
provider: {
|
||||||
options: { baseURL: endpoint, apiKey },
|
"9router": {
|
||||||
models: modelEntries,
|
npm: "@ai-sdk/openai-compatible",
|
||||||
} },
|
name: "9Router",
|
||||||
model: `9router/${model}`,
|
options: { baseURL: endpoint, apiKey },
|
||||||
|
models: Object.fromEntries(configuredModels.map((id) => [id, { name: id }])),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
model: `9router/${modelId}`,
|
||||||
}),
|
}),
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
@@ -78,8 +85,19 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
}];
|
}];
|
||||||
case "cowork":
|
case "cowork":
|
||||||
return [{
|
return [{
|
||||||
filename: "Claude Desktop third-party inference configuration.json",
|
// Claude Desktop reads third-party inference profiles from its 3P
|
||||||
content: toJson({ baseUrl: endpoint, apiKey, models: selectedModels }),
|
// config library. Unlike Claude Code, Cowork does not support named
|
||||||
|
// Sonnet/Opus/Haiku environment-variable slots: every entry here is
|
||||||
|
// exposed directly in Claude Desktop's model picker.
|
||||||
|
filename: "Claude-3p/configLibrary/<appliedId>.json",
|
||||||
|
content: toJson({
|
||||||
|
inferenceProvider: "gateway",
|
||||||
|
inferenceGatewayBaseUrl: endpoint,
|
||||||
|
inferenceGatewayApiKey: apiKey,
|
||||||
|
inferenceModels: selectedModels.map((name) => ({
|
||||||
|
name: withThinkingLevel(name, coworkThinking[name]),
|
||||||
|
})),
|
||||||
|
}),
|
||||||
}];
|
}];
|
||||||
default:
|
default:
|
||||||
return [{ filename: "config.json", content: toJson({ baseUrl: endpoint, apiKey, model }) }];
|
return [{ filename: "config.json", content: toJson({ baseUrl: endpoint, apiKey, model }) }];
|
||||||
@@ -105,6 +123,9 @@ export default function ConfigGeneratorCard({
|
|||||||
const [claudeModelSlot, setClaudeModelSlot] = useState("");
|
const [claudeModelSlot, setClaudeModelSlot] = useState("");
|
||||||
const [codexModel, setCodexModel] = useState("");
|
const [codexModel, setCodexModel] = useState("");
|
||||||
const [codexThinking, setCodexThinking] = useState("");
|
const [codexThinking, setCodexThinking] = useState("");
|
||||||
|
const [opencodeModels, setOpencodeModels] = useState([]);
|
||||||
|
const [opencodeDefaultModel, setOpencodeDefaultModel] = useState("");
|
||||||
|
const [coworkThinking, setCoworkThinking] = useState({});
|
||||||
const [connectedModels, setConnectedModels] = useState(null);
|
const [connectedModels, setConnectedModels] = useState(null);
|
||||||
const [customBaseUrl, setCustomBaseUrl] = useState("");
|
const [customBaseUrl, setCustomBaseUrl] = useState("");
|
||||||
const [modelModalOpen, setModelModalOpen] = useState(false);
|
const [modelModalOpen, setModelModalOpen] = useState(false);
|
||||||
@@ -113,8 +134,8 @@ export default function ConfigGeneratorCard({
|
|||||||
const effectiveBaseUrl = customBaseUrl || baseUrl;
|
const effectiveBaseUrl = customBaseUrl || baseUrl;
|
||||||
const apiKey = selectedApiKey.trim() || (cloudEnabled ? "<API_KEY_FROM_DASHBOARD>" : "sk_9router");
|
const apiKey = selectedApiKey.trim() || (cloudEnabled ? "<API_KEY_FROM_DASHBOARD>" : "sk_9router");
|
||||||
const configs = useMemo(
|
const configs = useMemo(
|
||||||
() => buildConfigs(toolId, { baseUrl: effectiveBaseUrl, apiKey, models: selectedModels, claudeModels, claudeThinking, codexModel, codexThinking }),
|
() => buildConfigs(toolId, { baseUrl: effectiveBaseUrl, apiKey, models: selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking }),
|
||||||
[toolId, effectiveBaseUrl, apiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking]
|
[toolId, effectiveBaseUrl, apiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking]
|
||||||
);
|
);
|
||||||
|
|
||||||
const getThinkingLevelsForModel = (fullModel) => {
|
const getThinkingLevelsForModel = (fullModel) => {
|
||||||
@@ -124,7 +145,7 @@ export default function ConfigGeneratorCard({
|
|||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (toolId !== "claude" && toolId !== "codex") return;
|
if (toolId !== "claude" && toolId !== "codex" && toolId !== "opencode" && toolId !== "cowork") return;
|
||||||
|
|
||||||
let cancelled = false;
|
let cancelled = false;
|
||||||
const loadConnectedModels = async () => {
|
const loadConnectedModels = async () => {
|
||||||
@@ -146,6 +167,16 @@ export default function ConfigGeneratorCard({
|
|||||||
const addModel = (selected) => {
|
const addModel = (selected) => {
|
||||||
if (!selected?.value || selectedModels.includes(selected.value)) return;
|
if (!selected?.value || selectedModels.includes(selected.value)) return;
|
||||||
setSelectedModels((current) => [...current, selected.value]);
|
setSelectedModels((current) => [...current, selected.value]);
|
||||||
|
if (toolId === "cowork") setCoworkThinking((current) => ({ ...current, [selected.value]: "" }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeCoworkModel = (model) => {
|
||||||
|
setSelectedModels((current) => current.filter((item) => item !== model));
|
||||||
|
setCoworkThinking((current) => {
|
||||||
|
const remainingThinking = { ...current };
|
||||||
|
delete remainingThinking[model];
|
||||||
|
return remainingThinking;
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectClaudeModel = (selected) => {
|
const selectClaudeModel = (selected) => {
|
||||||
@@ -166,6 +197,18 @@ export default function ConfigGeneratorCard({
|
|||||||
setCodexThinking("");
|
setCodexThinking("");
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const selectOpenCodeModel = (selected) => {
|
||||||
|
if (!selected?.value || opencodeModels.includes(selected.value)) return;
|
||||||
|
setOpencodeModels((current) => [...current, selected.value]);
|
||||||
|
if (!opencodeDefaultModel) setOpencodeDefaultModel(selected.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeOpenCodeModel = (model) => {
|
||||||
|
const remainingModels = opencodeModels.filter((item) => item !== model);
|
||||||
|
setOpencodeModels(remainingModels);
|
||||||
|
if (opencodeDefaultModel === model) setOpencodeDefaultModel(remainingModels[0] || "");
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="sm" className="overflow-hidden">
|
<Card padding="sm" className="overflow-hidden">
|
||||||
<div className="flex items-start gap-3 sm:items-center">
|
<div className="flex items-start gap-3 sm:items-center">
|
||||||
@@ -274,6 +317,71 @@ export default function ConfigGeneratorCard({
|
|||||||
)}
|
)}
|
||||||
<p className="text-xs text-text-muted">The selected reasoning level is appended to the model ID, for example <code>cx/gpt-5.6-sol(high)</code>.</p>
|
<p className="text-xs text-text-muted">The selected reasoning level is appended to the model ID, for example <code>cx/gpt-5.6-sol(high)</code>.</p>
|
||||||
</div>
|
</div>
|
||||||
|
) : toolId === "opencode" ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-xs font-medium text-text-muted">Models</span>
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => setModelModalOpen(true)}>
|
||||||
|
<span className="material-symbols-outlined mr-1 text-[16px]">add</span>
|
||||||
|
Add model
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{opencodeModels.length ? (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{opencodeModels.map((model) => {
|
||||||
|
const isDefault = model === opencodeDefaultModel;
|
||||||
|
return (
|
||||||
|
<div key={model} className={`inline-flex items-center gap-1 rounded-full border bg-bg-secondary pr-1 text-xs text-text-main ${isDefault ? "border-primary" : "border-border"}`}>
|
||||||
|
<button type="button" onClick={() => setOpencodeDefaultModel(model)} className="rounded-full px-2 py-1 hover:text-primary" title="Set as default model">
|
||||||
|
{model}{isDefault && <span className="ml-1 text-primary">default</span>}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => removeOpenCodeModel(model)} className="rounded-full p-1 hover:text-red-500" title="Remove model" aria-label={`Remove ${model}`}>
|
||||||
|
<span className="material-symbols-outlined block text-[14px]">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : <p className="text-xs text-text-muted">No model selected. The generated file uses <code>{DEFAULT_MODEL}</code> as a placeholder.</p>}
|
||||||
|
<p className="text-xs text-text-muted">Add every model to expose in OpenCode, then click a model to make it the default.</p>
|
||||||
|
</div>
|
||||||
|
) : toolId === "cowork" ? (
|
||||||
|
<div className="flex flex-col gap-3">
|
||||||
|
<div className="flex items-center justify-between gap-3">
|
||||||
|
<span className="text-xs font-medium text-text-muted">Models</span>
|
||||||
|
<Button variant="secondary" size="sm" onClick={() => setModelModalOpen(true)}>
|
||||||
|
<span className="material-symbols-outlined mr-1 text-[16px]">add</span>
|
||||||
|
Add model
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{selectedModels.length ? (
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
{selectedModels.map((model) => {
|
||||||
|
const thinkingLevels = getThinkingLevelsForModel(model);
|
||||||
|
return (
|
||||||
|
<div key={model} className="flex flex-wrap items-center gap-2 rounded-lg border border-border bg-bg-secondary px-3 py-2">
|
||||||
|
<span className="min-w-0 flex-1 break-all text-xs text-text-main">{model}</span>
|
||||||
|
{thinkingLevels && (
|
||||||
|
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
||||||
|
Reasoning / thinking
|
||||||
|
<select
|
||||||
|
value={coworkThinking[model] || ""}
|
||||||
|
onChange={(event) => setCoworkThinking((current) => ({ ...current, [model]: event.target.value }))}
|
||||||
|
className="min-w-28 rounded-lg border border-border bg-bg-primary px-2 py-1.5 text-xs text-text-main outline-none focus:border-primary"
|
||||||
|
>
|
||||||
|
<option value="">Default</option>
|
||||||
|
{thinkingLevels.map((level) => <option key={level} value={level}>{level === "none" ? "Disabled" : level}</option>)}
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
)}
|
||||||
|
<Button type="button" variant="ghost" size="sm" onClick={() => removeCoworkModel(model)} aria-label={`Remove ${model}`}>Remove</Button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : <p className="text-xs text-text-muted">No model selected. The generated file uses <code>{DEFAULT_MODEL}</code> as a placeholder.</p>}
|
||||||
|
<p className="text-xs text-text-muted">Cowork exposes every configured model in its picker. For models that support it, select a reasoning level to append it to the routed model ID.</p>
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="flex items-center justify-between gap-3">
|
<div className="flex items-center justify-between gap-3">
|
||||||
@@ -304,13 +412,13 @@ export default function ConfigGeneratorCard({
|
|||||||
<ModelSelectModal
|
<ModelSelectModal
|
||||||
isOpen={modelModalOpen}
|
isOpen={modelModalOpen}
|
||||||
onClose={() => { setModelModalOpen(false); setClaudeModelSlot(""); }}
|
onClose={() => { setModelModalOpen(false); setClaudeModelSlot(""); }}
|
||||||
onSelect={toolId === "claude" ? selectClaudeModel : toolId === "codex" ? selectCodexModel : addModel}
|
onSelect={toolId === "claude" ? selectClaudeModel : toolId === "codex" ? selectCodexModel : toolId === "opencode" ? selectOpenCodeModel : addModel}
|
||||||
selectedModel=""
|
selectedModel=""
|
||||||
activeProviders={activeProviders}
|
activeProviders={activeProviders}
|
||||||
title={toolId === "claude" && claudeModelSlot ? `Select ${claudeModelSlot} model` : toolId === "codex" ? "Select Codex model" : `Add model for ${tool.name}`}
|
title={toolId === "claude" && claudeModelSlot ? `Select ${claudeModelSlot} model` : toolId === "codex" ? "Select Codex model" : toolId === "opencode" ? "Add OpenCode model" : `Add model for ${tool.name}`}
|
||||||
closeOnSelect={toolId === "claude" || toolId === "codex"}
|
closeOnSelect={toolId === "claude" || toolId === "codex"}
|
||||||
addedModelValues={toolId === "claude" ? Object.values(claudeModels).filter(Boolean) : toolId === "codex" ? [codexModel].filter(Boolean) : selectedModels}
|
addedModelValues={toolId === "claude" ? Object.values(claudeModels).filter(Boolean) : toolId === "codex" ? [codexModel].filter(Boolean) : toolId === "opencode" ? opencodeModels : selectedModels}
|
||||||
availableModels={toolId === "claude" || toolId === "codex" ? connectedModels : null}
|
availableModels={toolId === "claude" || toolId === "codex" || toolId === "opencode" || toolId === "cowork" ? connectedModels : null}
|
||||||
/>
|
/>
|
||||||
<ManualConfigModal isOpen={configModalOpen} onClose={() => setConfigModalOpen(false)} title={`${tool.name} configuration`} configs={configs} />
|
<ManualConfigModal isOpen={configModalOpen} onClose={() => setConfigModalOpen(false)} title={`${tool.name} configuration`} configs={configs} />
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -1,21 +1,44 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Card, ModelSelectModal } from "@/shared/components";
|
import { Card, ModelSelectModal } from "@/shared/components";
|
||||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import ApiKeySelect from "./ApiKeySelect";
|
import ApiKeySelect from "./ApiKeySelect";
|
||||||
|
|
||||||
export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, baseUrl, apiKeys, activeProviders = [], cloudEnabled = false, tunnelEnabled = false }) {
|
export default function DefaultToolCard({ toolId, tool, baseUrl, apiKeys, activeProviders = [], cloudEnabled = false, tunnelEnabled = false }) {
|
||||||
const [copiedField, setCopiedField] = useState(null);
|
const [copiedField, setCopiedField] = useState(null);
|
||||||
const [showModelModal, setShowModelModal] = useState(false);
|
const [showModelModal, setShowModelModal] = useState(false);
|
||||||
const [modelValue, setModelValue] = useState("");
|
const [modelValue, setModelValue] = useState("");
|
||||||
|
const [selectedModels, setSelectedModels] = useState([]);
|
||||||
|
const [connectedModels, setConnectedModels] = useState(null);
|
||||||
|
const [isExpanded, setIsExpanded] = useState(true);
|
||||||
|
|
||||||
// Initialize state directly with computed value - no need for useEffect
|
// Initialize state directly with computed value - no need for useEffect
|
||||||
const [selectedApiKey, setSelectedApiKey] = useState(() =>
|
const [selectedApiKey, setSelectedApiKey] = useState(() =>
|
||||||
apiKeys?.length > 0 ? apiKeys[0].key : ""
|
apiKeys?.length > 0 ? apiKeys[0].key : ""
|
||||||
);
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (toolId !== "cursor") return;
|
||||||
|
|
||||||
|
let cancelled = false;
|
||||||
|
const loadConnectedModels = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch("/api/models/connected", { cache: "no-store" });
|
||||||
|
if (!response.ok) throw new Error("Failed to load connected models");
|
||||||
|
const data = await response.json();
|
||||||
|
if (!cancelled) setConnectedModels(data.models || []);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(`Error loading connected models for ${tool.name}:`, error);
|
||||||
|
if (!cancelled) setConnectedModels([]);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
loadConnectedModels();
|
||||||
|
return () => { cancelled = true; };
|
||||||
|
}, [toolId, tool.name]);
|
||||||
|
|
||||||
const replaceVars = (text) => {
|
const replaceVars = (text) => {
|
||||||
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
const keyToUse = (selectedApiKey && selectedApiKey.trim())
|
||||||
? selectedApiKey
|
? selectedApiKey
|
||||||
@@ -42,9 +65,19 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectModel = (model) => {
|
const handleSelectModel = (model) => {
|
||||||
|
if (!model?.value) return;
|
||||||
setModelValue(model.value);
|
setModelValue(model.value);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const addSelectedModel = (model) => {
|
||||||
|
if (!model?.value || selectedModels.includes(model.value)) return;
|
||||||
|
setSelectedModels((current) => [...current, model.value]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const removeSelectedModel = (model) => {
|
||||||
|
setSelectedModels((current) => current.filter((value) => value !== model.value));
|
||||||
|
};
|
||||||
|
|
||||||
const hasActiveProviders = activeProviders.length > 0;
|
const hasActiveProviders = activeProviders.length > 0;
|
||||||
|
|
||||||
const renderApiKeySelector = () => (
|
const renderApiKeySelector = () => (
|
||||||
@@ -53,7 +86,42 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderModelSelector = () => {
|
const renderModelSelector = (item) => {
|
||||||
|
if (item.multiple) {
|
||||||
|
return (
|
||||||
|
<div className="mt-2 flex flex-col gap-2">
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<span className="text-xs text-text-muted">Custom models to add in Cursor</span>
|
||||||
|
<button
|
||||||
|
onClick={() => setShowModelModal(true)}
|
||||||
|
disabled={!hasActiveProviders}
|
||||||
|
className={`shrink-0 px-3 py-2 rounded-lg border text-sm transition-colors ${
|
||||||
|
hasActiveProviders
|
||||||
|
? "bg-bg-secondary border-border text-text-main hover:border-primary cursor-pointer"
|
||||||
|
: "opacity-50 cursor-not-allowed border-border"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
Add model
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{selectedModels.length ? (
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{selectedModels.map((model) => (
|
||||||
|
<div key={model} className="inline-flex max-w-full items-center gap-1 rounded-full border border-border bg-bg-secondary py-1 pl-2 pr-1 text-xs text-text-main">
|
||||||
|
<button type="button" onClick={() => handleCopy(model, `model-${model}`)} className="min-w-0 truncate hover:text-primary" title="Copy model ID">
|
||||||
|
{model}
|
||||||
|
</button>
|
||||||
|
<button type="button" onClick={() => setSelectedModels((current) => current.filter((value) => value !== model))} className="rounded-full p-1 hover:text-red-500" title="Remove model" aria-label={`Remove ${model}`}>
|
||||||
|
<span className="material-symbols-outlined block text-[14px]">close</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : <p className="text-xs text-text-muted">Select every 9Router model you plan to add as a Cursor custom model.</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
<input
|
<input
|
||||||
@@ -161,7 +229,7 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
<p className="font-medium text-text">{item.title}</p>
|
<p className="font-medium text-text">{item.title}</p>
|
||||||
{item.desc && <p className="text-sm text-text-muted mt-0.5">{item.desc}</p>}
|
{item.desc && <p className="text-sm text-text-muted mt-0.5">{item.desc}</p>}
|
||||||
{item.type === "apiKeySelector" && renderApiKeySelector()}
|
{item.type === "apiKeySelector" && renderApiKeySelector()}
|
||||||
{item.type === "modelSelector" && renderModelSelector()}
|
{item.type === "modelSelector" && renderModelSelector(item)}
|
||||||
{item.value && (
|
{item.value && (
|
||||||
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
<div className="mt-2 flex flex-col sm:flex-row sm:items-center gap-2">
|
||||||
<code className="w-full sm:w-auto flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm font-mono border border-border truncate">
|
<code className="w-full sm:w-auto flex-1 px-3 py-2 bg-bg-secondary rounded-lg text-sm font-mono border border-border truncate">
|
||||||
@@ -238,7 +306,12 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Card padding="xs" className="overflow-hidden overflow-x-hidden">
|
<Card padding="xs" className="overflow-hidden overflow-x-hidden">
|
||||||
<div className="flex items-center justify-between hover:cursor-pointer" onClick={onToggle}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="flex w-full items-center justify-between text-left hover:cursor-pointer"
|
||||||
|
onClick={() => setIsExpanded((expanded) => !expanded)}
|
||||||
|
aria-expanded={isExpanded}
|
||||||
|
>
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
<div className="size-8 rounded-lg flex items-center justify-center shrink-0">
|
||||||
{renderIcon()}
|
{renderIcon()}
|
||||||
@@ -249,7 +322,7 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
<span className={`material-symbols-outlined text-text-muted text-[20px] transition-transform ${isExpanded ? "rotate-180" : ""}`}>expand_more</span>
|
||||||
</div>
|
</button>
|
||||||
|
|
||||||
{isExpanded && (
|
{isExpanded && (
|
||||||
<div className="mt-6 pt-6 border-t border-border">
|
<div className="mt-6 pt-6 border-t border-border">
|
||||||
@@ -260,10 +333,14 @@ export default function DefaultToolCard({ toolId, tool, isExpanded, onToggle, ba
|
|||||||
<ModelSelectModal
|
<ModelSelectModal
|
||||||
isOpen={showModelModal}
|
isOpen={showModelModal}
|
||||||
onClose={() => setShowModelModal(false)}
|
onClose={() => setShowModelModal(false)}
|
||||||
onSelect={handleSelectModel}
|
onSelect={tool.modelSelection === "multiple" ? addSelectedModel : handleSelectModel}
|
||||||
|
onDeselect={tool.modelSelection === "multiple" ? removeSelectedModel : undefined}
|
||||||
selectedModel={modelValue}
|
selectedModel={modelValue}
|
||||||
activeProviders={activeProviders}
|
activeProviders={activeProviders}
|
||||||
title="Select Model"
|
title={tool.modelSelection === "multiple" ? "Add Cursor custom model" : "Select Model"}
|
||||||
|
closeOnSelect={tool.modelSelection !== "multiple"}
|
||||||
|
addedModelValues={tool.modelSelection === "multiple" ? selectedModels : []}
|
||||||
|
availableModels={toolId === "cursor" ? connectedModels : null}
|
||||||
/>
|
/>
|
||||||
</Card>
|
</Card>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ export const CLI_TOOLS = {
|
|||||||
color: "#000000",
|
color: "#000000",
|
||||||
description: "Cursor AI Code Editor",
|
description: "Cursor AI Code Editor",
|
||||||
configType: "guide",
|
configType: "guide",
|
||||||
|
modelSelection: "multiple",
|
||||||
requiresExternalUrl: true,
|
requiresExternalUrl: true,
|
||||||
notes: [
|
notes: [
|
||||||
{ type: "warning", text: "Requires Cursor Pro account to use this feature." },
|
{ type: "warning", text: "Requires Cursor Pro account to use this feature." },
|
||||||
@@ -148,8 +149,8 @@ export const CLI_TOOLS = {
|
|||||||
{ step: 2, title: "Enable OpenAI API", desc: "Enable \"OpenAI API key\" option" },
|
{ step: 2, title: "Enable OpenAI API", desc: "Enable \"OpenAI API key\" option" },
|
||||||
{ step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
|
{ step: 3, title: "Base URL", value: "{{baseUrl}}", copyable: true },
|
||||||
{ step: 4, title: "API Key", type: "apiKeySelector" },
|
{ step: 4, title: "API Key", type: "apiKeySelector" },
|
||||||
{ step: 5, title: "Add Custom Model", desc: "Click \"View All Model\" → \"Add Custom Model\"" },
|
{ step: 5, title: "Add Custom Model", desc: "Click \"View All Models\" → \"Add Custom Model\". Repeat this step for each model selected below." },
|
||||||
{ step: 6, title: "Select Model", type: "modelSelector" },
|
{ step: 6, title: "Select Custom Models", desc: "Add the exact 9Router model IDs below to Cursor. Click a selected model ID to copy it.", type: "modelSelector", multiple: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
// HIDDEN: gemini-cli
|
// HIDDEN: gemini-cli
|
||||||
|
|||||||
Reference in New Issue
Block a user