From 39ae0314b6b150301e2d1e083767d2f402f6fa74 Mon Sep 17 00:00:00 2001 From: Dohyun Jung Date: Sat, 9 May 2026 05:34:56 +0900 Subject: [PATCH] feat(combo): add context_length input field to combo edit form (#2047) Integrated into release/v3.8.0 --- src/app/(dashboard)/dashboard/combos/page.tsx | 88 +++++- src/i18n/messages/ar.json | 7 + src/i18n/messages/bg.json | 7 + src/i18n/messages/bn.json | 7 + src/i18n/messages/cs.json | 7 + src/i18n/messages/da.json | 7 + src/i18n/messages/de.json | 7 + src/i18n/messages/en.json | 7 +- src/i18n/messages/es.json | 7 + src/i18n/messages/fa.json | 7 + src/i18n/messages/fi.json | 7 + src/i18n/messages/fr.json | 7 + src/i18n/messages/gu.json | 7 + src/i18n/messages/he.json | 7 + src/i18n/messages/hi.json | 7 + src/i18n/messages/hu.json | 7 + src/i18n/messages/id.json | 7 + src/i18n/messages/in.json | 7 + src/i18n/messages/it.json | 7 + src/i18n/messages/ja.json | 7 + src/i18n/messages/ko.json | 7 + src/i18n/messages/mr.json | 7 + src/i18n/messages/ms.json | 7 + src/i18n/messages/nl.json | 7 + src/i18n/messages/no.json | 7 + src/i18n/messages/phi.json | 7 + src/i18n/messages/pl.json | 7 + src/i18n/messages/pt-BR.json | 7 + src/i18n/messages/pt.json | 7 + src/i18n/messages/ro.json | 7 + src/i18n/messages/ru.json | 7 + src/i18n/messages/sk.json | 7 + src/i18n/messages/sv.json | 7 + src/i18n/messages/sw.json | 7 + src/i18n/messages/ta.json | 7 + src/i18n/messages/te.json | 7 + src/i18n/messages/th.json | 7 + src/i18n/messages/tr.json | 7 + src/i18n/messages/uk-UA.json | 7 + src/i18n/messages/ur.json | 7 + src/i18n/messages/vi.json | 7 + src/i18n/messages/zh-CN.json | 7 + src/lib/db/combos.ts | 6 + src/shared/validation/schemas.ts | 2 +- tests/unit/combo-context-length.test.ts | 265 ++++++++++++++++++ 45 files changed, 644 insertions(+), 4 deletions(-) create mode 100644 tests/unit/combo-context-length.test.ts diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 58c8b3ebca..1a271e7e1e 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -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( !!combo?.context_cache_protection ); + const [contextLength, setContextLength] = useState( + combo?.context_length || undefined + ); + const [contextLengthError, setContextLengthError] = useState(""); 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" /> + + {/* Context Length */} +
+ + { + 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 && ( +

{contextLengthError}

+ )} + {!contextLengthError && !isExpertMode && ( +

+ {getI18nOrFallback( + t, + "agentFeaturesContextLengthHint", + "Defines the context window for this combo in /v1/models." + )} +

+ )} +
)} diff --git a/src/i18n/messages/ar.json b/src/i18n/messages/ar.json index 989ef7987d..361f4547ff 100644 --- a/src/i18n/messages/ar.json +++ b/src/i18n/messages/ar.json @@ -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" } } diff --git a/src/i18n/messages/bg.json b/src/i18n/messages/bg.json index bcb9fa6050..b63d19bd74 100644 --- a/src/i18n/messages/bg.json +++ b/src/i18n/messages/bg.json @@ -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" } } diff --git a/src/i18n/messages/bn.json b/src/i18n/messages/bn.json index c7d76a36e7..97d4ee4581 100644 --- a/src/i18n/messages/bn.json +++ b/src/i18n/messages/bn.json @@ -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" } } diff --git a/src/i18n/messages/cs.json b/src/i18n/messages/cs.json index 1253886043..63fcda2255 100644 --- a/src/i18n/messages/cs.json +++ b/src/i18n/messages/cs.json @@ -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" } } diff --git a/src/i18n/messages/da.json b/src/i18n/messages/da.json index 5419e633db..875518ba16 100644 --- a/src/i18n/messages/da.json +++ b/src/i18n/messages/da.json @@ -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" } } diff --git a/src/i18n/messages/de.json b/src/i18n/messages/de.json index 5daaaa212d..f744062e48 100644 --- a/src/i18n/messages/de.json +++ b/src/i18n/messages/de.json @@ -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" } } diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index 3b97d7edae..3f53a98a3b 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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", diff --git a/src/i18n/messages/es.json b/src/i18n/messages/es.json index 84ebe91b16..496ef9cc6b 100644 --- a/src/i18n/messages/es.json +++ b/src/i18n/messages/es.json @@ -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" } } diff --git a/src/i18n/messages/fa.json b/src/i18n/messages/fa.json index 3a15e48922..dd2e5fc1f7 100644 --- a/src/i18n/messages/fa.json +++ b/src/i18n/messages/fa.json @@ -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" } } diff --git a/src/i18n/messages/fi.json b/src/i18n/messages/fi.json index af2b333bb2..920499a1fb 100644 --- a/src/i18n/messages/fi.json +++ b/src/i18n/messages/fi.json @@ -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" } } diff --git a/src/i18n/messages/fr.json b/src/i18n/messages/fr.json index 652f2add5e..5334ab39fb 100644 --- a/src/i18n/messages/fr.json +++ b/src/i18n/messages/fr.json @@ -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" } } diff --git a/src/i18n/messages/gu.json b/src/i18n/messages/gu.json index 1e8450a550..8f6dcdf384 100644 --- a/src/i18n/messages/gu.json +++ b/src/i18n/messages/gu.json @@ -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" } } diff --git a/src/i18n/messages/he.json b/src/i18n/messages/he.json index a91b77250b..e761b57e07 100644 --- a/src/i18n/messages/he.json +++ b/src/i18n/messages/he.json @@ -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" } } diff --git a/src/i18n/messages/hi.json b/src/i18n/messages/hi.json index 0cb3b55b9e..6a6baccae6 100644 --- a/src/i18n/messages/hi.json +++ b/src/i18n/messages/hi.json @@ -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" } } diff --git a/src/i18n/messages/hu.json b/src/i18n/messages/hu.json index 75d4714f9b..bc340a1fa6 100644 --- a/src/i18n/messages/hu.json +++ b/src/i18n/messages/hu.json @@ -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" } } diff --git a/src/i18n/messages/id.json b/src/i18n/messages/id.json index a7f0f928a3..cab46b4920 100644 --- a/src/i18n/messages/id.json +++ b/src/i18n/messages/id.json @@ -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" } } diff --git a/src/i18n/messages/in.json b/src/i18n/messages/in.json index 68de27273f..2413bbe089 100644 --- a/src/i18n/messages/in.json +++ b/src/i18n/messages/in.json @@ -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" } } diff --git a/src/i18n/messages/it.json b/src/i18n/messages/it.json index d5951e5591..fce014a701 100644 --- a/src/i18n/messages/it.json +++ b/src/i18n/messages/it.json @@ -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" } } diff --git a/src/i18n/messages/ja.json b/src/i18n/messages/ja.json index 7d7ab409e3..c99b968af0 100644 --- a/src/i18n/messages/ja.json +++ b/src/i18n/messages/ja.json @@ -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" } } diff --git a/src/i18n/messages/ko.json b/src/i18n/messages/ko.json index 0c92c7fd0e..da2c000a30 100644 --- a/src/i18n/messages/ko.json +++ b/src/i18n/messages/ko.json @@ -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" } } diff --git a/src/i18n/messages/mr.json b/src/i18n/messages/mr.json index 230af1fa18..842c9b1ccb 100644 --- a/src/i18n/messages/mr.json +++ b/src/i18n/messages/mr.json @@ -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" } } diff --git a/src/i18n/messages/ms.json b/src/i18n/messages/ms.json index 3022f9b12b..01116bf3f4 100644 --- a/src/i18n/messages/ms.json +++ b/src/i18n/messages/ms.json @@ -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" } } diff --git a/src/i18n/messages/nl.json b/src/i18n/messages/nl.json index 045d8022f0..d9f6372441 100644 --- a/src/i18n/messages/nl.json +++ b/src/i18n/messages/nl.json @@ -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" } } diff --git a/src/i18n/messages/no.json b/src/i18n/messages/no.json index e63f57edd8..67586bc2bd 100644 --- a/src/i18n/messages/no.json +++ b/src/i18n/messages/no.json @@ -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" } } diff --git a/src/i18n/messages/phi.json b/src/i18n/messages/phi.json index 6daab89dad..44ef332012 100644 --- a/src/i18n/messages/phi.json +++ b/src/i18n/messages/phi.json @@ -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" } } diff --git a/src/i18n/messages/pl.json b/src/i18n/messages/pl.json index 173b306384..e6bab6dc05 100644 --- a/src/i18n/messages/pl.json +++ b/src/i18n/messages/pl.json @@ -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" } } diff --git a/src/i18n/messages/pt-BR.json b/src/i18n/messages/pt-BR.json index 8a9fa3b305..b27a9aa252 100644 --- a/src/i18n/messages/pt-BR.json +++ b/src/i18n/messages/pt-BR.json @@ -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" } } diff --git a/src/i18n/messages/pt.json b/src/i18n/messages/pt.json index 5d04d4603e..afac044cea 100644 --- a/src/i18n/messages/pt.json +++ b/src/i18n/messages/pt.json @@ -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" } } diff --git a/src/i18n/messages/ro.json b/src/i18n/messages/ro.json index 9b7f8fee84..7b1511c6e0 100644 --- a/src/i18n/messages/ro.json +++ b/src/i18n/messages/ro.json @@ -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" } } diff --git a/src/i18n/messages/ru.json b/src/i18n/messages/ru.json index 6061dda7d7..f2646196ab 100644 --- a/src/i18n/messages/ru.json +++ b/src/i18n/messages/ru.json @@ -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" } } diff --git a/src/i18n/messages/sk.json b/src/i18n/messages/sk.json index 7355157afa..9ba990ef5a 100644 --- a/src/i18n/messages/sk.json +++ b/src/i18n/messages/sk.json @@ -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" } } diff --git a/src/i18n/messages/sv.json b/src/i18n/messages/sv.json index a925c89a80..54d5b59747 100644 --- a/src/i18n/messages/sv.json +++ b/src/i18n/messages/sv.json @@ -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" } } diff --git a/src/i18n/messages/sw.json b/src/i18n/messages/sw.json index 68de27273f..2413bbe089 100644 --- a/src/i18n/messages/sw.json +++ b/src/i18n/messages/sw.json @@ -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" } } diff --git a/src/i18n/messages/ta.json b/src/i18n/messages/ta.json index d63405fb2a..4b1c910900 100644 --- a/src/i18n/messages/ta.json +++ b/src/i18n/messages/ta.json @@ -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" } } diff --git a/src/i18n/messages/te.json b/src/i18n/messages/te.json index e19a68b3eb..075a32fee0 100644 --- a/src/i18n/messages/te.json +++ b/src/i18n/messages/te.json @@ -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" } } diff --git a/src/i18n/messages/th.json b/src/i18n/messages/th.json index 9a1535082f..737a223051 100644 --- a/src/i18n/messages/th.json +++ b/src/i18n/messages/th.json @@ -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" } } diff --git a/src/i18n/messages/tr.json b/src/i18n/messages/tr.json index 9a0908adf5..cd42d29758 100644 --- a/src/i18n/messages/tr.json +++ b/src/i18n/messages/tr.json @@ -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" } } diff --git a/src/i18n/messages/uk-UA.json b/src/i18n/messages/uk-UA.json index a510185d5c..08f0e28e2c 100644 --- a/src/i18n/messages/uk-UA.json +++ b/src/i18n/messages/uk-UA.json @@ -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" } } diff --git a/src/i18n/messages/ur.json b/src/i18n/messages/ur.json index 7a5f38120a..0b19464a15 100644 --- a/src/i18n/messages/ur.json +++ b/src/i18n/messages/ur.json @@ -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" } } diff --git a/src/i18n/messages/vi.json b/src/i18n/messages/vi.json index 679a8ac539..8a2d85fd67 100644 --- a/src/i18n/messages/vi.json +++ b/src/i18n/messages/vi.json @@ -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" } } diff --git a/src/i18n/messages/zh-CN.json b/src/i18n/messages/zh-CN.json index 9cce2c33d9..1e5ccbf474 100644 --- a/src/i18n/messages/zh-CN.json +++ b/src/i18n/messages/zh-CN.json @@ -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" } } diff --git a/src/lib/db/combos.ts b/src/lib/db/combos.ts index 371771c34f..8719618432 100644 --- a/src/lib/db/combos.ts +++ b/src/lib/db/combos.ts @@ -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 diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index a6096b41aa..941e3c4787 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -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) => { diff --git a/tests/unit/combo-context-length.test.ts b/tests/unit/combo-context-length.test.ts new file mode 100644 index 0000000000..f47539d64a --- /dev/null +++ b/tests/unit/combo-context-length.test.ts @@ -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); +});