mirror of
https://github.com/Nezumi-2711/9router.git
synced 2026-09-22 13:38:31 +00:00
fix: update the logic to update thinking in cli tools page
This commit is contained in:
@@ -67,7 +67,20 @@ const getDuplicateModelClass = ({ total, occurrence }) => (
|
|||||||
total > 1 ? DUPLICATE_MODEL_COLORS[(occurrence - 1) % DUPLICATE_MODEL_COLORS.length] : "border-border bg-bg-secondary"
|
total > 1 ? DUPLICATE_MODEL_COLORS[(occurrence - 1) % DUPLICATE_MODEL_COLORS.length] : "border-border bg-bg-secondary"
|
||||||
);
|
);
|
||||||
|
|
||||||
function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, claudeThinking = {}, codexModel = "", codexThinking = "", opencodeModels = [], opencodeDefaultModel = "", coworkThinking = {}, copilotTokens = {}, copilotThinking = {}, connectedModels = [] }) {
|
const createCoworkModelSettings = (models, savedSettings = [], legacyThinking = {}) => (
|
||||||
|
models.map((model, index) => ({
|
||||||
|
thinking: savedSettings[index]?.thinking || legacyThinking[model] || "",
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
const createCopilotModelSettings = (models, savedSettings = [], legacyThinking = {}, legacyTokens = {}) => (
|
||||||
|
models.map((model, index) => ({
|
||||||
|
thinking: savedSettings[index]?.thinking || legacyThinking[model] || "",
|
||||||
|
tokens: savedSettings[index]?.tokens || legacyTokens[model] || DEFAULT_MODEL_TOKEN_LIMITS,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
|
||||||
|
function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, claudeThinking = {}, codexModel = "", codexThinking = "", opencodeModels = [], opencodeDefaultModel = "", coworkModelSettings = [], copilotModelSettings = [], connectedModels = [] }) {
|
||||||
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];
|
||||||
@@ -123,7 +136,7 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
case "copilot":
|
case "copilot":
|
||||||
return [{
|
return [{
|
||||||
filename: "chatLanguageModels.json",
|
filename: "chatLanguageModels.json",
|
||||||
content: toJson(Object.values(selectedModels.reduce((groups, id) => {
|
content: toJson(Object.values(selectedModels.reduce((groups, id, index) => {
|
||||||
const connectedModel = connectedModels.find((item) => item.fullModel === id);
|
const connectedModel = connectedModels.find((item) => item.fullModel === id);
|
||||||
const providerId = connectedModel?.providerAlias || id.split("/")[0] || "9router";
|
const providerId = connectedModel?.providerAlias || id.split("/")[0] || "9router";
|
||||||
const providerName = connectedModel?.provider?.name || formatModelName(providerId);
|
const providerName = connectedModel?.provider?.name || formatModelName(providerId);
|
||||||
@@ -136,8 +149,9 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
apiKey: COPILOT_API_KEY_INPUT,
|
apiKey: COPILOT_API_KEY_INPUT,
|
||||||
models: [],
|
models: [],
|
||||||
};
|
};
|
||||||
const tokens = copilotTokens[id] || {};
|
const modelSettings = copilotModelSettings[index] || {};
|
||||||
const modelId = withThinkingLevel(id, copilotThinking[id]);
|
const tokens = modelSettings.tokens || {};
|
||||||
|
const modelId = withThinkingLevel(id, modelSettings.thinking);
|
||||||
const entry = {
|
const entry = {
|
||||||
id: modelId,
|
id: modelId,
|
||||||
name: formatModelName(modelId),
|
name: formatModelName(modelId),
|
||||||
@@ -146,7 +160,7 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
vision: true,
|
vision: true,
|
||||||
streaming: true,
|
streaming: true,
|
||||||
};
|
};
|
||||||
if (copilotThinking[id]) {
|
if (modelSettings.thinking) {
|
||||||
entry.thinking = true;
|
entry.thinking = true;
|
||||||
entry.reasoningEffortFormat = "chat-completions";
|
entry.reasoningEffortFormat = "chat-completions";
|
||||||
}
|
}
|
||||||
@@ -168,8 +182,8 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
|||||||
inferenceProvider: "gateway",
|
inferenceProvider: "gateway",
|
||||||
inferenceGatewayBaseUrl: endpoint,
|
inferenceGatewayBaseUrl: endpoint,
|
||||||
inferenceGatewayApiKey: apiKey,
|
inferenceGatewayApiKey: apiKey,
|
||||||
inferenceModels: selectedModels.map((name) => ({
|
inferenceModels: selectedModels.map((name, index) => ({
|
||||||
name: withThinkingLevel(name, coworkThinking[name]),
|
name: withThinkingLevel(name, coworkModelSettings[index]?.thinking),
|
||||||
})),
|
})),
|
||||||
}),
|
}),
|
||||||
}];
|
}];
|
||||||
@@ -211,9 +225,17 @@ export default function ConfigGeneratorCard({
|
|||||||
const [codexThinking, setCodexThinking] = useState(() => initialConfig?.codexThinking || "");
|
const [codexThinking, setCodexThinking] = useState(() => initialConfig?.codexThinking || "");
|
||||||
const [opencodeModels, setOpencodeModels] = useState(() => initialConfig?.opencodeModels || []);
|
const [opencodeModels, setOpencodeModels] = useState(() => initialConfig?.opencodeModels || []);
|
||||||
const [opencodeDefaultModel, setOpencodeDefaultModel] = useState(() => initialConfig?.opencodeDefaultModel || "");
|
const [opencodeDefaultModel, setOpencodeDefaultModel] = useState(() => initialConfig?.opencodeDefaultModel || "");
|
||||||
const [coworkThinking, setCoworkThinking] = useState(() => initialConfig?.coworkThinking || {});
|
const [coworkModelSettings, setCoworkModelSettings] = useState(() => createCoworkModelSettings(
|
||||||
const [copilotTokens, setCopilotTokens] = useState(() => initialConfig?.copilotTokens || {});
|
initialConfig?.selectedModels || [],
|
||||||
const [copilotThinking, setCopilotThinking] = useState(() => initialConfig?.copilotThinking || {});
|
initialConfig?.coworkModelSettings,
|
||||||
|
initialConfig?.coworkThinking,
|
||||||
|
));
|
||||||
|
const [copilotModelSettings, setCopilotModelSettings] = useState(() => createCopilotModelSettings(
|
||||||
|
initialConfig?.selectedModels || [],
|
||||||
|
initialConfig?.copilotModelSettings,
|
||||||
|
initialConfig?.copilotThinking,
|
||||||
|
initialConfig?.copilotTokens,
|
||||||
|
));
|
||||||
const connectedModels = availableModels;
|
const connectedModels = availableModels;
|
||||||
const [customBaseUrl, setCustomBaseUrl] = useState(() => resolveInitialCliToolBaseUrl(initialConfig?.baseUrl, baseUrl));
|
const [customBaseUrl, setCustomBaseUrl] = useState(() => resolveInitialCliToolBaseUrl(initialConfig?.baseUrl, baseUrl));
|
||||||
const [modelModalOpen, setModelModalOpen] = useState(false);
|
const [modelModalOpen, setModelModalOpen] = useState(false);
|
||||||
@@ -227,8 +249,8 @@ export default function ConfigGeneratorCard({
|
|||||||
? COPILOT_API_KEY_INPUT
|
? COPILOT_API_KEY_INPUT
|
||||||
: selectedApiKey.trim() || (cloudEnabled ? "<API_KEY_FROM_DASHBOARD>" : "sk_9router");
|
: 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, opencodeModels, opencodeDefaultModel, coworkThinking, copilotTokens, copilotThinking, connectedModels: connectedModels || [] }),
|
() => buildConfigs(toolId, { baseUrl: effectiveBaseUrl, apiKey, models: selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkModelSettings, copilotModelSettings, connectedModels: connectedModels || [] }),
|
||||||
[toolId, effectiveBaseUrl, apiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking, copilotTokens, copilotThinking, connectedModels]
|
[toolId, effectiveBaseUrl, apiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkModelSettings, copilotModelSettings, connectedModels]
|
||||||
);
|
);
|
||||||
|
|
||||||
const buildPersistableConfig = () => {
|
const buildPersistableConfig = () => {
|
||||||
@@ -242,8 +264,8 @@ export default function ConfigGeneratorCard({
|
|||||||
if (toolId === "claude") Object.assign(config, { claudeModels, claudeThinking });
|
if (toolId === "claude") Object.assign(config, { claudeModels, claudeThinking });
|
||||||
if (toolId === "codex") Object.assign(config, { codexModel, codexThinking });
|
if (toolId === "codex") Object.assign(config, { codexModel, codexThinking });
|
||||||
if (toolId === "opencode") Object.assign(config, { opencodeModels, opencodeDefaultModel });
|
if (toolId === "opencode") Object.assign(config, { opencodeModels, opencodeDefaultModel });
|
||||||
if (toolId === "cowork") Object.assign(config, { selectedModels, coworkThinking });
|
if (toolId === "cowork") Object.assign(config, { selectedModels, coworkModelSettings });
|
||||||
if (toolId === "copilot") Object.assign(config, { selectedModels, copilotThinking, copilotTokens });
|
if (toolId === "copilot") Object.assign(config, { selectedModels, copilotModelSettings });
|
||||||
return config;
|
return config;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -266,7 +288,7 @@ export default function ConfigGeneratorCard({
|
|||||||
}
|
}
|
||||||
setSaveStatus((current) => current === "saving" ? current : "dirty");
|
setSaveStatus((current) => current === "saving" ? current : "dirty");
|
||||||
setSaveError("");
|
setSaveError("");
|
||||||
}, [effectiveBaseUrl, apiKeyMode, selectedApiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking, copilotTokens, copilotThinking]);
|
}, [effectiveBaseUrl, apiKeyMode, selectedApiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkModelSettings, copilotModelSettings]);
|
||||||
|
|
||||||
const getThinkingLevelsForModel = (fullModel) => {
|
const getThinkingLevelsForModel = (fullModel) => {
|
||||||
const connectedModel = connectedModels?.find((model) => model.fullModel === fullModel);
|
const connectedModel = connectedModels?.find((model) => model.fullModel === fullModel);
|
||||||
@@ -274,7 +296,7 @@ export default function ConfigGeneratorCard({
|
|||||||
return getThinkingLevels(connectedModel.provider.id, connectedModel.model);
|
return getThinkingLevels(connectedModel.provider.id, connectedModel.model);
|
||||||
};
|
};
|
||||||
|
|
||||||
const loadCopilotTokenLimits = async (modelIds) => {
|
const loadCopilotTokenLimits = async (modelIds, modelsForSettings = selectedModels) => {
|
||||||
try {
|
try {
|
||||||
const response = await fetch("/api/models/token-limits", {
|
const response = await fetch("/api/models/token-limits", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -284,14 +306,16 @@ export default function ConfigGeneratorCard({
|
|||||||
if (!response.ok) throw new Error("Failed to load models.dev token limits");
|
if (!response.ok) throw new Error("Failed to load models.dev token limits");
|
||||||
|
|
||||||
const { limits = {} } = await response.json();
|
const { limits = {} } = await response.json();
|
||||||
setCopilotTokens((current) => Object.entries(limits).reduce((next, [model, limit]) => {
|
setCopilotModelSettings((current) => current.map((settings, index) => {
|
||||||
const currentLimit = current[model];
|
const model = modelsForSettings[index];
|
||||||
|
const limit = limits[model];
|
||||||
|
if (!limit) return settings;
|
||||||
|
const currentLimit = settings.tokens;
|
||||||
const hasUserOverride = currentLimit
|
const hasUserOverride = currentLimit
|
||||||
&& (currentLimit.maxInputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxInputTokens
|
&& (currentLimit.maxInputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxInputTokens
|
||||||
|| currentLimit.maxOutputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxOutputTokens);
|
|| currentLimit.maxOutputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxOutputTokens);
|
||||||
next[model] = hasUserOverride ? currentLimit : limit;
|
return { ...settings, tokens: hasUserOverride ? currentLimit : limit };
|
||||||
return next;
|
}));
|
||||||
}, { ...current }));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log("Error loading models.dev token limits:", error);
|
console.log("Error loading models.dev token limits:", error);
|
||||||
}
|
}
|
||||||
@@ -299,42 +323,23 @@ export default function ConfigGeneratorCard({
|
|||||||
|
|
||||||
const addModel = (selected) => {
|
const addModel = (selected) => {
|
||||||
if (!selected?.value) return;
|
if (!selected?.value) return;
|
||||||
setSelectedModels((current) => [...current, selected.value]);
|
const nextModels = [...selectedModels, selected.value];
|
||||||
if (toolId === "cowork") setCoworkThinking((current) => ({ ...current, [selected.value]: "" }));
|
setSelectedModels(nextModels);
|
||||||
|
if (toolId === "cowork") setCoworkModelSettings((current) => [...current, { thinking: "" }]);
|
||||||
if (toolId === "copilot") {
|
if (toolId === "copilot") {
|
||||||
setCopilotThinking((current) => ({ ...current, [selected.value]: "" }));
|
setCopilotModelSettings((current) => [...current, { thinking: "", tokens: DEFAULT_MODEL_TOKEN_LIMITS }]);
|
||||||
setCopilotTokens((current) => ({ ...current, [selected.value]: DEFAULT_MODEL_TOKEN_LIMITS }));
|
loadCopilotTokenLimits([selected.value], nextModels);
|
||||||
loadCopilotTokenLimits([selected.value]);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCoworkModel = (model, index) => {
|
const removeCoworkModel = (index) => {
|
||||||
const remainingModels = selectedModels.filter((_, currentIndex) => currentIndex !== index);
|
setSelectedModels((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||||
setSelectedModels(remainingModels);
|
setCoworkModelSettings((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||||
if (!remainingModels.includes(model)) {
|
|
||||||
setCoworkThinking((current) => {
|
|
||||||
const remainingThinking = { ...current };
|
|
||||||
delete remainingThinking[model];
|
|
||||||
return remainingThinking;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const removeCopilotModel = (model, index) => {
|
const removeCopilotModel = (index) => {
|
||||||
const remainingModels = selectedModels.filter((_, currentIndex) => currentIndex !== index);
|
setSelectedModels((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||||
setSelectedModels(remainingModels);
|
setCopilotModelSettings((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||||
if (!remainingModels.includes(model)) {
|
|
||||||
setCopilotThinking((current) => {
|
|
||||||
const remaining = { ...current };
|
|
||||||
delete remaining[model];
|
|
||||||
return remaining;
|
|
||||||
});
|
|
||||||
setCopilotTokens((current) => {
|
|
||||||
const remaining = { ...current };
|
|
||||||
delete remaining[model];
|
|
||||||
return remaining;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectClaudeModel = (selected) => {
|
const selectClaudeModel = (selected) => {
|
||||||
@@ -530,8 +535,10 @@ export default function ConfigGeneratorCard({
|
|||||||
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
||||||
Reasoning / thinking
|
Reasoning / thinking
|
||||||
<select
|
<select
|
||||||
value={coworkThinking[model] || ""}
|
value={coworkModelSettings[index]?.thinking || ""}
|
||||||
onChange={(event) => setCoworkThinking((current) => ({ ...current, [model]: event.target.value }))}
|
onChange={(event) => setCoworkModelSettings((current) => current.map((settings, currentIndex) => (
|
||||||
|
currentIndex === index ? { ...settings, thinking: event.target.value } : settings
|
||||||
|
)))}
|
||||||
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"
|
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>
|
<option value="">Default</option>
|
||||||
@@ -539,7 +546,7 @@ export default function ConfigGeneratorCard({
|
|||||||
</select>
|
</select>
|
||||||
</label>
|
</label>
|
||||||
)}
|
)}
|
||||||
<Button type="button" variant="ghost" size="sm" onClick={() => removeCoworkModel(model, index)} aria-label={`Remove ${model} instance ${index + 1}`}>Remove</Button>
|
<Button type="button" variant="ghost" size="sm" onClick={() => removeCoworkModel(index)} aria-label={`Remove ${model} instance ${index + 1}`}>Remove</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -560,7 +567,8 @@ export default function ConfigGeneratorCard({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
{selectedModels.map((model, index) => {
|
{selectedModels.map((model, index) => {
|
||||||
const thinkingLevels = getThinkingLevelsForModel(model);
|
const thinkingLevels = getThinkingLevelsForModel(model);
|
||||||
const tokens = copilotTokens[model] || DEFAULT_MODEL_TOKEN_LIMITS;
|
const modelSettings = copilotModelSettings[index] || {};
|
||||||
|
const tokens = modelSettings.tokens || DEFAULT_MODEL_TOKEN_LIMITS;
|
||||||
const inputOptions = getInputTokenOptions(tokens);
|
const inputOptions = getInputTokenOptions(tokens);
|
||||||
const outputOptions = getOutputTokenOptions(tokens);
|
const outputOptions = getOutputTokenOptions(tokens);
|
||||||
const occurrence = getModelOccurrence(selectedModels, model, index);
|
const occurrence = getModelOccurrence(selectedModels, model, index);
|
||||||
@@ -568,14 +576,16 @@ export default function ConfigGeneratorCard({
|
|||||||
<div key={`${model}-${index}`} className={`flex flex-col gap-2 rounded-lg border px-3 py-2 ${getDuplicateModelClass(occurrence)}`}>
|
<div key={`${model}-${index}`} className={`flex flex-col gap-2 rounded-lg border px-3 py-2 ${getDuplicateModelClass(occurrence)}`}>
|
||||||
<div className="flex items-center justify-between gap-2">
|
<div className="flex items-center justify-between gap-2">
|
||||||
<span className="min-w-0 flex-1 break-all text-xs font-medium text-text-main">{model}{occurrence.total > 1 && <span className="ml-1 text-[10px] font-bold opacity-70">#{occurrence.occurrence}</span>}</span>
|
<span className="min-w-0 flex-1 break-all text-xs font-medium text-text-main">{model}{occurrence.total > 1 && <span className="ml-1 text-[10px] font-bold opacity-70">#{occurrence.occurrence}</span>}</span>
|
||||||
<Button type="button" variant="ghost" size="sm" onClick={() => removeCopilotModel(model, index)} aria-label={`Remove ${model} instance ${index + 1}`}>Remove</Button>
|
<Button type="button" variant="ghost" size="sm" onClick={() => removeCopilotModel(index)} aria-label={`Remove ${model} instance ${index + 1}`}>Remove</Button>
|
||||||
</div>
|
</div>
|
||||||
{thinkingLevels && (
|
{thinkingLevels && (
|
||||||
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
||||||
Reasoning / thinking
|
Reasoning / thinking
|
||||||
<select
|
<select
|
||||||
value={copilotThinking[model] || ""}
|
value={modelSettings.thinking || ""}
|
||||||
onChange={(event) => setCopilotThinking((current) => ({ ...current, [model]: event.target.value }))}
|
onChange={(event) => setCopilotModelSettings((current) => current.map((settings, currentIndex) => (
|
||||||
|
currentIndex === index ? { ...settings, thinking: event.target.value } : settings
|
||||||
|
)))}
|
||||||
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"
|
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>
|
<option value="">Default</option>
|
||||||
@@ -588,10 +598,11 @@ export default function ConfigGeneratorCard({
|
|||||||
maxInputTokens
|
maxInputTokens
|
||||||
<select
|
<select
|
||||||
value={tokens.maxInputTokens || ""}
|
value={tokens.maxInputTokens || ""}
|
||||||
onChange={(event) => setCopilotTokens((current) => ({
|
onChange={(event) => setCopilotModelSettings((current) => current.map((settings, currentIndex) => (
|
||||||
...current,
|
currentIndex === index
|
||||||
[model]: { ...current[model], maxInputTokens: event.target.value ? Number(event.target.value) : undefined },
|
? { ...settings, tokens: { ...settings.tokens, maxInputTokens: event.target.value ? Number(event.target.value) : undefined } }
|
||||||
}))}
|
: settings
|
||||||
|
)))}
|
||||||
className="min-w-20 rounded-lg border border-border bg-bg-primary px-2 py-1.5 text-xs text-text-main outline-none focus:border-primary"
|
className="min-w-20 rounded-lg border border-border bg-bg-primary px-2 py-1.5 text-xs text-text-main outline-none focus:border-primary"
|
||||||
>
|
>
|
||||||
{inputOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
{inputOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
||||||
@@ -601,10 +612,11 @@ export default function ConfigGeneratorCard({
|
|||||||
maxOutputTokens
|
maxOutputTokens
|
||||||
<select
|
<select
|
||||||
value={tokens.maxOutputTokens || ""}
|
value={tokens.maxOutputTokens || ""}
|
||||||
onChange={(event) => setCopilotTokens((current) => ({
|
onChange={(event) => setCopilotModelSettings((current) => current.map((settings, currentIndex) => (
|
||||||
...current,
|
currentIndex === index
|
||||||
[model]: { ...current[model], maxOutputTokens: event.target.value ? Number(event.target.value) : undefined },
|
? { ...settings, tokens: { ...settings.tokens, maxOutputTokens: event.target.value ? Number(event.target.value) : undefined } }
|
||||||
}))}
|
: settings
|
||||||
|
)))}
|
||||||
className="min-w-20 rounded-lg border border-border bg-bg-primary px-2 py-1.5 text-xs text-text-main outline-none focus:border-primary"
|
className="min-w-20 rounded-lg border border-border bg-bg-primary px-2 py-1.5 text-xs text-text-main outline-none focus:border-primary"
|
||||||
>
|
>
|
||||||
{outputOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
{outputOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
||||||
|
|||||||
@@ -188,9 +188,14 @@ function cleanCliToolConfigsSync(db, providerAliases, modelId) {
|
|||||||
|
|
||||||
for (const field of ["opencodeModels", "selectedModels"]) {
|
for (const field of ["opencodeModels", "selectedModels"]) {
|
||||||
if (!Array.isArray(config[field])) continue;
|
if (!Array.isArray(config[field])) continue;
|
||||||
const removedModels = config[field].filter(deleted);
|
const removedIndexes = config[field].flatMap((reference, index) => (deleted(reference) ? [index] : []));
|
||||||
if (removedModels.length === 0) continue;
|
if (removedIndexes.length === 0) continue;
|
||||||
config[field] = config[field].filter((reference) => !deleted(reference));
|
const removedModels = removedIndexes.map((index) => config[field][index]);
|
||||||
|
config[field] = config[field].filter((_, index) => !removedIndexes.includes(index));
|
||||||
|
for (const settingsField of ["coworkModelSettings", "copilotModelSettings"]) {
|
||||||
|
if (!Array.isArray(config[settingsField])) continue;
|
||||||
|
config[settingsField] = config[settingsField].filter((_, index) => !removedIndexes.includes(index));
|
||||||
|
}
|
||||||
for (const mapField of ["coworkThinking", "copilotThinking", "copilotTokens"]) {
|
for (const mapField of ["coworkThinking", "copilotThinking", "copilotTokens"]) {
|
||||||
if (!config[mapField] || typeof config[mapField] !== "object") continue;
|
if (!config[mapField] || typeof config[mapField] !== "object") continue;
|
||||||
const nextMap = { ...config[mapField] };
|
const nextMap = { ...config[mapField] };
|
||||||
|
|||||||
@@ -89,6 +89,22 @@ function normalizeThinkingMap(value, allowedKeys, field) {
|
|||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeTokenLimits(value, field) {
|
||||||
|
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||||
|
throw new CliToolConfigValidationError(`${field} must be an object`);
|
||||||
|
}
|
||||||
|
const normalized = {};
|
||||||
|
for (const tokenField of ["maxInputTokens", "maxOutputTokens"]) {
|
||||||
|
if (value[tokenField] === undefined || value[tokenField] === null || value[tokenField] === "") continue;
|
||||||
|
const number = Number(value[tokenField]);
|
||||||
|
if (!Number.isSafeInteger(number) || number <= 0 || number > MAX_TOKEN_LIMIT) {
|
||||||
|
throw new CliToolConfigValidationError(`${tokenField} must be a positive integer no greater than ${MAX_TOKEN_LIMIT}`);
|
||||||
|
}
|
||||||
|
normalized[tokenField] = number;
|
||||||
|
}
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
function normalizeApiKeyReference(input) {
|
function normalizeApiKeyReference(input) {
|
||||||
const mode = input.apiKeyMode === undefined ? "managed" : input.apiKeyMode;
|
const mode = input.apiKeyMode === undefined ? "managed" : input.apiKeyMode;
|
||||||
if (!['managed', 'custom'].includes(mode)) {
|
if (!['managed', 'custom'].includes(mode)) {
|
||||||
@@ -98,30 +114,43 @@ function normalizeApiKeyReference(input) {
|
|||||||
return { apiKeyMode: mode, apiKeyId: mode === "managed" ? apiKeyId : null };
|
return { apiKeyMode: mode, apiKeyId: mode === "managed" ? apiKeyId : null };
|
||||||
}
|
}
|
||||||
|
|
||||||
function normalizeTokenMap(value, selectedModels) {
|
function normalizeCoworkModelSettings(value, selectedModels, legacyThinking) {
|
||||||
if (value === undefined) return {};
|
if (value !== undefined && !Array.isArray(value)) {
|
||||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
throw new CliToolConfigValidationError("coworkModelSettings must be an array");
|
||||||
|
}
|
||||||
|
const legacySettings = normalizeThinkingMap(legacyThinking, selectedModels, "coworkThinking");
|
||||||
|
return selectedModels.map((model, index) => {
|
||||||
|
const setting = value?.[index];
|
||||||
|
if (setting !== undefined && (!setting || typeof setting !== "object" || Array.isArray(setting))) {
|
||||||
|
throw new CliToolConfigValidationError(`coworkModelSettings.${index} must be an object`);
|
||||||
|
}
|
||||||
|
const thinking = normalizeThinking(setting?.thinking ?? legacySettings[model], `coworkModelSettings.${index}.thinking`);
|
||||||
|
return thinking ? { thinking } : {};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCopilotModelSettings(value, selectedModels, legacyThinking, legacyTokens) {
|
||||||
|
if (value !== undefined && !Array.isArray(value)) {
|
||||||
|
throw new CliToolConfigValidationError("copilotModelSettings must be an array");
|
||||||
|
}
|
||||||
|
const legacyThinkingSettings = normalizeThinkingMap(legacyThinking, selectedModels, "copilotThinking");
|
||||||
|
const legacyTokenSettings = legacyTokens === undefined ? {} : legacyTokens;
|
||||||
|
if (!legacyTokenSettings || typeof legacyTokenSettings !== "object" || Array.isArray(legacyTokenSettings)) {
|
||||||
throw new CliToolConfigValidationError("copilotTokens must be an object");
|
throw new CliToolConfigValidationError("copilotTokens must be an object");
|
||||||
}
|
}
|
||||||
const normalized = {};
|
return selectedModels.map((model, index) => {
|
||||||
for (const model of selectedModels) {
|
const setting = value?.[index];
|
||||||
const limits = value[model];
|
if (setting !== undefined && (!setting || typeof setting !== "object" || Array.isArray(setting))) {
|
||||||
if (limits === undefined) continue;
|
throw new CliToolConfigValidationError(`copilotModelSettings.${index} must be an object`);
|
||||||
if (!limits || typeof limits !== "object" || Array.isArray(limits)) {
|
|
||||||
throw new CliToolConfigValidationError(`copilotTokens.${model} must be an object`);
|
|
||||||
}
|
}
|
||||||
const next = {};
|
const thinking = normalizeThinking(setting?.thinking ?? legacyThinkingSettings[model], `copilotModelSettings.${index}.thinking`);
|
||||||
for (const field of ["maxInputTokens", "maxOutputTokens"]) {
|
const tokensSource = setting?.tokens ?? legacyTokenSettings[model];
|
||||||
if (limits[field] === undefined || limits[field] === null || limits[field] === "") continue;
|
const tokens = tokensSource === undefined ? {} : normalizeTokenLimits(tokensSource, `copilotModelSettings.${index}.tokens`);
|
||||||
const number = Number(limits[field]);
|
return {
|
||||||
if (!Number.isSafeInteger(number) || number <= 0 || number > MAX_TOKEN_LIMIT) {
|
...(thinking ? { thinking } : {}),
|
||||||
throw new CliToolConfigValidationError(`${field} must be a positive integer no greater than ${MAX_TOKEN_LIMIT}`);
|
...(Object.keys(tokens).length ? { tokens } : {}),
|
||||||
}
|
};
|
||||||
next[field] = number;
|
});
|
||||||
}
|
|
||||||
if (Object.keys(next).length) normalized[model] = next;
|
|
||||||
}
|
|
||||||
return normalized;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCliToolConfig(toolId, input) {
|
export function normalizeCliToolConfig(toolId, input) {
|
||||||
@@ -155,13 +184,12 @@ export function normalizeCliToolConfig(toolId, input) {
|
|||||||
: (config.opencodeModels[0] || "");
|
: (config.opencodeModels[0] || "");
|
||||||
} else if (toolId === "cowork") {
|
} else if (toolId === "cowork") {
|
||||||
config.selectedModels = normalizeModels(input.selectedModels);
|
config.selectedModels = normalizeModels(input.selectedModels);
|
||||||
config.coworkThinking = normalizeThinkingMap(input.coworkThinking, config.selectedModels, "coworkThinking");
|
config.coworkModelSettings = normalizeCoworkModelSettings(input.coworkModelSettings, config.selectedModels, input.coworkThinking);
|
||||||
} else if (toolId === "cursor") {
|
} else if (toolId === "cursor") {
|
||||||
config.selectedModels = normalizeModels(input.selectedModels);
|
config.selectedModels = normalizeModels(input.selectedModels);
|
||||||
} else if (toolId === "copilot") {
|
} else if (toolId === "copilot") {
|
||||||
config.selectedModels = normalizeModels(input.selectedModels);
|
config.selectedModels = normalizeModels(input.selectedModels);
|
||||||
config.copilotThinking = normalizeThinkingMap(input.copilotThinking, config.selectedModels, "copilotThinking");
|
config.copilotModelSettings = normalizeCopilotModelSettings(input.copilotModelSettings, config.selectedModels, input.copilotThinking, input.copilotTokens);
|
||||||
config.copilotTokens = normalizeTokenMap(input.copilotTokens, config.selectedModels);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return config;
|
return config;
|
||||||
|
|||||||
@@ -54,7 +54,10 @@ describe("CLI tool configuration contract", () => {
|
|||||||
apiKeyMode: "managed",
|
apiKeyMode: "managed",
|
||||||
selectedModels: ["cc/a", "cc/a"],
|
selectedModels: ["cc/a", "cc/a"],
|
||||||
coworkThinking: { "cc/a": "high", "stale/model": "low" },
|
coworkThinking: { "cc/a": "high", "stale/model": "low" },
|
||||||
})).toMatchObject({ selectedModels: ["cc/a", "cc/a"], coworkThinking: { "cc/a": "high" } });
|
})).toMatchObject({
|
||||||
|
selectedModels: ["cc/a", "cc/a"],
|
||||||
|
coworkModelSettings: [{ thinking: "high" }, { thinking: "high" }],
|
||||||
|
});
|
||||||
|
|
||||||
expect(normalizeCliToolConfig("cursor", {
|
expect(normalizeCliToolConfig("cursor", {
|
||||||
apiKeyMode: "managed",
|
apiKeyMode: "managed",
|
||||||
@@ -73,11 +76,36 @@ describe("CLI tool configuration contract", () => {
|
|||||||
})).toEqual({
|
})).toEqual({
|
||||||
baseUrl: "https://router.example/v1",
|
baseUrl: "https://router.example/v1",
|
||||||
selectedModels: ["cc/a", "cc/a"],
|
selectedModels: ["cc/a", "cc/a"],
|
||||||
copilotThinking: { "cc/a": "high" },
|
copilotModelSettings: [
|
||||||
copilotTokens: { "cc/a": { maxInputTokens: 100000, maxOutputTokens: 32000 } },
|
{ thinking: "high", tokens: { maxInputTokens: 100000, maxOutputTokens: 32000 } },
|
||||||
|
{ thinking: "high", tokens: { maxInputTokens: 100000, maxOutputTokens: 32000 } },
|
||||||
|
],
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps duplicate model settings independent by occurrence", () => {
|
||||||
|
expect(normalizeCliToolConfig("copilot", {
|
||||||
|
baseUrl: "https://router.example/v1",
|
||||||
|
selectedModels: ["cc/a", "cc/a"],
|
||||||
|
copilotModelSettings: [
|
||||||
|
{ thinking: "low", tokens: { maxInputTokens: 16000 } },
|
||||||
|
{ thinking: "high", tokens: { maxOutputTokens: 32000 } },
|
||||||
|
],
|
||||||
|
})).toMatchObject({
|
||||||
|
copilotModelSettings: [
|
||||||
|
{ thinking: "low", tokens: { maxInputTokens: 16000 } },
|
||||||
|
{ thinking: "high", tokens: { maxOutputTokens: 32000 } },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(normalizeCliToolConfig("cowork", {
|
||||||
|
baseUrl: "https://router.example",
|
||||||
|
apiKeyMode: "managed",
|
||||||
|
selectedModels: ["cc/a", "cc/a"],
|
||||||
|
coworkModelSettings: [{ thinking: "low" }, { thinking: "high" }],
|
||||||
|
})).toMatchObject({ coworkModelSettings: [{ thinking: "low" }, { thinking: "high" }] });
|
||||||
|
});
|
||||||
|
|
||||||
it("rejects plaintext secrets, invalid URLs, and invalid token limits", () => {
|
it("rejects plaintext secrets, invalid URLs, and invalid token limits", () => {
|
||||||
expect(() => normalizeCliToolConfig("claude", {
|
expect(() => normalizeCliToolConfig("claude", {
|
||||||
baseUrl: "https://router.example",
|
baseUrl: "https://router.example",
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ describe("permanent model deletion", () => {
|
|||||||
});
|
});
|
||||||
expect((await db.getCliToolConfig(cliUser.id, "cowork")).config).toMatchObject({
|
expect((await db.getCliToolConfig(cliUser.id, "cowork")).config).toMatchObject({
|
||||||
selectedModels: [`${providerPrefix}/gpt-keep`],
|
selectedModels: [`${providerPrefix}/gpt-keep`],
|
||||||
coworkThinking: { [`${providerPrefix}/gpt-keep`]: "low" },
|
coworkModelSettings: [{ thinking: "low" }],
|
||||||
});
|
});
|
||||||
|
|
||||||
const history = await db.getUsageHistory({});
|
const history = await db.getUsageHistory({});
|
||||||
|
|||||||
Reference in New Issue
Block a user