mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-01 12:52:11 +03:00
Expose per-combo reasoning token buffer toggle (#6702)
* fix(combos): default reasoning token buffer off
* feat(combos): expose reasoning token buffer toggle
* fix(combos): keep reasoning-token buffer default enabled, opt-out toggle
#6702 shipped bundled with #6536's own commit (identical SHA
37ac38f17f), flipping the combo-wide reasoningTokenBufferEnabled
default from true to false. #6536 was subsequently closed by the
author in favor of #6714, which explicitly keeps the existing
default-enabled buffer behavior and instead clamps the buffer to the
model's known output cap. Reconciled #6702 with that resolution:
dropped the default-flip changes across comboConfig.ts, combo.ts,
comboSetup.ts, ComboDefaultsTab.tsx, and the combo-defaults settings
route (plus their test assertions), and inverted the new per-combo
ReasoningTokenBufferToggle to opt-out semantics (`!== false`) so an
existing combo's behavior is unchanged unless the operator explicitly
unchecks it.
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
This commit is contained in:
@@ -11,6 +11,7 @@ _Living section — bullets land here as PRs merge into `release/v3.8.47` (paral
|
||||
### ✨ New Features
|
||||
|
||||
- **Provider/model param filters**: config-driven parameter denylist/allowlist per provider/model with auto-learn from upstream 400s (#6649 — thanks @ThongAccount, closes #6625)
|
||||
- **Per-combo reasoning token buffer toggle**: the combo builder now exposes an explicit checkbox for the `#3587` reasoning-model `max_tokens` buffer, defaulting to the existing enabled behavior, so a combo can opt out without hand-editing raw JSON config (#6702 — thanks @xz-dev)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
|
||||
@@ -3032,7 +3032,10 @@ async function handleRoundRobinCombo({
|
||||
resilienceSettings.providerCooldown.enabled &&
|
||||
provider &&
|
||||
provider !== "unknown" &&
|
||||
!(result.status === 500 && hasPerModelQuota(provider, parseModel(modelStr).model || modelStr))
|
||||
!(
|
||||
result.status === 500 &&
|
||||
hasPerModelQuota(provider, parseModel(modelStr).model || modelStr)
|
||||
)
|
||||
) {
|
||||
recordProviderCooldown(
|
||||
provider,
|
||||
|
||||
@@ -90,9 +90,7 @@ export function phaseComboSetup(ctx: ComboContext): ComboSetup {
|
||||
|
||||
const universalHandoffConfig = resolveUniversalHandoffConfig(
|
||||
(combo.universal_handoff || combo.universalHandoff) as
|
||||
| Record<string, unknown>
|
||||
| null
|
||||
| undefined,
|
||||
Record<string, unknown> | null | undefined,
|
||||
relayOptions?.universalHandoffConfig as Record<string, unknown> | null | undefined
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import Tooltip from "@/shared/components/Tooltip";
|
||||
|
||||
type TranslationFn = {
|
||||
(key: string): string;
|
||||
has?: (key: string) => boolean;
|
||||
};
|
||||
|
||||
type Props = {
|
||||
config: Record<string, any>;
|
||||
setConfig: (config: Record<string, any>) => void;
|
||||
t: TranslationFn;
|
||||
};
|
||||
|
||||
function getI18nOrFallback(t: TranslationFn, key: string, fallback: string): string {
|
||||
try {
|
||||
if (typeof t.has === "function" && t.has(key)) return t(key);
|
||||
} catch {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
export default function ReasoningTokenBufferToggle({ config, setConfig, t }: Props) {
|
||||
return (
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="reasoningTokenBufferEnabled"
|
||||
data-testid="combo-reasoning-token-buffer-enabled"
|
||||
checked={config.reasoningTokenBufferEnabled !== false}
|
||||
onChange={(e) => setConfig({ ...config, reasoningTokenBufferEnabled: e.target.checked })}
|
||||
className="w-3.5 h-3.5 rounded border border-black/20 dark:border-white/20 accent-primary cursor-pointer"
|
||||
/>
|
||||
<label
|
||||
htmlFor="reasoningTokenBufferEnabled"
|
||||
className="text-xs text-text-muted cursor-pointer select-none"
|
||||
>
|
||||
{getI18nOrFallback(t, "reasoningTokenBuffer", "Reasoning token buffer")}
|
||||
</label>
|
||||
<Tooltip
|
||||
position="bottom"
|
||||
content={getI18nOrFallback(
|
||||
t,
|
||||
"advancedHelp.reasoningTokenBuffer",
|
||||
"When enabled (default), OmniRoute may increase max_tokens for reasoning-capable models so they have headroom to think. Turn this off if you need this combo to preserve the client's exact max_tokens."
|
||||
)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[12px] text-text-muted cursor-help">
|
||||
help
|
||||
</span>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import Tooltip from "@/shared/components/Tooltip";
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { FieldLabelWithHelp, WeightTotalBar } from "./parts";
|
||||
import { ResponseValidationEditor, type ResponseValidationValue } from "./ResponseValidationEditor";
|
||||
import ReasoningTokenBufferToggle from "./ReasoningTokenBufferToggle";
|
||||
import { pickDisplayValue } from "@/shared/utils/maskEmail";
|
||||
import useEmailPrivacyStore from "@/store/emailPrivacyStore";
|
||||
import { useNotificationStore } from "@/store/notificationStore";
|
||||
@@ -217,10 +218,6 @@ function sanitizeComboRuntimeConfig(config) {
|
||||
);
|
||||
}
|
||||
|
||||
// Build the next combo config when a Fusion tuning field changes. Prunes empty /
|
||||
// non-finite entries and drops the whole `fusionTuning` object when no field is
|
||||
// set, so an empty `{}` is never persisted (sanitizeComboRuntimeConfig keeps any
|
||||
// non-null object as-is).
|
||||
function updateFusionTuning(config, field, rawValue) {
|
||||
const value = rawValue === "" ? undefined : Number(rawValue);
|
||||
const next = { ...(config.fusionTuning || {}), [field]: value };
|
||||
@@ -522,9 +519,7 @@ function getStrategyBadgeClass(strategy) {
|
||||
function getI18nOrFallback(t, key, fallback) {
|
||||
try {
|
||||
if (typeof t.has === "function" && t.has(key)) return t(key);
|
||||
} catch {
|
||||
// Some translations require ICU variables; fallback keeps optional helper text safe.
|
||||
}
|
||||
} catch {}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
@@ -1023,7 +1018,6 @@ export default function CombosPage() {
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-col gap-3 md:flex-row md:items-center md:justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-semibold">{t("title")}</h1>
|
||||
@@ -2063,7 +2057,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
}
|
||||
}, [builderStage, comboBuilderStages]);
|
||||
|
||||
// DnD state
|
||||
const hasPricingForModel = useCallback(
|
||||
(modelValue) => {
|
||||
const parsed = parseQualifiedModel(modelValue);
|
||||
@@ -2752,9 +2745,7 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
saveData.description = null;
|
||||
}
|
||||
|
||||
// Include config only if any values are set
|
||||
const configToSave = sanitizeComboRuntimeConfig(config);
|
||||
// Add round-robin specific fields to config
|
||||
if (strategy === "round-robin") {
|
||||
if (config.concurrencyPerModel !== undefined)
|
||||
configToSave.concurrencyPerModel = config.concurrencyPerModel;
|
||||
@@ -3740,7 +3731,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/* failoverBeforeRetry + maxSetRetries + setRetryDelayMs */}
|
||||
<div className="grid grid-cols-2 gap-2 pt-2 border-t border-black/5 dark:border-white/5">
|
||||
<div className="col-span-2">
|
||||
<div className="flex items-center gap-2 py-1">
|
||||
@@ -3776,6 +3766,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<ReasoningTokenBufferToggle config={config} setConfig={setConfig} t={t} />
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabelWithHelp
|
||||
label={t("maxSetRetries")}
|
||||
@@ -4166,7 +4159,11 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
</div>
|
||||
<div>
|
||||
<FieldLabelWithHelp
|
||||
label={getI18nOrFallback(t, "fusionStragglerGraceMs", "Straggler grace (ms)")}
|
||||
label={getI18nOrFallback(
|
||||
t,
|
||||
"fusionStragglerGraceMs",
|
||||
"Straggler grace (ms)"
|
||||
)}
|
||||
help={getI18nOrFallback(
|
||||
t,
|
||||
"fusionStragglerGraceMsHelp",
|
||||
@@ -4181,7 +4178,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
value={config.fusionTuning?.stragglerGraceMs ?? ""}
|
||||
placeholder="8000"
|
||||
onChange={(e) =>
|
||||
setConfig(updateFusionTuning(config, "stragglerGraceMs", e.target.value))
|
||||
setConfig(
|
||||
updateFusionTuning(config, "stragglerGraceMs", e.target.value)
|
||||
)
|
||||
}
|
||||
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"
|
||||
/>
|
||||
@@ -4580,7 +4579,6 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Actions */}
|
||||
{isExpertMode ? (
|
||||
<div className="flex gap-2 pt-1">
|
||||
<Button onClick={onClose} variant="ghost" fullWidth size="sm">
|
||||
|
||||
@@ -292,8 +292,7 @@ export default function ComboDefaultsTab() {
|
||||
|
||||
// Filtered provider list — excludes already-added ones, filtered by search query
|
||||
const filteredProviders = availableProviders.filter(
|
||||
(p) =>
|
||||
!providerOverrides[p.provider] && matchesSearch(p.provider, searchQuery)
|
||||
(p) => !providerOverrides[p.provider] && matchesSearch(p.provider, searchQuery)
|
||||
);
|
||||
|
||||
const handleDropdownKeyDown = (e: React.KeyboardEvent) => {
|
||||
|
||||
Reference in New Issue
Block a user