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"
|
||||
);
|
||||
|
||||
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 selectedModels = models.length ? models : [DEFAULT_MODEL];
|
||||
const model = selectedModels[0];
|
||||
@@ -123,7 +136,7 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
||||
case "copilot":
|
||||
return [{
|
||||
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 providerId = connectedModel?.providerAlias || id.split("/")[0] || "9router";
|
||||
const providerName = connectedModel?.provider?.name || formatModelName(providerId);
|
||||
@@ -136,8 +149,9 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
||||
apiKey: COPILOT_API_KEY_INPUT,
|
||||
models: [],
|
||||
};
|
||||
const tokens = copilotTokens[id] || {};
|
||||
const modelId = withThinkingLevel(id, copilotThinking[id]);
|
||||
const modelSettings = copilotModelSettings[index] || {};
|
||||
const tokens = modelSettings.tokens || {};
|
||||
const modelId = withThinkingLevel(id, modelSettings.thinking);
|
||||
const entry = {
|
||||
id: modelId,
|
||||
name: formatModelName(modelId),
|
||||
@@ -146,7 +160,7 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
||||
vision: true,
|
||||
streaming: true,
|
||||
};
|
||||
if (copilotThinking[id]) {
|
||||
if (modelSettings.thinking) {
|
||||
entry.thinking = true;
|
||||
entry.reasoningEffortFormat = "chat-completions";
|
||||
}
|
||||
@@ -168,8 +182,8 @@ function buildConfigs(toolId, { baseUrl, apiKey, models, claudeModels = {}, clau
|
||||
inferenceProvider: "gateway",
|
||||
inferenceGatewayBaseUrl: endpoint,
|
||||
inferenceGatewayApiKey: apiKey,
|
||||
inferenceModels: selectedModels.map((name) => ({
|
||||
name: withThinkingLevel(name, coworkThinking[name]),
|
||||
inferenceModels: selectedModels.map((name, index) => ({
|
||||
name: withThinkingLevel(name, coworkModelSettings[index]?.thinking),
|
||||
})),
|
||||
}),
|
||||
}];
|
||||
@@ -211,9 +225,17 @@ export default function ConfigGeneratorCard({
|
||||
const [codexThinking, setCodexThinking] = useState(() => initialConfig?.codexThinking || "");
|
||||
const [opencodeModels, setOpencodeModels] = useState(() => initialConfig?.opencodeModels || []);
|
||||
const [opencodeDefaultModel, setOpencodeDefaultModel] = useState(() => initialConfig?.opencodeDefaultModel || "");
|
||||
const [coworkThinking, setCoworkThinking] = useState(() => initialConfig?.coworkThinking || {});
|
||||
const [copilotTokens, setCopilotTokens] = useState(() => initialConfig?.copilotTokens || {});
|
||||
const [copilotThinking, setCopilotThinking] = useState(() => initialConfig?.copilotThinking || {});
|
||||
const [coworkModelSettings, setCoworkModelSettings] = useState(() => createCoworkModelSettings(
|
||||
initialConfig?.selectedModels || [],
|
||||
initialConfig?.coworkModelSettings,
|
||||
initialConfig?.coworkThinking,
|
||||
));
|
||||
const [copilotModelSettings, setCopilotModelSettings] = useState(() => createCopilotModelSettings(
|
||||
initialConfig?.selectedModels || [],
|
||||
initialConfig?.copilotModelSettings,
|
||||
initialConfig?.copilotThinking,
|
||||
initialConfig?.copilotTokens,
|
||||
));
|
||||
const connectedModels = availableModels;
|
||||
const [customBaseUrl, setCustomBaseUrl] = useState(() => resolveInitialCliToolBaseUrl(initialConfig?.baseUrl, baseUrl));
|
||||
const [modelModalOpen, setModelModalOpen] = useState(false);
|
||||
@@ -227,8 +249,8 @@ export default function ConfigGeneratorCard({
|
||||
? COPILOT_API_KEY_INPUT
|
||||
: selectedApiKey.trim() || (cloudEnabled ? "<API_KEY_FROM_DASHBOARD>" : "sk_9router");
|
||||
const configs = useMemo(
|
||||
() => buildConfigs(toolId, { baseUrl: effectiveBaseUrl, apiKey, models: selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking, copilotTokens, copilotThinking, connectedModels: connectedModels || [] }),
|
||||
[toolId, effectiveBaseUrl, apiKey, selectedModels, claudeModels, claudeThinking, codexModel, codexThinking, opencodeModels, opencodeDefaultModel, coworkThinking, copilotTokens, copilotThinking, 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, coworkModelSettings, copilotModelSettings, connectedModels]
|
||||
);
|
||||
|
||||
const buildPersistableConfig = () => {
|
||||
@@ -242,8 +264,8 @@ export default function ConfigGeneratorCard({
|
||||
if (toolId === "claude") Object.assign(config, { claudeModels, claudeThinking });
|
||||
if (toolId === "codex") Object.assign(config, { codexModel, codexThinking });
|
||||
if (toolId === "opencode") Object.assign(config, { opencodeModels, opencodeDefaultModel });
|
||||
if (toolId === "cowork") Object.assign(config, { selectedModels, coworkThinking });
|
||||
if (toolId === "copilot") Object.assign(config, { selectedModels, copilotThinking, copilotTokens });
|
||||
if (toolId === "cowork") Object.assign(config, { selectedModels, coworkModelSettings });
|
||||
if (toolId === "copilot") Object.assign(config, { selectedModels, copilotModelSettings });
|
||||
return config;
|
||||
};
|
||||
|
||||
@@ -266,7 +288,7 @@ export default function ConfigGeneratorCard({
|
||||
}
|
||||
setSaveStatus((current) => current === "saving" ? current : "dirty");
|
||||
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 connectedModel = connectedModels?.find((model) => model.fullModel === fullModel);
|
||||
@@ -274,7 +296,7 @@ export default function ConfigGeneratorCard({
|
||||
return getThinkingLevels(connectedModel.provider.id, connectedModel.model);
|
||||
};
|
||||
|
||||
const loadCopilotTokenLimits = async (modelIds) => {
|
||||
const loadCopilotTokenLimits = async (modelIds, modelsForSettings = selectedModels) => {
|
||||
try {
|
||||
const response = await fetch("/api/models/token-limits", {
|
||||
method: "POST",
|
||||
@@ -284,14 +306,16 @@ export default function ConfigGeneratorCard({
|
||||
if (!response.ok) throw new Error("Failed to load models.dev token limits");
|
||||
|
||||
const { limits = {} } = await response.json();
|
||||
setCopilotTokens((current) => Object.entries(limits).reduce((next, [model, limit]) => {
|
||||
const currentLimit = current[model];
|
||||
setCopilotModelSettings((current) => current.map((settings, index) => {
|
||||
const model = modelsForSettings[index];
|
||||
const limit = limits[model];
|
||||
if (!limit) return settings;
|
||||
const currentLimit = settings.tokens;
|
||||
const hasUserOverride = currentLimit
|
||||
&& (currentLimit.maxInputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxInputTokens
|
||||
|| currentLimit.maxOutputTokens !== DEFAULT_MODEL_TOKEN_LIMITS.maxOutputTokens);
|
||||
next[model] = hasUserOverride ? currentLimit : limit;
|
||||
return next;
|
||||
}, { ...current }));
|
||||
return { ...settings, tokens: hasUserOverride ? currentLimit : limit };
|
||||
}));
|
||||
} catch (error) {
|
||||
console.log("Error loading models.dev token limits:", error);
|
||||
}
|
||||
@@ -299,42 +323,23 @@ export default function ConfigGeneratorCard({
|
||||
|
||||
const addModel = (selected) => {
|
||||
if (!selected?.value) return;
|
||||
setSelectedModels((current) => [...current, selected.value]);
|
||||
if (toolId === "cowork") setCoworkThinking((current) => ({ ...current, [selected.value]: "" }));
|
||||
const nextModels = [...selectedModels, selected.value];
|
||||
setSelectedModels(nextModels);
|
||||
if (toolId === "cowork") setCoworkModelSettings((current) => [...current, { thinking: "" }]);
|
||||
if (toolId === "copilot") {
|
||||
setCopilotThinking((current) => ({ ...current, [selected.value]: "" }));
|
||||
setCopilotTokens((current) => ({ ...current, [selected.value]: DEFAULT_MODEL_TOKEN_LIMITS }));
|
||||
loadCopilotTokenLimits([selected.value]);
|
||||
setCopilotModelSettings((current) => [...current, { thinking: "", tokens: DEFAULT_MODEL_TOKEN_LIMITS }]);
|
||||
loadCopilotTokenLimits([selected.value], nextModels);
|
||||
}
|
||||
};
|
||||
|
||||
const removeCoworkModel = (model, index) => {
|
||||
const remainingModels = selectedModels.filter((_, currentIndex) => currentIndex !== index);
|
||||
setSelectedModels(remainingModels);
|
||||
if (!remainingModels.includes(model)) {
|
||||
setCoworkThinking((current) => {
|
||||
const remainingThinking = { ...current };
|
||||
delete remainingThinking[model];
|
||||
return remainingThinking;
|
||||
});
|
||||
}
|
||||
const removeCoworkModel = (index) => {
|
||||
setSelectedModels((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||
setCoworkModelSettings((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||
};
|
||||
|
||||
const removeCopilotModel = (model, index) => {
|
||||
const remainingModels = selectedModels.filter((_, currentIndex) => currentIndex !== index);
|
||||
setSelectedModels(remainingModels);
|
||||
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 removeCopilotModel = (index) => {
|
||||
setSelectedModels((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||
setCopilotModelSettings((current) => current.filter((_, currentIndex) => currentIndex !== index));
|
||||
};
|
||||
|
||||
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">
|
||||
Reasoning / thinking
|
||||
<select
|
||||
value={coworkThinking[model] || ""}
|
||||
onChange={(event) => setCoworkThinking((current) => ({ ...current, [model]: event.target.value }))}
|
||||
value={coworkModelSettings[index]?.thinking || ""}
|
||||
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"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
@@ -539,7 +546,7 @@ export default function ConfigGeneratorCard({
|
||||
</select>
|
||||
</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>
|
||||
);
|
||||
})}
|
||||
@@ -560,7 +567,8 @@ export default function ConfigGeneratorCard({
|
||||
<div className="flex flex-col gap-2">
|
||||
{selectedModels.map((model, index) => {
|
||||
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 outputOptions = getOutputTokenOptions(tokens);
|
||||
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 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>
|
||||
<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>
|
||||
{thinkingLevels && (
|
||||
<label className="flex items-center gap-2 text-xs font-medium text-text-muted">
|
||||
Reasoning / thinking
|
||||
<select
|
||||
value={copilotThinking[model] || ""}
|
||||
onChange={(event) => setCopilotThinking((current) => ({ ...current, [model]: event.target.value }))}
|
||||
value={modelSettings.thinking || ""}
|
||||
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"
|
||||
>
|
||||
<option value="">Default</option>
|
||||
@@ -588,10 +598,11 @@ export default function ConfigGeneratorCard({
|
||||
maxInputTokens
|
||||
<select
|
||||
value={tokens.maxInputTokens || ""}
|
||||
onChange={(event) => setCopilotTokens((current) => ({
|
||||
...current,
|
||||
[model]: { ...current[model], maxInputTokens: event.target.value ? Number(event.target.value) : undefined },
|
||||
}))}
|
||||
onChange={(event) => setCopilotModelSettings((current) => current.map((settings, currentIndex) => (
|
||||
currentIndex === index
|
||||
? { ...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"
|
||||
>
|
||||
{inputOptions.map((opt) => <option key={opt.value} value={opt.value}>{opt.label}</option>)}
|
||||
@@ -601,10 +612,11 @@ export default function ConfigGeneratorCard({
|
||||
maxOutputTokens
|
||||
<select
|
||||
value={tokens.maxOutputTokens || ""}
|
||||
onChange={(event) => setCopilotTokens((current) => ({
|
||||
...current,
|
||||
[model]: { ...current[model], maxOutputTokens: event.target.value ? Number(event.target.value) : undefined },
|
||||
}))}
|
||||
onChange={(event) => setCopilotModelSettings((current) => current.map((settings, currentIndex) => (
|
||||
currentIndex === index
|
||||
? { ...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"
|
||||
>
|
||||
{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"]) {
|
||||
if (!Array.isArray(config[field])) continue;
|
||||
const removedModels = config[field].filter(deleted);
|
||||
if (removedModels.length === 0) continue;
|
||||
config[field] = config[field].filter((reference) => !deleted(reference));
|
||||
const removedIndexes = config[field].flatMap((reference, index) => (deleted(reference) ? [index] : []));
|
||||
if (removedIndexes.length === 0) continue;
|
||||
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"]) {
|
||||
if (!config[mapField] || typeof config[mapField] !== "object") continue;
|
||||
const nextMap = { ...config[mapField] };
|
||||
|
||||
@@ -89,6 +89,22 @@ function normalizeThinkingMap(value, allowedKeys, field) {
|
||||
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) {
|
||||
const mode = input.apiKeyMode === undefined ? "managed" : input.apiKeyMode;
|
||||
if (!['managed', 'custom'].includes(mode)) {
|
||||
@@ -98,30 +114,43 @@ function normalizeApiKeyReference(input) {
|
||||
return { apiKeyMode: mode, apiKeyId: mode === "managed" ? apiKeyId : null };
|
||||
}
|
||||
|
||||
function normalizeTokenMap(value, selectedModels) {
|
||||
if (value === undefined) return {};
|
||||
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
||||
function normalizeCoworkModelSettings(value, selectedModels, legacyThinking) {
|
||||
if (value !== undefined && !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");
|
||||
}
|
||||
const normalized = {};
|
||||
for (const model of selectedModels) {
|
||||
const limits = value[model];
|
||||
if (limits === undefined) continue;
|
||||
if (!limits || typeof limits !== "object" || Array.isArray(limits)) {
|
||||
throw new CliToolConfigValidationError(`copilotTokens.${model} must be an object`);
|
||||
return selectedModels.map((model, index) => {
|
||||
const setting = value?.[index];
|
||||
if (setting !== undefined && (!setting || typeof setting !== "object" || Array.isArray(setting))) {
|
||||
throw new CliToolConfigValidationError(`copilotModelSettings.${index} must be an object`);
|
||||
}
|
||||
const next = {};
|
||||
for (const field of ["maxInputTokens", "maxOutputTokens"]) {
|
||||
if (limits[field] === undefined || limits[field] === null || limits[field] === "") continue;
|
||||
const number = Number(limits[field]);
|
||||
if (!Number.isSafeInteger(number) || number <= 0 || number > MAX_TOKEN_LIMIT) {
|
||||
throw new CliToolConfigValidationError(`${field} must be a positive integer no greater than ${MAX_TOKEN_LIMIT}`);
|
||||
}
|
||||
next[field] = number;
|
||||
}
|
||||
if (Object.keys(next).length) normalized[model] = next;
|
||||
}
|
||||
return normalized;
|
||||
const thinking = normalizeThinking(setting?.thinking ?? legacyThinkingSettings[model], `copilotModelSettings.${index}.thinking`);
|
||||
const tokensSource = setting?.tokens ?? legacyTokenSettings[model];
|
||||
const tokens = tokensSource === undefined ? {} : normalizeTokenLimits(tokensSource, `copilotModelSettings.${index}.tokens`);
|
||||
return {
|
||||
...(thinking ? { thinking } : {}),
|
||||
...(Object.keys(tokens).length ? { tokens } : {}),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
export function normalizeCliToolConfig(toolId, input) {
|
||||
@@ -155,13 +184,12 @@ export function normalizeCliToolConfig(toolId, input) {
|
||||
: (config.opencodeModels[0] || "");
|
||||
} else if (toolId === "cowork") {
|
||||
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") {
|
||||
config.selectedModels = normalizeModels(input.selectedModels);
|
||||
} else if (toolId === "copilot") {
|
||||
config.selectedModels = normalizeModels(input.selectedModels);
|
||||
config.copilotThinking = normalizeThinkingMap(input.copilotThinking, config.selectedModels, "copilotThinking");
|
||||
config.copilotTokens = normalizeTokenMap(input.copilotTokens, config.selectedModels);
|
||||
config.copilotModelSettings = normalizeCopilotModelSettings(input.copilotModelSettings, config.selectedModels, input.copilotThinking, input.copilotTokens);
|
||||
}
|
||||
|
||||
return config;
|
||||
|
||||
@@ -54,7 +54,10 @@ describe("CLI tool configuration contract", () => {
|
||||
apiKeyMode: "managed",
|
||||
selectedModels: ["cc/a", "cc/a"],
|
||||
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", {
|
||||
apiKeyMode: "managed",
|
||||
@@ -73,11 +76,36 @@ describe("CLI tool configuration contract", () => {
|
||||
})).toEqual({
|
||||
baseUrl: "https://router.example/v1",
|
||||
selectedModels: ["cc/a", "cc/a"],
|
||||
copilotThinking: { "cc/a": "high" },
|
||||
copilotTokens: { "cc/a": { maxInputTokens: 100000, maxOutputTokens: 32000 } },
|
||||
copilotModelSettings: [
|
||||
{ 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", () => {
|
||||
expect(() => normalizeCliToolConfig("claude", {
|
||||
baseUrl: "https://router.example",
|
||||
|
||||
@@ -161,7 +161,7 @@ describe("permanent model deletion", () => {
|
||||
});
|
||||
expect((await db.getCliToolConfig(cliUser.id, "cowork")).config).toMatchObject({
|
||||
selectedModels: [`${providerPrefix}/gpt-keep`],
|
||||
coworkThinking: { [`${providerPrefix}/gpt-keep`]: "low" },
|
||||
coworkModelSettings: [{ thinking: "low" }],
|
||||
});
|
||||
|
||||
const history = await db.getUsageHistory({});
|
||||
|
||||
Reference in New Issue
Block a user