feat(combo): add context_length input field to combo edit form (#2047)

Integrated into release/v3.8.0
This commit is contained in:
Dohyun Jung
2026-05-09 05:34:56 +09:00
committed by GitHub
parent dc94613e6a
commit 39ae0314b6
45 changed files with 644 additions and 4 deletions

View File

@@ -1869,6 +1869,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
agentSystemMessage: string;
agentToolFilter: string;
agentContextCache: boolean;
contextLength: number | undefined;
};
const getEmptyCreateDraftSnapshot = useCallback(
@@ -1882,6 +1883,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
agentSystemMessage: "",
agentToolFilter: "",
agentContextCache: false,
contextLength: undefined,
}),
[]
);
@@ -1923,6 +1925,10 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
const [agentContextCache, setAgentContextCache] = useState<boolean>(
!!combo?.context_cache_protection
);
const [contextLength, setContextLength] = useState<number | undefined>(
combo?.context_length || undefined
);
const [contextLengthError, setContextLengthError] = useState<string>("");
const comboBuilderStages = useMemo(() => getComboBuilderStages({ strategy }), [strategy]);
const visibleStageMeta = useMemo(
() => COMBO_FORM_STAGE_META.filter((stageMeta) => comboBuilderStages.includes(stageMeta.id)),
@@ -1951,11 +1957,13 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
setConfig(nextConfig);
setShowAdvanced(isExpertMode);
setNameError("");
setContextLengthError("");
setAgentSystemMessage(nextCombo?.system_message || "");
setAgentToolFilter(nextCombo?.tool_filter_regex || "");
setAgentContextCache(!!nextCombo?.context_cache_protection);
setContextLength(nextCombo?.context_length || undefined);
},
[isExpertMode, setAgentContextCache]
[isExpertMode, setAgentContextCache, setContextLength]
);
useEffect(() => {
@@ -1969,6 +1977,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
agentSystemMessage,
agentToolFilter,
agentContextCache,
contextLength,
};
}, [
name,
@@ -1980,6 +1989,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
agentSystemMessage,
agentToolFilter,
agentContextCache,
contextLength,
]);
useEffect(() => {
@@ -2093,6 +2103,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
const saveBlocked =
!name.trim() ||
!!nameError ||
!!contextLengthError ||
saving ||
hasNoModels ||
hasInvalidWeightedTotal ||
@@ -2235,7 +2246,8 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
draft.nameError.length === 0 &&
draft.agentSystemMessage.length === 0 &&
draft.agentToolFilter.length === 0 &&
draft.agentContextCache === false;
draft.agentContextCache === false &&
draft.contextLength === undefined;
if (!cancelled && isPristineDraft) {
resetFormForCombo(null, data.comboDefaults || null);
@@ -2642,6 +2654,28 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
if (agentContextCache) saveData.context_cache_protection = true;
else delete saveData.context_cache_protection;
// Validate and save context_length
if (contextLength !== undefined && contextLength !== null) {
const ctxLen = Number(contextLength);
if (isNaN(ctxLen) || !Number.isInteger(ctxLen)) {
setContextLengthError(t("agentFeaturesContextLengthErrorInteger"));
setSaving(false);
return;
}
if (ctxLen >= 1000 && ctxLen <= 2000000) {
saveData.context_length = ctxLen;
} else {
setContextLengthError(t("agentFeaturesContextLengthErrorRange"));
setSaving(false);
return;
}
} else if (isEdit) {
// Editing: send null to explicitly clear context_length
saveData.context_length = null;
} else {
delete saveData.context_length;
}
await onSave(saveData);
setSaving(false);
};
@@ -3750,6 +3784,56 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
className="accent-primary shrink-0"
/>
</div>
{/* Context Length */}
<div>
<label className="text-[11px] font-medium text-text-muted block mb-0.5">
{getI18nOrFallback(t, "agentFeaturesContextLength", "Context length")}
</label>
<input
type="number"
min="1000"
max="2000000"
step="1000"
value={contextLength || ""}
onChange={(e) => {
const value = e.target.value;
setContextLengthError("");
if (value === "") {
setContextLength(undefined);
return;
}
const num = Number(value);
if (isNaN(num) || !Number.isInteger(num)) {
setContextLengthError(t("agentFeaturesContextLengthErrorInteger"));
// Keep the raw input value so the user can correct it
} else if (num < 1000 || num > 2000000) {
setContextLengthError(t("agentFeaturesContextLengthErrorRange"));
setContextLength(num);
} else {
setContextLength(num);
}
}}
placeholder={getI18nOrFallback(
t,
"agentFeaturesContextLengthPlaceholder",
"e.g. 128000"
)}
className="w-full text-xs py-1.5 px-2 rounded border border-black/10 dark:border-white/10 bg-transparent focus:border-primary focus:outline-none"
/>
{contextLengthError && (
<p className="text-[10px] text-red-500 mt-0.5">{contextLengthError}</p>
)}
{!contextLengthError && !isExpertMode && (
<p className="text-[10px] text-text-muted mt-0.5">
{getI18nOrFallback(
t,
"agentFeaturesContextLengthHint",
"Defines the context window for this combo in /v1/models."
)}
</p>
)}
</div>
</div>
)}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -1887,7 +1887,12 @@
"agentFeaturesToolFilterRegex": "/regex-pattern/",
"agentFeaturesToolFilterHint": "Tool filter regex for agents",
"agentFeaturesContextCacheHint": "Enable in-context cache for agent tools",
"agentFeaturesContextCacheProtection": "Protect cache from agent tool mutations"
"agentFeaturesContextCacheProtection": "Protect cache from agent tool mutations",
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
},
"costs": {
"title": "Costs",

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4609,5 +4609,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4785,5 +4785,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4639,5 +4639,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4631,5 +4631,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4900,5 +4900,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4607,5 +4607,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -4855,5 +4855,12 @@
"noProxyLogs": "No proxy logs yet. Configure proxies and make API calls to see them here.",
"noMatchingLogs": "No logs match the current filters.",
"tlsFingerprint": "Chrome 124 TLS Fingerprint"
},
"agentFeatures": {
"agentFeaturesContextLength": "Context length",
"agentFeaturesContextLengthPlaceholder": "e.g. 128000",
"agentFeaturesContextLengthHint": "Defines the context window for this combo in /v1/models.",
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
}
}

View File

@@ -161,6 +161,12 @@ export async function updateCombo(id: string, data: JsonRecord) {
sortOrder,
updatedAt: new Date().toISOString(),
};
// Remove fields explicitly set to null (for deletion support)
for (const key of Object.keys(data)) {
if (data[key] === null) {
delete merged[key];
}
}
const currentName = typeof current.name === "string" ? current.name : "";
const nextName =
typeof merged["name"] === "string" && merged["name"].trim().length > 0

View File

@@ -1325,7 +1325,7 @@ export const updateComboSchema = z
system_message: z.string().max(50000).optional(),
tool_filter_regex: z.string().max(1000).optional(),
context_cache_protection: z.boolean().optional(),
context_length: z.number().int().min(1000).max(2000000).optional(),
context_length: z.number().int().min(1000).max(2000000).optional().nullable(),
compressionOverride: comboCompressionOverrideSchema.optional(),
})
.superRefine((value, ctx) => {

View File

@@ -0,0 +1,265 @@
import test from "node:test";
import assert from "node:assert/strict";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-combo-ctx-"));
process.env.DATA_DIR = TEST_DATA_DIR;
const core = await import("../../src/lib/db/core.ts");
const combosDb = await import("../../src/lib/db/combos.ts");
const schemas = await import("../../src/shared/validation/schemas.ts");
async function resetStorage() {
core.resetDbInstance();
for (let attempt = 0; attempt < 10; attempt++) {
try {
if (fs.existsSync(TEST_DATA_DIR)) {
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
}
break;
} catch (error: any) {
if ((error?.code === "EBUSY" || error?.code === "EPERM") && attempt < 9) {
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
} else {
throw error;
}
}
}
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
}
test.beforeEach(async () => {
await resetStorage();
});
test.after(async () => {
core.resetDbInstance();
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
});
// ─── Zod Schema Validation (createComboSchema) ───
test("createComboSchema accepts valid context_length", () => {
const result = schemas.createComboSchema.safeParse({
name: "TestCombo",
context_length: 128000,
});
assert.equal(result.success, true);
});
test("createComboSchema rejects context_length below minimum (1000)", () => {
const result = schemas.createComboSchema.safeParse({
name: "TestCombo",
context_length: 999,
});
assert.equal(result.success, false);
});
test("createComboSchema rejects context_length above maximum (2000000)", () => {
const result = schemas.createComboSchema.safeParse({
name: "TestCombo",
context_length: 2000001,
});
assert.equal(result.success, false);
});
test("createComboSchema accepts context_length at exact boundaries", () => {
const min = schemas.createComboSchema.safeParse({
name: "MinCombo",
context_length: 1000,
});
assert.equal(min.success, true);
const max = schemas.createComboSchema.safeParse({
name: "MaxCombo",
context_length: 2000000,
});
assert.equal(max.success, true);
});
test("createComboSchema rejects non-integer context_length", () => {
const result = schemas.createComboSchema.safeParse({
name: "TestCombo",
context_length: 128000.5,
});
assert.equal(result.success, false);
});
test("createComboSchema accepts omitted context_length", () => {
const result = schemas.createComboSchema.safeParse({
name: "TestCombo",
});
assert.equal(result.success, true);
});
// ─── Zod Schema Validation (updateComboSchema) ───
test("updateComboSchema accepts valid context_length", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: 256000,
});
assert.equal(result.success, true);
});
test("updateComboSchema accepts null context_length (for clearing)", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: null,
});
assert.equal(result.success, true);
});
test("updateComboSchema rejects context_length below minimum", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: 500,
});
assert.equal(result.success, false);
});
test("updateComboSchema rejects context_length above maximum", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: 3000000,
});
assert.equal(result.success, false);
});
test("updateComboSchema rejects empty object (no fields)", () => {
const result = schemas.updateComboSchema.safeParse({});
assert.equal(result.success, false);
});
// ─── DB Operations ───
test("createCombo with context_length stores it correctly", async () => {
const combo = await combosDb.createCombo({
name: "CtxCombo",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 128000,
});
assert.equal(combo.context_length, 128000);
const retrieved = await combosDb.getComboById(combo.id);
assert.equal(retrieved?.context_length, 128000);
});
test("createCombo without context_length stores undefined", async () => {
const combo = await combosDb.createCombo({
name: "NoCtxCombo",
models: [{ provider: "openai", model: "gpt-4.1" }],
});
assert.equal(combo.context_length, undefined);
});
test("updateCombo can set context_length", async () => {
const combo = await combosDb.createCombo({
name: "UpdateCtxCombo",
models: [{ provider: "openai", model: "gpt-4.1" }],
});
assert.equal(combo.context_length, undefined);
const updated = await combosDb.updateCombo(combo.id, { context_length: 256000 });
assert.equal(updated?.context_length, 256000);
const retrieved = await combosDb.getComboById(combo.id);
assert.equal(retrieved?.context_length, 256000);
});
test("updateCombo can clear context_length with null", async () => {
const combo = await combosDb.createCombo({
name: "ClearCtxCombo",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 128000,
});
assert.equal(combo.context_length, 128000);
const updated = await combosDb.updateCombo(combo.id, { context_length: null });
assert.equal(updated?.context_length, undefined);
const retrieved = await combosDb.getComboById(combo.id);
assert.equal(retrieved?.context_length, undefined);
});
test("updateCombo preserves context_length when not included in update", async () => {
const combo = await combosDb.createCombo({
name: "PreserveCtxCombo",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 128000,
});
const updated = await combosDb.updateCombo(combo.id, { strategy: "round-robin" });
assert.equal(updated?.context_length, 128000);
});
test("getCombos returns context_length for all combos", async () => {
await combosDb.createCombo({
name: "ComboA",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 64000,
});
await combosDb.createCombo({
name: "ComboB",
models: [{ provider: "anthropic", model: "claude-3-7-sonnet" }],
context_length: 128000,
});
await combosDb.createCombo({
name: "ComboC",
models: [{ provider: "openai", model: "gpt-4o" }],
});
const combos = await combosDb.getCombos();
const comboA = combos.find((c) => c.name === "ComboA");
const comboB = combos.find((c) => c.name === "ComboB");
const comboC = combos.find((c) => c.name === "ComboC");
assert.equal(comboA?.context_length, 64000);
assert.equal(comboB?.context_length, 128000);
assert.equal(comboC?.context_length, undefined);
});
// ─── Edge Cases ───
test("context_length boundary value: exactly 1000", async () => {
const combo = await combosDb.createCombo({
name: "MinBoundary",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 1000,
});
assert.equal(combo.context_length, 1000);
});
test("context_length boundary value: exactly 2000000", async () => {
const combo = await combosDb.createCombo({
name: "MaxBoundary",
models: [{ provider: "openai", model: "gpt-4.1" }],
context_length: 2000000,
});
assert.equal(combo.context_length, 2000000);
});
test("updateCombo with context_length 0 is rejected by schema", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: 0,
});
assert.equal(result.success, false);
});
test("updateCombo with negative context_length is rejected by schema", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: -100,
});
assert.equal(result.success, false);
});
test("updateCombo with string context_length is rejected by schema", () => {
const result = schemas.updateComboSchema.safeParse({
context_length: "128000",
} as any);
assert.equal(result.success, false);
});