Преглед изворни кода

refactor(anthropic): decouple adaptive thinking from thinking budget preference

Previously, `anthropicThinkingBudgetPreference` carried triple duty:
"inherit" / numeric string / "adaptive", making adaptive thinking and
thinking budget mutually exclusive. This change makes them independent
configurations that can coexist.

Priority order: adaptive thinking (model match) > thinking budget
override > inherit client request.

Changes:
- Remove "adaptive" literal from AnthropicThinkingBudgetPreference type
- Decouple validation: adaptive config validated independently
- Core logic: dual-path with fallback (adaptive -> budget -> inherit)
- UI: separate Switch toggle for adaptive thinking
- i18n: update all 5 language files
- Migration: clean up legacy 'adaptive' values in budget preference
- Tests: update existing + add 6 new coexistence tests (57 total)
ding113 пре 1 недеља
родитељ
комит
dc646926

+ 8 - 1
drizzle/0066_hot_mauler.sql

@@ -1 +1,8 @@
-ALTER TABLE "providers" ADD COLUMN "anthropic_adaptive_thinking" jsonb DEFAULT NULL;
+ALTER TABLE "providers" ADD COLUMN "anthropic_adaptive_thinking" jsonb DEFAULT NULL;
+
+-- Decouple adaptive thinking from thinking budget preference:
+-- The adaptive config is now independently stored in anthropic_adaptive_thinking (JSONB),
+-- so reset legacy 'adaptive' values in the varchar field to 'inherit' for data consistency.
+UPDATE providers
+SET anthropic_thinking_budget_preference = 'inherit'
+WHERE anthropic_thinking_budget_preference = 'adaptive';

+ 4 - 3
messages/en/settings/providers/form/sections.json

@@ -230,16 +230,17 @@
       },
       "thinkingBudget": {
         "label": "Thinking Budget Override",
-        "help": "Override thinking.budget_tokens in request body. Range: 1024-32000. Forces thinking.type to 'enabled' when set.",
+        "help": "Override thinking.budget_tokens in request body. Range: 1024-32000. Forces thinking.type to 'enabled' when set. If adaptive thinking is also enabled and model matches, adaptive takes priority.",
         "options": {
           "inherit": "No override (follow client)",
-          "custom": "Custom",
-          "adaptive": "Adaptive Thinking"
+          "custom": "Custom"
         },
         "placeholder": "e.g. 10240",
         "maxOutButton": "Max Out (32000)"
       },
       "adaptiveThinking": {
+        "label": "Adaptive Thinking",
+        "help": "Enable adaptive thinking mode. When enabled and model matches, overrides thinking budget. Non-matching models fall back to thinking budget override.",
         "effort": {
           "label": "Effort Level",
           "help": "Controls depth of reasoning. Higher effort = deeper thinking.",

+ 4 - 3
messages/ja/settings/providers/form/sections.json

@@ -231,16 +231,17 @@
       },
       "thinkingBudget": {
         "label": "思考予算オーバーライド",
-        "help": "リクエストボディの thinking.budget_tokens を上書きします。範囲:1024-32000。設定すると thinking.type が 'enabled' に強制されます。",
+        "help": "リクエストボディの thinking.budget_tokens を上書きします。範囲:1024-32000。設定すると thinking.type が 'enabled' に強制されます。アダプティブ思考も有効でモデルが一致する場合、アダプティブが優先されます。",
         "options": {
           "inherit": "上書きなし(クライアントに従う)",
-          "custom": "カスタム",
-          "adaptive": "アダプティブ思考"
+          "custom": "カスタム"
         },
         "placeholder": "例: 10240",
         "maxOutButton": "最大化 (32000)"
       },
       "adaptiveThinking": {
+        "label": "アダプティブ思考",
+        "help": "アダプティブ思考モードを有効にします。有効かつモデルが一致する場合、思考予算オーバーライドより優先されます。一致しないモデルは思考予算オーバーライドにフォールバックします。",
         "effort": {
           "label": "思考レベル",
           "help": "推論の深さを制御します。高いほど深く考えます。",

+ 4 - 3
messages/ru/settings/providers/form/sections.json

@@ -231,16 +231,17 @@
       },
       "thinkingBudget": {
         "label": "Переопределение бюджета размышлений",
-        "help": "Переопределяет thinking.budget_tokens в теле запроса. Диапазон: 1024-32000. При установке принудительно включает thinking.type = 'enabled'.",
+        "help": "Переопределяет thinking.budget_tokens в теле запроса. Диапазон: 1024-32000. При установке принудительно включает thinking.type = 'enabled'. Если также включено адаптивное мышление и модель совпадает, адаптивное имеет приоритет.",
         "options": {
           "inherit": "Без переопределения (следовать клиенту)",
-          "custom": "Пользовательское",
-          "adaptive": "Адаптивное мышление"
+          "custom": "Пользовательское"
         },
         "placeholder": "напр. 10240",
         "maxOutButton": "Максимум (32000)"
       },
       "adaptiveThinking": {
+        "label": "Адаптивное мышление",
+        "help": "Включить режим адаптивного мышления. При включении и совпадении модели имеет приоритет над бюджетом размышлений. Несовпадающие модели используют переопределение бюджета.",
         "effort": {
           "label": "Уровень усилий",
           "help": "Управляет глубиной рассуждений. Выше = глубже.",

+ 4 - 3
messages/zh-CN/settings/providers/form/sections.json

@@ -153,16 +153,17 @@
       },
       "thinkingBudget": {
         "label": "思考预算覆写",
-        "help": "覆写请求体中的 thinking.budget_tokens。范围:1024-32000。设置后会强制 thinking.type 为 'enabled'。",
+        "help": "覆写请求体中的 thinking.budget_tokens。范围:1024-32000。设置后会强制 thinking.type 为 'enabled'。如果同时启用了自适应思考且模型匹配,则自适应优先。",
         "options": {
           "inherit": "不覆写(遵循客户端)",
-          "custom": "自定义",
-          "adaptive": "自适应思考"
+          "custom": "自定义"
         },
         "placeholder": "如 10240",
         "maxOutButton": "拉满 (32000)"
       },
       "adaptiveThinking": {
+        "label": "自适应思考",
+        "help": "启用自适应思考模式。启用且模型匹配时优先于思考预算覆写,不匹配的模型回退到思考预算覆写。",
         "effort": {
           "label": "思考深度",
           "help": "控制推理深度。越高 = 思考越深入。",

+ 4 - 3
messages/zh-TW/settings/providers/form/sections.json

@@ -231,16 +231,17 @@
       },
       "thinkingBudget": {
         "label": "思考預算覆寫",
-        "help": "覆寫請求體中的 thinking.budget_tokens。範圍:1024-32000。設置後會強制 thinking.type 為 'enabled'。",
+        "help": "覆寫請求體中的 thinking.budget_tokens。範圍:1024-32000。設置後會強制 thinking.type 為 'enabled'。如果同時啟用了自適應思考且模型匹配,則自適應優先。",
         "options": {
           "inherit": "不覆寫(遵循客戶端)",
-          "custom": "自訂",
-          "adaptive": "自適應思考"
+          "custom": "自訂"
         },
         "placeholder": "如 10240",
         "maxOutButton": "拉滿 (32000)"
       },
       "adaptiveThinking": {
+        "label": "自適應思考",
+        "help": "啟用自適應思考模式。啟用且模型匹配時優先於思考預算覆寫,不匹配的模型回退到思考預算覆寫。",
         "effort": {
           "label": "思考深度",
           "help": "控制推理深度。越高 = 思考越深入。",

+ 1 - 4
src/app/[locale]/settings/providers/_components/forms/provider-form/index.tsx

@@ -323,10 +323,7 @@ function ProviderFormContent({
           codex_parallel_tool_calls_preference: state.routing.codexParallelToolCallsPreference,
           anthropic_max_tokens_preference: state.routing.anthropicMaxTokensPreference,
           anthropic_thinking_budget_preference: state.routing.anthropicThinkingBudgetPreference,
-          anthropic_adaptive_thinking:
-            state.routing.anthropicThinkingBudgetPreference === "adaptive"
-              ? state.routing.anthropicAdaptiveThinking
-              : null,
+          anthropic_adaptive_thinking: state.routing.anthropicAdaptiveThinking,
           gemini_google_search_preference: state.routing.geminiGoogleSearchPreference,
           limit_5h_usd: state.rateLimit.limit5hUsd,
           limit_daily_usd: state.rateLimit.limitDailyUsd,

+ 10 - 5
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-context.tsx

@@ -179,12 +179,19 @@ export function providerFormReducer(
         routing: { ...state.routing, anthropicMaxTokensPreference: action.payload },
       };
     case "SET_ANTHROPIC_THINKING_BUDGET":
-      if (action.payload === "adaptive") {
+      return {
+        ...state,
+        routing: {
+          ...state.routing,
+          anthropicThinkingBudgetPreference: action.payload,
+        },
+      };
+    case "SET_ADAPTIVE_THINKING_ENABLED":
+      if (action.payload) {
         return {
           ...state,
           routing: {
             ...state.routing,
-            anthropicThinkingBudgetPreference: "adaptive",
             anthropicAdaptiveThinking: state.routing.anthropicAdaptiveThinking ?? {
               effort: "high",
               modelMatchMode: "specific",
@@ -197,9 +204,7 @@ export function providerFormReducer(
         ...state,
         routing: {
           ...state.routing,
-          anthropicThinkingBudgetPreference: action.payload,
-          anthropicAdaptiveThinking:
-            action.payload === "inherit" ? null : state.routing.anthropicAdaptiveThinking,
+          anthropicAdaptiveThinking: null,
         },
       };
     case "SET_ADAPTIVE_THINKING_EFFORT":

+ 1 - 0
src/app/[locale]/settings/providers/_components/forms/provider-form/provider-form-types.ts

@@ -140,6 +140,7 @@ export type ProviderFormAction =
       payload: AnthropicAdaptiveThinkingModelMatchMode;
     }
   | { type: "SET_ADAPTIVE_THINKING_MODELS"; payload: string[] }
+  | { type: "SET_ADAPTIVE_THINKING_ENABLED"; payload: boolean }
   | { type: "SET_GEMINI_GOOGLE_SEARCH"; payload: GeminiGoogleSearchPreference }
   // Rate limit actions
   | { type: "SET_LIMIT_5H_USD"; payload: number | null }

+ 156 - 161
src/app/[locale]/settings/providers/_components/forms/provider-form/sections/routing-section.tsx

@@ -607,9 +607,7 @@ export function RoutingSection() {
                         value={
                           state.routing.anthropicThinkingBudgetPreference === "inherit"
                             ? "inherit"
-                            : state.routing.anthropicThinkingBudgetPreference === "adaptive"
-                              ? "adaptive"
-                              : "custom"
+                            : "custom"
                         }
                         onValueChange={(val) => {
                           if (val === "inherit") {
@@ -617,11 +615,6 @@ export function RoutingSection() {
                               type: "SET_ANTHROPIC_THINKING_BUDGET",
                               payload: "inherit",
                             });
-                          } else if (val === "adaptive") {
-                            dispatch({
-                              type: "SET_ANTHROPIC_THINKING_BUDGET",
-                              payload: "adaptive",
-                            });
                           } else {
                             dispatch({
                               type: "SET_ANTHROPIC_THINKING_BUDGET",
@@ -643,56 +636,50 @@ export function RoutingSection() {
                           <SelectItem value="custom">
                             {t("sections.routing.anthropicOverrides.thinkingBudget.options.custom")}
                           </SelectItem>
-                          <SelectItem value="adaptive">
-                            {t(
-                              "sections.routing.anthropicOverrides.thinkingBudget.options.adaptive"
-                            )}
-                          </SelectItem>
                         </SelectContent>
                       </Select>
-                      {state.routing.anthropicThinkingBudgetPreference !== "inherit" &&
-                        state.routing.anthropicThinkingBudgetPreference !== "adaptive" && (
-                          <>
-                            <Input
-                              type="number"
-                              value={state.routing.anthropicThinkingBudgetPreference}
-                              onChange={(e) => {
-                                const val = e.target.value;
-                                if (val === "") {
-                                  dispatch({
-                                    type: "SET_ANTHROPIC_THINKING_BUDGET",
-                                    payload: "inherit",
-                                  });
-                                } else {
-                                  dispatch({
-                                    type: "SET_ANTHROPIC_THINKING_BUDGET",
-                                    payload: val,
-                                  });
-                                }
-                              }}
-                              placeholder={t(
-                                "sections.routing.anthropicOverrides.thinkingBudget.placeholder"
-                              )}
-                              disabled={state.ui.isPending}
-                              min="1024"
-                              max="32000"
-                              className="flex-1"
-                            />
-                            <button
-                              type="button"
-                              onClick={() =>
+                      {state.routing.anthropicThinkingBudgetPreference !== "inherit" && (
+                        <>
+                          <Input
+                            type="number"
+                            value={state.routing.anthropicThinkingBudgetPreference}
+                            onChange={(e) => {
+                              const val = e.target.value;
+                              if (val === "") {
                                 dispatch({
                                   type: "SET_ANTHROPIC_THINKING_BUDGET",
-                                  payload: "32000",
-                                })
+                                  payload: "inherit",
+                                });
+                              } else {
+                                dispatch({
+                                  type: "SET_ANTHROPIC_THINKING_BUDGET",
+                                  payload: val,
+                                });
                               }
-                              className="px-3 py-2 text-xs bg-primary/10 hover:bg-primary/20 text-primary rounded-md transition-colors whitespace-nowrap"
-                              disabled={state.ui.isPending}
-                            >
-                              {t("sections.routing.anthropicOverrides.thinkingBudget.maxOutButton")}
-                            </button>
-                          </>
-                        )}
+                            }}
+                            placeholder={t(
+                              "sections.routing.anthropicOverrides.thinkingBudget.placeholder"
+                            )}
+                            disabled={state.ui.isPending}
+                            min="1024"
+                            max="32000"
+                            className="flex-1"
+                          />
+                          <button
+                            type="button"
+                            onClick={() =>
+                              dispatch({
+                                type: "SET_ANTHROPIC_THINKING_BUDGET",
+                                payload: "32000",
+                              })
+                            }
+                            className="px-3 py-2 text-xs bg-primary/10 hover:bg-primary/20 text-primary rounded-md transition-colors whitespace-nowrap"
+                            disabled={state.ui.isPending}
+                          >
+                            {t("sections.routing.anthropicOverrides.thinkingBudget.maxOutButton")}
+                          </button>
+                        </>
+                      )}
                       <Info className="h-4 w-4 text-muted-foreground shrink-0" />
                     </div>
                   </TooltipTrigger>
@@ -704,133 +691,141 @@ export function RoutingSection() {
                 </Tooltip>
               </SmartInputWrapper>
 
-              {state.routing.anthropicThinkingBudgetPreference === "adaptive" &&
-                state.routing.anthropicAdaptiveThinking && (
-                  <div className="ml-4 space-y-3 border-l-2 border-primary/20 pl-4">
-                    <SmartInputWrapper
-                      label={t("sections.routing.anthropicOverrides.adaptiveThinking.effort.label")}
-                    >
-                      <Tooltip>
-                        <TooltipTrigger asChild>
-                          <div className="flex gap-2 items-center">
-                            <Select
-                              value={state.routing.anthropicAdaptiveThinking.effort}
-                              onValueChange={(val) =>
-                                dispatch({
-                                  type: "SET_ADAPTIVE_THINKING_EFFORT",
-                                  payload: val as AnthropicAdaptiveThinkingEffort,
-                                })
-                              }
-                              disabled={state.ui.isPending}
-                            >
-                              <SelectTrigger className="w-40">
-                                <SelectValue />
-                              </SelectTrigger>
-                              <SelectContent>
-                                {(["low", "medium", "high", "max"] as const).map((level) => (
-                                  <SelectItem key={level} value={level}>
-                                    {t(
-                                      `sections.routing.anthropicOverrides.adaptiveThinking.effort.options.${level}`
-                                    )}
-                                  </SelectItem>
-                                ))}
-                              </SelectContent>
-                            </Select>
-                            <Info className="h-4 w-4 text-muted-foreground shrink-0" />
-                          </div>
-                        </TooltipTrigger>
-                        <TooltipContent side="top" className="max-w-xs">
-                          <p className="text-sm">
-                            {t("sections.routing.anthropicOverrides.adaptiveThinking.effort.help")}
-                          </p>
-                        </TooltipContent>
-                      </Tooltip>
-                    </SmartInputWrapper>
+              <ToggleRow
+                label={t("sections.routing.anthropicOverrides.adaptiveThinking.label")}
+                description={t("sections.routing.anthropicOverrides.adaptiveThinking.help")}
+              >
+                <Switch
+                  checked={state.routing.anthropicAdaptiveThinking !== null}
+                  onCheckedChange={(checked) =>
+                    dispatch({ type: "SET_ADAPTIVE_THINKING_ENABLED", payload: checked })
+                  }
+                  disabled={state.ui.isPending}
+                />
+              </ToggleRow>
 
+              {state.routing.anthropicAdaptiveThinking && (
+                <div className="ml-4 space-y-3 border-l-2 border-primary/20 pl-4">
+                  <SmartInputWrapper
+                    label={t("sections.routing.anthropicOverrides.adaptiveThinking.effort.label")}
+                  >
+                    <Tooltip>
+                      <TooltipTrigger asChild>
+                        <div className="flex gap-2 items-center">
+                          <Select
+                            value={state.routing.anthropicAdaptiveThinking.effort}
+                            onValueChange={(val) =>
+                              dispatch({
+                                type: "SET_ADAPTIVE_THINKING_EFFORT",
+                                payload: val as AnthropicAdaptiveThinkingEffort,
+                              })
+                            }
+                            disabled={state.ui.isPending}
+                          >
+                            <SelectTrigger className="w-40">
+                              <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent>
+                              {(["low", "medium", "high", "max"] as const).map((level) => (
+                                <SelectItem key={level} value={level}>
+                                  {t(
+                                    `sections.routing.anthropicOverrides.adaptiveThinking.effort.options.${level}`
+                                  )}
+                                </SelectItem>
+                              ))}
+                            </SelectContent>
+                          </Select>
+                          <Info className="h-4 w-4 text-muted-foreground shrink-0" />
+                        </div>
+                      </TooltipTrigger>
+                      <TooltipContent side="top" className="max-w-xs">
+                        <p className="text-sm">
+                          {t("sections.routing.anthropicOverrides.adaptiveThinking.effort.help")}
+                        </p>
+                      </TooltipContent>
+                    </Tooltip>
+                  </SmartInputWrapper>
+
+                  <SmartInputWrapper
+                    label={t(
+                      "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.label"
+                    )}
+                  >
+                    <Tooltip>
+                      <TooltipTrigger asChild>
+                        <div className="flex gap-2 items-center">
+                          <Select
+                            value={state.routing.anthropicAdaptiveThinking.modelMatchMode}
+                            onValueChange={(val) =>
+                              dispatch({
+                                type: "SET_ADAPTIVE_THINKING_MODEL_MATCH_MODE",
+                                payload: val as AnthropicAdaptiveThinkingModelMatchMode,
+                              })
+                            }
+                            disabled={state.ui.isPending}
+                          >
+                            <SelectTrigger className="w-40">
+                              <SelectValue />
+                            </SelectTrigger>
+                            <SelectContent>
+                              <SelectItem value="all">
+                                {t(
+                                  "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.options.all"
+                                )}
+                              </SelectItem>
+                              <SelectItem value="specific">
+                                {t(
+                                  "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.options.specific"
+                                )}
+                              </SelectItem>
+                            </SelectContent>
+                          </Select>
+                          <Info className="h-4 w-4 text-muted-foreground shrink-0" />
+                        </div>
+                      </TooltipTrigger>
+                      <TooltipContent side="top" className="max-w-xs">
+                        <p className="text-sm">
+                          {t(
+                            "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.help"
+                          )}
+                        </p>
+                      </TooltipContent>
+                    </Tooltip>
+                  </SmartInputWrapper>
+
+                  {state.routing.anthropicAdaptiveThinking.modelMatchMode === "specific" && (
                     <SmartInputWrapper
-                      label={t(
-                        "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.label"
-                      )}
+                      label={t("sections.routing.anthropicOverrides.adaptiveThinking.models.label")}
                     >
                       <Tooltip>
                         <TooltipTrigger asChild>
                           <div className="flex gap-2 items-center">
-                            <Select
-                              value={state.routing.anthropicAdaptiveThinking.modelMatchMode}
-                              onValueChange={(val) =>
+                            <TagInput
+                              value={state.routing.anthropicAdaptiveThinking.models}
+                              onChange={(models) =>
                                 dispatch({
-                                  type: "SET_ADAPTIVE_THINKING_MODEL_MATCH_MODE",
-                                  payload: val as AnthropicAdaptiveThinkingModelMatchMode,
+                                  type: "SET_ADAPTIVE_THINKING_MODELS",
+                                  payload: models,
                                 })
                               }
+                              placeholder={t(
+                                "sections.routing.anthropicOverrides.adaptiveThinking.models.placeholder"
+                              )}
                               disabled={state.ui.isPending}
-                            >
-                              <SelectTrigger className="w-40">
-                                <SelectValue />
-                              </SelectTrigger>
-                              <SelectContent>
-                                <SelectItem value="all">
-                                  {t(
-                                    "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.options.all"
-                                  )}
-                                </SelectItem>
-                                <SelectItem value="specific">
-                                  {t(
-                                    "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.options.specific"
-                                  )}
-                                </SelectItem>
-                              </SelectContent>
-                            </Select>
+                            />
                             <Info className="h-4 w-4 text-muted-foreground shrink-0" />
                           </div>
                         </TooltipTrigger>
                         <TooltipContent side="top" className="max-w-xs">
                           <p className="text-sm">
-                            {t(
-                              "sections.routing.anthropicOverrides.adaptiveThinking.modelMatchMode.help"
-                            )}
+                            {t("sections.routing.anthropicOverrides.adaptiveThinking.models.help")}
                           </p>
                         </TooltipContent>
                       </Tooltip>
                     </SmartInputWrapper>
-
-                    {state.routing.anthropicAdaptiveThinking.modelMatchMode === "specific" && (
-                      <SmartInputWrapper
-                        label={t(
-                          "sections.routing.anthropicOverrides.adaptiveThinking.models.label"
-                        )}
-                      >
-                        <Tooltip>
-                          <TooltipTrigger asChild>
-                            <div className="flex gap-2 items-center">
-                              <TagInput
-                                value={state.routing.anthropicAdaptiveThinking.models}
-                                onChange={(models) =>
-                                  dispatch({
-                                    type: "SET_ADAPTIVE_THINKING_MODELS",
-                                    payload: models,
-                                  })
-                                }
-                                placeholder={t(
-                                  "sections.routing.anthropicOverrides.adaptiveThinking.models.placeholder"
-                                )}
-                                disabled={state.ui.isPending}
-                              />
-                              <Info className="h-4 w-4 text-muted-foreground shrink-0" />
-                            </div>
-                          </TooltipTrigger>
-                          <TooltipContent side="top" className="max-w-xs">
-                            <p className="text-sm">
-                              {t(
-                                "sections.routing.anthropicOverrides.adaptiveThinking.models.help"
-                              )}
-                            </p>
-                          </TooltipContent>
-                        </Tooltip>
-                      </SmartInputWrapper>
-                    )}
-                  </div>
-                )}
+                  )}
+                </div>
+              )}
             </div>
           </SectionCard>
         )}

+ 1 - 1
src/drizzle/schema.ts

@@ -284,7 +284,7 @@ export const providers = pgTable('providers', {
   anthropicThinkingBudgetPreference: varchar('anthropic_thinking_budget_preference', { length: 20 }),
 
   // Anthropic adaptive thinking config (JSONB)
-  // When anthropicThinkingBudgetPreference === "adaptive", this stores the structured config
+  // Independent config for adaptive thinking mode; takes priority over budget override when model matches
   anthropicAdaptiveThinking: jsonb('anthropic_adaptive_thinking')
     .$type<{ effort: string; modelMatchMode: string; models: string[] } | null>()
     .default(null),

+ 18 - 22
src/lib/anthropic/provider-overrides.ts

@@ -60,26 +60,24 @@ export function applyAnthropicProviderOverrides(
     output.max_tokens = maxTokens;
   }
 
-  if (provider.anthropicThinkingBudgetPreference === "adaptive") {
-    const config = provider.anthropicAdaptiveThinking;
-    if (config) {
-      const modelId = typeof request.model === "string" ? request.model : null;
-      const isMatch =
-        config.modelMatchMode === "all" ||
-        (modelId !== null &&
-          config.models.some((m) => modelId === m || modelId.startsWith(`${m}-`)));
-      if (isMatch) {
-        ensureCloned();
-        output.thinking = { type: "adaptive" };
-        const existingOutputConfig = isPlainObject(output.output_config)
-          ? output.output_config
-          : {};
-        output.output_config = { ...existingOutputConfig, effort: config.effort };
-      }
+  // Step 1: Try adaptive thinking (independent of budgetPreference)
+  const adaptiveConfig = provider.anthropicAdaptiveThinking;
+  if (adaptiveConfig) {
+    const modelId = typeof request.model === "string" ? request.model : null;
+    const isMatch =
+      adaptiveConfig.modelMatchMode === "all" ||
+      (modelId !== null &&
+        adaptiveConfig.models.some((m) => modelId === m || modelId.startsWith(`${m}-`)));
+    if (isMatch) {
+      ensureCloned();
+      output.thinking = { type: "adaptive" };
+      const existingOutputConfig = isPlainObject(output.output_config) ? output.output_config : {};
+      output.output_config = { ...existingOutputConfig, effort: adaptiveConfig.effort };
+      return output;
     }
-    return output;
   }
 
+  // Step 2: Fall through to thinking budget override
   const thinkingBudget = normalizeNumericPreference(provider.anthropicThinkingBudgetPreference);
   if (thinkingBudget !== null) {
     ensureCloned();
@@ -116,12 +114,10 @@ export function applyAnthropicProviderOverridesWithAudit(
   }
 
   const maxTokens = normalizeNumericPreference(provider.anthropicMaxTokensPreference);
-  const isAdaptive = provider.anthropicThinkingBudgetPreference === "adaptive";
-  const thinkingBudget = isAdaptive
-    ? null
-    : normalizeNumericPreference(provider.anthropicThinkingBudgetPreference);
+  const hasAdaptiveConfig = provider.anthropicAdaptiveThinking != null;
+  const thinkingBudget = normalizeNumericPreference(provider.anthropicThinkingBudgetPreference);
 
-  const hit = maxTokens !== null || thinkingBudget !== null || isAdaptive;
+  const hit = maxTokens !== null || thinkingBudget !== null || hasAdaptiveConfig;
 
   if (!hit) {
     return { request, audit: null };

+ 0 - 21
src/lib/validation/schemas.ts

@@ -43,7 +43,6 @@ const ANTHROPIC_MAX_TOKENS_PREFERENCE = z.union([
 
 const ANTHROPIC_THINKING_BUDGET_PREFERENCE = z.union([
   z.literal("inherit"),
-  z.literal("adaptive"),
   z
     .string()
     .regex(/^\d+$/, 'thinking.budget_tokens 必须为 "inherit" 或数字字符串')
@@ -596,16 +595,6 @@ export const CreateProviderSchema = z
   .superRefine((data, ctx) => {
     const maxTokens = data.anthropic_max_tokens_preference;
     const budget = data.anthropic_thinking_budget_preference;
-    if (budget === "adaptive") {
-      if (!data.anthropic_adaptive_thinking) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: "adaptive thinking config is required when mode is adaptive",
-          path: ["anthropic_adaptive_thinking"],
-        });
-      }
-      return;
-    }
     if (maxTokens && maxTokens !== "inherit" && budget && budget !== "inherit") {
       const maxTokensNum = Number.parseInt(maxTokens, 10);
       const budgetNum = Number.parseInt(budget, 10);
@@ -806,16 +795,6 @@ export const UpdateProviderSchema = z
   .superRefine((data, ctx) => {
     const maxTokens = data.anthropic_max_tokens_preference;
     const budget = data.anthropic_thinking_budget_preference;
-    if (budget === "adaptive") {
-      if (!data.anthropic_adaptive_thinking) {
-        ctx.addIssue({
-          code: z.ZodIssueCode.custom,
-          message: "adaptive thinking config is required when mode is adaptive",
-          path: ["anthropic_adaptive_thinking"],
-        });
-      }
-      return;
-    }
     if (maxTokens && maxTokens !== "inherit" && budget && budget !== "inherit") {
       const maxTokensNum = Number.parseInt(maxTokens, 10);
       const budgetNum = Number.parseInt(budget, 10);

+ 1 - 2
src/types/provider.ts

@@ -33,9 +33,8 @@ export type CodexParallelToolCallsPreference = "inherit" | "true" | "false";
 // Anthropic (Messages API) parameter overrides
 // - "inherit": follow client request (default)
 // - numeric string: force override to that value
-// - "adaptive": use adaptive thinking mode (read config from anthropicAdaptiveThinking)
 export type AnthropicMaxTokensPreference = "inherit" | string;
-export type AnthropicThinkingBudgetPreference = "inherit" | "adaptive" | string;
+export type AnthropicThinkingBudgetPreference = "inherit" | string;
 
 // Anthropic adaptive thinking configuration
 export type AnthropicAdaptiveThinkingEffort = "low" | "medium" | "high" | "max";

+ 161 - 10
tests/unit/proxy/anthropic-provider-overrides.test.ts

@@ -777,7 +777,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should apply adaptive thinking for matching model (all models mode)", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "all" as const,
@@ -799,7 +798,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should apply adaptive thinking for matching model (specific models mode)", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "max" as const,
           modelMatchMode: "specific" as const,
@@ -820,7 +818,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should passthrough for non-matching model (specific models mode)", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "specific" as const,
@@ -843,7 +840,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should preserve existing output_config properties", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "medium" as const,
           modelMatchMode: "all" as const,
@@ -866,7 +862,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should apply adaptive with effort 'low'", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "low" as const,
           modelMatchMode: "all" as const,
@@ -886,7 +881,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should remove budget_tokens from existing thinking when applying adaptive", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "all" as const,
@@ -909,7 +903,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should passthrough when adaptive config is null (defensive)", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: null,
       };
 
@@ -928,7 +921,6 @@ describe("Anthropic Provider Overrides", () => {
       const provider = {
         providerType: "claude",
         anthropicMaxTokensPreference: "32000",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "all" as const,
@@ -951,7 +943,6 @@ describe("Anthropic Provider Overrides", () => {
     it("should match model prefix (claude-opus-4-6 matches claude-opus-4-6-20250514)", () => {
       const provider = {
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "specific" as const,
@@ -974,7 +965,6 @@ describe("Anthropic Provider Overrides", () => {
         id: 1,
         name: "adaptive-provider",
         providerType: "claude",
-        anthropicThinkingBudgetPreference: "adaptive",
         anthropicAdaptiveThinking: {
           effort: "high" as const,
           modelMatchMode: "all" as const,
@@ -1000,4 +990,165 @@ describe("Anthropic Provider Overrides", () => {
       expect(thinkingTypeChange?.after).toBe("adaptive");
     });
   });
+
+  describe("Adaptive + Budget coexistence", () => {
+    it("should apply adaptive when model matches, ignoring budget override", () => {
+      const provider = {
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "10240",
+        anthropicAdaptiveThinking: {
+          effort: "high" as const,
+          modelMatchMode: "specific" as const,
+          models: ["claude-opus-4-6"],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-opus-4-6",
+        messages: [],
+        max_tokens: 32000,
+      };
+
+      const output = applyAnthropicProviderOverrides(provider, input);
+      expect(output.thinking).toEqual({ type: "adaptive" });
+      expect(output.output_config).toEqual({ effort: "high" });
+      // Budget should NOT be applied when adaptive matches
+      const thinking = output.thinking as Record<string, unknown>;
+      expect(thinking.budget_tokens).toBeUndefined();
+    });
+
+    it("should fallback to budget when model does not match adaptive config", () => {
+      const provider = {
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "10240",
+        anthropicAdaptiveThinking: {
+          effort: "high" as const,
+          modelMatchMode: "specific" as const,
+          models: ["claude-opus-4-6"],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-sonnet-4-5",
+        messages: [],
+        max_tokens: 32000,
+      };
+
+      const output = applyAnthropicProviderOverrides(provider, input);
+      const thinking = output.thinking as Record<string, unknown>;
+      expect(thinking.type).toBe("enabled");
+      expect(thinking.budget_tokens).toBe(10240);
+      expect(output.output_config).toBeUndefined();
+    });
+
+    it("should passthrough when model does not match adaptive and budget is inherit", () => {
+      const provider = {
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "inherit",
+        anthropicAdaptiveThinking: {
+          effort: "high" as const,
+          modelMatchMode: "specific" as const,
+          models: ["claude-opus-4-6"],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-sonnet-4-5",
+        messages: [],
+        max_tokens: 32000,
+        thinking: { type: "enabled", budget_tokens: 5000 },
+      };
+      const snapshot = structuredClone(input);
+
+      const output = applyAnthropicProviderOverrides(provider, input);
+      expect(output).toEqual(snapshot);
+    });
+
+    it("should always apply adaptive when modelMatchMode=all, regardless of budget", () => {
+      const provider = {
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "10240",
+        anthropicAdaptiveThinking: {
+          effort: "max" as const,
+          modelMatchMode: "all" as const,
+          models: [],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-sonnet-4-5",
+        messages: [],
+        max_tokens: 32000,
+      };
+
+      const output = applyAnthropicProviderOverrides(provider, input);
+      expect(output.thinking).toEqual({ type: "adaptive" });
+      expect(output.output_config).toEqual({ effort: "max" });
+    });
+
+    it("should produce correct audit trail when adaptive matches (coexistence)", () => {
+      const provider = {
+        id: 10,
+        name: "coexist-provider",
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "10240",
+        anthropicAdaptiveThinking: {
+          effort: "high" as const,
+          modelMatchMode: "specific" as const,
+          models: ["claude-opus-4-6"],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-opus-4-6",
+        messages: [],
+        max_tokens: 32000,
+      };
+
+      const result = applyAnthropicProviderOverridesWithAudit(provider, input);
+      expect(result.audit?.hit).toBe(true);
+      expect(result.audit?.changed).toBe(true);
+
+      const effortChange = result.audit?.changes.find((c) => c.path === "output_config.effort");
+      expect(effortChange?.after).toBe("high");
+      expect(effortChange?.changed).toBe(true);
+
+      const thinkingTypeChange = result.audit?.changes.find((c) => c.path === "thinking.type");
+      expect(thinkingTypeChange?.after).toBe("adaptive");
+    });
+
+    it("should produce correct audit trail when falling back to budget (coexistence)", () => {
+      const provider = {
+        id: 10,
+        name: "coexist-provider",
+        providerType: "claude",
+        anthropicThinkingBudgetPreference: "10240",
+        anthropicAdaptiveThinking: {
+          effort: "high" as const,
+          modelMatchMode: "specific" as const,
+          models: ["claude-opus-4-6"],
+        },
+      };
+
+      const input: Record<string, unknown> = {
+        model: "claude-sonnet-4-5",
+        messages: [],
+        max_tokens: 32000,
+      };
+
+      const result = applyAnthropicProviderOverridesWithAudit(provider, input);
+      expect(result.audit?.hit).toBe(true);
+      expect(result.audit?.changed).toBe(true);
+
+      const thinkingTypeChange = result.audit?.changes.find((c) => c.path === "thinking.type");
+      expect(thinkingTypeChange?.after).toBe("enabled");
+
+      const budgetChange = result.audit?.changes.find((c) => c.path === "thinking.budget_tokens");
+      expect(budgetChange?.after).toBe(10240);
+
+      // output_config.effort should NOT be set for budget fallback
+      const effortChange = result.audit?.changes.find((c) => c.path === "output_config.effort");
+      expect(effortChange?.changed).toBe(false);
+    });
+  });
 });