From edae0cf33ff723aa3a24cfb8461c72085477a5f0 Mon Sep 17 00:00:00 2001 From: Xiangzhe <32761048+xz-dev@users.noreply.github.com> Date: Fri, 10 Jul 2026 03:49:20 +0800 Subject: [PATCH] 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 Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com> --- CHANGELOG.md | 1 + open-sse/services/combo.ts | 5 +- open-sse/services/combo/comboSetup.ts | 4 +- .../combos/ReasoningTokenBufferToggle.tsx | 52 +++++++++++++++++++ src/app/(dashboard)/dashboard/combos/page.tsx | 28 +++++----- .../settings/components/ComboDefaultsTab.tsx | 3 +- 6 files changed, 72 insertions(+), 21 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx diff --git a/CHANGELOG.md b/CHANGELOG.md index e0203f2f9a..f209f9bcad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/open-sse/services/combo.ts b/open-sse/services/combo.ts index 94a090c87c..f4608d83c4 100644 --- a/open-sse/services/combo.ts +++ b/open-sse/services/combo.ts @@ -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, diff --git a/open-sse/services/combo/comboSetup.ts b/open-sse/services/combo/comboSetup.ts index 4b7e8b56e9..99197743ea 100644 --- a/open-sse/services/combo/comboSetup.ts +++ b/open-sse/services/combo/comboSetup.ts @@ -90,9 +90,7 @@ export function phaseComboSetup(ctx: ComboContext): ComboSetup { const universalHandoffConfig = resolveUniversalHandoffConfig( (combo.universal_handoff || combo.universalHandoff) as - | Record - | null - | undefined, + Record | null | undefined, relayOptions?.universalHandoffConfig as Record | null | undefined ); diff --git a/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx b/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx new file mode 100644 index 0000000000..643658346e --- /dev/null +++ b/src/app/(dashboard)/dashboard/combos/ReasoningTokenBufferToggle.tsx @@ -0,0 +1,52 @@ +import Tooltip from "@/shared/components/Tooltip"; + +type TranslationFn = { + (key: string): string; + has?: (key: string) => boolean; +}; + +type Props = { + config: Record; + setConfig: (config: Record) => 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 ( +
+ 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" + /> + + + + help + + +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index c5c068000c..fefc15aeda 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -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 (
- {/* Header */}

{t("title")}

@@ -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 />
- {/* failoverBeforeRetry + maxSetRetries + setRetryDelayMs */}
@@ -3776,6 +3766,9 @@ function ComboFormModal({ isOpen, combo, onClose, onSave, activeProviders, combo
+
+ +
- 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
)} - {/* Actions */} {isExpertMode ? (