From 863e44ec247f6787046649ab40cd9e72897dbb02 Mon Sep 17 00:00:00 2001 From: Autumn-27 <53844290+Autumn-27@users.noreply.github.com> Date: Thu, 20 Aug 2026 12:31:20 -0400 Subject: [PATCH] =?UTF-8?q?feat(llm):=20=E6=80=9D=E8=80=83=E5=BC=80?= =?UTF-8?q?=E5=85=B3=E4=B8=8E=E6=80=9D=E8=80=83=E5=BC=BA=E5=BA=A6=E6=8B=86?= =?UTF-8?q?=E6=88=90=E4=B8=A4=E4=B8=AA=E7=8B=AC=E7=AB=8B=E5=8F=AF=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E5=8F=82=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - thinking_type(''/disabled/enabled) 与 reasoning_effort(''/low/medium/high/xhigh/max) 彻底解耦,可任意组合:支持「不发 thinking 字段、只靠强度激活思考」的接口 - agent.Config 拆为 ThinkingType + ReasoningEffort 两字段,NewProvider 直接透传 - db/schema.sql 加 thinking_type 列 + 一次性 DO 块迁移(仅列不存在时回填旧语义) - 前端 LLM 配置页改为两个独立下拉,强度新增 xhigh 档 Claude-Session: https://claude.ai/code/session_01Bs2rrsVHUBTxRusm9espwB --- agent/provider.go | 26 +++---- db/config.go | 28 +++---- db/schema.sql | 23 ++++++ server/dto.go | 2 + server/server.go | 9 ++- web/src/app/(main)/system/llm/page.tsx | 100 ++++++++++++------------- web/src/lib/api.ts | 6 +- web/src/lib/mock/data.ts | 6 +- web/src/lib/types.ts | 4 +- 9 files changed, 121 insertions(+), 83 deletions(-) diff --git a/agent/provider.go b/agent/provider.go index 465812b..6c1c8b5 100644 --- a/agent/provider.go +++ b/agent/provider.go @@ -41,9 +41,14 @@ type Config struct { // ContextWindowK is the model's context window in K tokens (user-configured), // used to size compaction thresholds. 0 = default; see CompactionWindow. ContextWindowK int - // ReasoningEffort selects the thinking mode: "" = 默认(不发送思考参数); - // "off" = 关闭(thinking.type=disabled); "low"/"medium"/"high"/"max" = 开启并设强度. - // NewProvider derives the provider-specific request fields from it. + // ThinkingType 独立控制思考「开关」字段(thinking.type): + // "" = 不发送(默认,兼容不支持该字段的模型); "disabled" = 显式关闭; + // "enabled" = 开启. 与 ReasoningEffort 完全解耦——有些接口没有 thinking 字段、 + // 只靠强度参数就能激活思考,故两者可各自单独设置. + ThinkingType string + // ReasoningEffort 独立控制思考「强度」字段: + // "" = 不发送(默认); "low"/"medium"/"high"/"xhigh"/"max" = 对应强度. + // OpenAI 映射为顶层 reasoning_effort;Anthropic 映射为 output_config.effort. ReasoningEffort string } @@ -184,17 +189,10 @@ func (c Config) NewProvider() (llm.Provider, error) { Model: c.Model, HTTPClient: client, } - // Derive the provider thinking params from the single UI selector: - // "" → omit both (provider default); "off" → disable; else → enable + effort. - switch c.ReasoningEffort { - case "": - // leave both empty → omitted from the request - case "off": - lc.ThinkingType = "disabled" - default: - lc.ThinkingType = "enabled" - lc.ReasoningEffort = c.ReasoningEffort - } + // 思考开关与强度两个字段各自透传(空 = 该字段不发送)。二者解耦: + // 可只发 thinking.type、只发 effort、都发、或都不发。 + lc.ThinkingType = c.ThinkingType + lc.ReasoningEffort = c.ReasoningEffort if c.RatePerSecond > 0 || c.RatePerMinute > 0 { lc.RateLimit = &llm.RateLimit{PerSecond: c.RatePerSecond, PerMinute: c.RatePerMinute} } diff --git a/db/config.go b/db/config.go index 8d91487..40684d6 100644 --- a/db/config.go +++ b/db/config.go @@ -25,9 +25,11 @@ type LLMProfile struct { // ContextWindowK is the model's context window in K tokens, used to size // compaction thresholds. 0 = use a 200K default; capped at 1000 (1M). ContextWindowK int `json:"context_window_k"` - // ReasoningEffort is the thinking-mode selector. "" = 默认(不发送思考参数); - // "off" = 显式关闭(thinking.type=disabled); "low"/"medium"/"high"/"max" = - // 开启思考并设强度. Mapped to the provider request in agent.Config.NewProvider. + // ThinkingType 独立控制思考「开关」(thinking.type):"" = 不发送(默认); + // "disabled" = 显式关闭; "enabled" = 开启. 与 ReasoningEffort 解耦. + ThinkingType string `json:"thinking_type"` + // ReasoningEffort 独立控制思考「强度」:"" = 不发送(默认); + // "low"/"medium"/"high"/"xhigh"/"max" = 对应强度. 见 agent.Config.NewProvider. ReasoningEffort string `json:"reasoning_effort"` IsDefault bool `json:"is_default"` // Priority orders the failover chain: higher goes first. The ACTIVE profile @@ -41,14 +43,14 @@ type LLMProfile struct { // profileCols is the read column list (hint variant, no api key) shared by the // list query; profileColsKey is the same with api_key for the single-row loads. -const profileCols = `id,name,format,COALESCE(base_url,''),COALESCE(proxy,''),model,COALESCE(api_key_hint,''),rate_per_second,rate_per_minute,context_window_k,COALESCE(reasoning_effort,''),is_default,priority,pool_exclude` -const profileColsKey = `id,name,format,COALESCE(base_url,''),COALESCE(proxy,''),model,COALESCE(api_key,''),rate_per_second,rate_per_minute,context_window_k,COALESCE(reasoning_effort,''),is_default,priority,pool_exclude` +const profileCols = `id,name,format,COALESCE(base_url,''),COALESCE(proxy,''),model,COALESCE(api_key_hint,''),rate_per_second,rate_per_minute,context_window_k,COALESCE(reasoning_effort,''),is_default,priority,pool_exclude,COALESCE(thinking_type,'')` +const profileColsKey = `id,name,format,COALESCE(base_url,''),COALESCE(proxy,''),model,COALESCE(api_key,''),rate_per_second,rate_per_minute,context_window_k,COALESCE(reasoning_effort,''),is_default,priority,pool_exclude,COALESCE(thinking_type,'')` // scanProfile reads one row in the profileCols / profileColsKey column order. The // 7th column lands in APIKeyHint or APIKey depending on which list the caller used. func scanProfile(sc interface{ Scan(...any) error }, into *string, p *LLMProfile) error { return sc.Scan(&p.ID, &p.Name, &p.Format, &p.BaseURL, &p.Proxy, &p.Model, into, - &p.RatePerSecond, &p.RatePerMinute, &p.ContextWindowK, &p.ReasoningEffort, &p.IsDefault, &p.Priority, &p.PoolExclude) + &p.RatePerSecond, &p.RatePerMinute, &p.ContextWindowK, &p.ReasoningEffort, &p.IsDefault, &p.Priority, &p.PoolExclude, &p.ThinkingType) } func (d *DB) ListProfiles() ([]*LLMProfile, error) { @@ -121,18 +123,18 @@ func (d *DB) SaveProfile(p *LLMProfile) (int64, error) { } if p.ID == 0 { var id int64 - err := d.QueryRow(`INSERT INTO llm_profiles(name,format,base_url,proxy,model,api_key,api_key_hint,rate_per_second,rate_per_minute,context_window_k,reasoning_effort,priority,pool_exclude) -VALUES ($1,$2,NULLIF($3,''),NULLIF($4,''),$5,NULLIF($6,''),NULLIF($7,''),$8,$9,$10,$11,$12,$13) RETURNING id`, - p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude).Scan(&id) + err := d.QueryRow(`INSERT INTO llm_profiles(name,format,base_url,proxy,model,api_key,api_key_hint,rate_per_second,rate_per_minute,context_window_k,reasoning_effort,priority,pool_exclude,thinking_type) +VALUES ($1,$2,NULLIF($3,''),NULLIF($4,''),$5,NULLIF($6,''),NULLIF($7,''),$8,$9,$10,$11,$12,$13,$14) RETURNING id`, + p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType).Scan(&id) return id, err } if p.APIKey == "" { - _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11 WHERE id=$12`, - p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ID) + _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,rate_per_second=$6,rate_per_minute=$7,context_window_k=$8,reasoning_effort=$9,priority=$10,pool_exclude=$11,thinking_type=$12 WHERE id=$13`, + p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.ID) return p.ID, err } - _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,api_key=$6,api_key_hint=$7,rate_per_second=$8,rate_per_minute=$9,context_window_k=$10,reasoning_effort=$11,priority=$12,pool_exclude=$13 WHERE id=$14`, - p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ID) + _, err := d.Exec(`UPDATE llm_profiles SET name=$1,format=$2,base_url=NULLIF($3,''),proxy=NULLIF($4,''),model=$5,api_key=$6,api_key_hint=$7,rate_per_second=$8,rate_per_minute=$9,context_window_k=$10,reasoning_effort=$11,priority=$12,pool_exclude=$13,thinking_type=$14 WHERE id=$15`, + p.Name, p.Format, p.BaseURL, p.Proxy, p.Model, p.APIKey, hint, p.RatePerSecond, p.RatePerMinute, p.ContextWindowK, p.ReasoningEffort, p.Priority, p.PoolExclude, p.ThinkingType, p.ID) return p.ID, err } diff --git a/db/schema.sql b/db/schema.sql index 5cf002b..8f1886a 100644 --- a/db/schema.sql +++ b/db/schema.sql @@ -235,7 +235,10 @@ CREATE TABLE IF NOT EXISTS llm_profiles ( rate_per_second DOUBLE PRECISION NOT NULL DEFAULT 0, rate_per_minute DOUBLE PRECISION NOT NULL DEFAULT 0, context_window_k INTEGER NOT NULL DEFAULT 0, + -- 思考参数拆成两个独立字段:thinking_type=思考开关(''/disabled/enabled), + -- reasoning_effort=思考强度(''/low/medium/high/xhigh/max),互不牵连。 reasoning_effort TEXT NOT NULL DEFAULT '', + thinking_type TEXT NOT NULL DEFAULT '', is_default BOOLEAN NOT NULL DEFAULT false, -- 轮询(故障转移)参数,见 docs/LLM轮询设计.md: -- priority 顺位,越大越先被选中;激活配置(is_default)永远排链首,与本值无关。 @@ -253,6 +256,26 @@ CREATE TRIGGER trg_llm_upd BEFORE UPDATE ON llm_profiles ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS priority INTEGER NOT NULL DEFAULT 0; ALTER TABLE llm_profiles ADD COLUMN IF NOT EXISTS pool_exclude BOOLEAN NOT NULL DEFAULT false; +-- 思考开关字段 thinking_type,从旧的单一 reasoning_effort 语义一次性拆分而来。 +-- schema.sql 每次启动都执行,故迁移必须只跑一次:仅当该列尚不存在时才回填, +-- 否则每次启动都会把用户后来手动设的组合覆盖回去。旧 reasoning_effort 语义: +-- 'off' → 显式关闭 → thinking_type='disabled',强度清空 +-- 'low/medium/high/max' → 开启+强度 → thinking_type='enabled',强度保留 +-- '' → 不发送 → 两者皆空(默认) +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'llm_profiles' AND column_name = 'thinking_type' + ) THEN + ALTER TABLE llm_profiles ADD COLUMN thinking_type TEXT NOT NULL DEFAULT ''; + UPDATE llm_profiles SET thinking_type = 'enabled' + WHERE reasoning_effort IN ('low','medium','high','max'); + UPDATE llm_profiles SET thinking_type = 'disabled', reasoning_effort = '' + WHERE reasoning_effort = 'off'; + END IF; +END $$; + -- LLM 轮询熔断状态:某个配置连续失败(余额不足/key 失效/限流)后进入冷却,冷却期内 -- 轮询直接跳过它。内存态为准,这里落库只为重启后不丢冷却窗口——加载时只取尚未 -- 到期的行(open_until > now),已到期的自然回到"正常",等下一次调用半开试探。 diff --git a/server/dto.go b/server/dto.go index 9c320c0..599e47f 100644 --- a/server/dto.go +++ b/server/dto.go @@ -500,6 +500,7 @@ type LLMProfileDTO struct { RatePerSecond float64 `json:"rate_per_second"` RatePerMinute float64 `json:"rate_per_minute"` ContextWindowK int `json:"context_window_k"` + ThinkingType string `json:"thinking_type"` ReasoningEffort string `json:"reasoning_effort"` IsDefault bool `json:"is_default"` // 轮询(故障转移)参数:priority 越大越先被选中(激活配置恒为链首); @@ -520,6 +521,7 @@ func llmProfileDTO(p *db.LLMProfile) LLMProfileDTO { RatePerSecond: p.RatePerSecond, RatePerMinute: p.RatePerMinute, ContextWindowK: p.ContextWindowK, + ThinkingType: p.ThinkingType, ReasoningEffort: p.ReasoningEffort, IsDefault: p.IsDefault, Priority: p.Priority, diff --git a/server/server.go b/server/server.go index 8962cc5..36895b6 100644 --- a/server/server.go +++ b/server/server.go @@ -296,6 +296,7 @@ func (s *Server) loadLLMConfig() (agent.Config, bool) { cfg := agent.ConfigFrom(p.Format, p.Model, p.BaseURL, p.APIKey, p.Proxy) cfg.RatePerSecond, cfg.RatePerMinute = p.RatePerSecond, p.RatePerMinute cfg.ContextWindowK = p.ContextWindowK + cfg.ThinkingType = p.ThinkingType cfg.ReasoningEffort = p.ReasoningEffort if cfg.APIKey == "" { return cfg, false @@ -329,7 +330,7 @@ func (s *Server) saveLLMConfig(cfg agent.Config) error { newID, err := s.m.pg.SaveProfile(&db.LLMProfile{ ID: id, Name: "default", Format: format, Model: cfg.Model, BaseURL: cfg.BaseURL, Proxy: cfg.Proxy, APIKey: cfg.APIKey, RatePerSecond: cfg.RatePerSecond, RatePerMinute: cfg.RatePerMinute, - ContextWindowK: cfg.ContextWindowK, ReasoningEffort: cfg.ReasoningEffort, IsDefault: true, + ContextWindowK: cfg.ContextWindowK, ThinkingType: cfg.ThinkingType, ReasoningEffort: cfg.ReasoningEffort, IsDefault: true, Priority: priority, PoolExclude: poolExclude, }) if err != nil { @@ -459,6 +460,7 @@ func (s *Server) loadProfileConfig(id int64) (agent.Config, bool) { cfg := agent.ConfigFrom(p.Format, p.Model, p.BaseURL, p.APIKey, p.Proxy) cfg.RatePerSecond, cfg.RatePerMinute = p.RatePerSecond, p.RatePerMinute cfg.ContextWindowK = p.ContextWindowK + cfg.ThinkingType = p.ThinkingType cfg.ReasoningEffort = p.ReasoningEffort if cfg.APIKey == "" { return cfg, false @@ -1175,6 +1177,7 @@ func (s *Server) getLLM(w http.ResponseWriter, r *http.Request) { "rate_per_second": s.llmCfg.RatePerSecond, "rate_per_minute": s.llmCfg.RatePerMinute, "context_window_k": s.llmCfg.ContextWindowK, + "thinking_type": s.llmCfg.ThinkingType, "reasoning_effort": s.llmCfg.ReasoningEffort, }) } @@ -1190,6 +1193,7 @@ func (s *Server) setLLM(w http.ResponseWriter, r *http.Request) { RatePerSecond float64 `json:"rate_per_second"` RatePerMinute float64 `json:"rate_per_minute"` ContextWindowK int `json:"context_window_k"` + ThinkingType string `json:"thinking_type"` ReasoningEffort string `json:"reasoning_effort"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { @@ -1198,6 +1202,7 @@ func (s *Server) setLLM(w http.ResponseWriter, r *http.Request) { } cfg := agent.ConfigFrom(req.Provider, req.Model, req.BaseURL, req.APIKey, req.Proxy) cfg.RatePerSecond, cfg.RatePerMinute = req.RatePerSecond, req.RatePerMinute + cfg.ThinkingType = req.ThinkingType cfg.ReasoningEffort = req.ReasoningEffort if k := req.ContextWindowK; k > 0 { // 0 = keep default (200K); cap at 1M if k > 1000 { @@ -1244,6 +1249,7 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { BaseURL string `json:"base_url"` Proxy string `json:"proxy"` APIKey string `json:"api_key"` + ThinkingType string `json:"thinking_type"` ReasoningEffort string `json:"reasoning_effort"` ProfileID *int64 `json:"profile_id"` // 测已存 profile 时传入:api_key 为空则用它存的 key } @@ -1254,6 +1260,7 @@ func (s *Server) testLLM(w http.ResponseWriter, r *http.Request) { cfg := agent.ConfigFrom(req.Provider, req.Model, req.BaseURL, req.APIKey, req.Proxy) // mirror production: send the SAME thinking params so a provider that rejects the // reasoning_effort/thinking field fails the test too (no false "test ok, run 400"). + cfg.ThinkingType = req.ThinkingType cfg.ReasoningEffort = req.ReasoningEffort // API Key 解析优先级:表单输入 > 指定 profile 存的 key > 全局配置的 key。 // 已存 profile 的 key 不回传浏览器,所以测试已存配置时表单为空,需从 DB 取。 diff --git a/web/src/app/(main)/system/llm/page.tsx b/web/src/app/(main)/system/llm/page.tsx index fb37607..5ac1e28 100644 --- a/web/src/app/(main)/system/llm/page.tsx +++ b/web/src/app/(main)/system/llm/page.tsx @@ -29,26 +29,26 @@ import { api } from "@/lib/api"; import type { LLMPoolMember, LLMPoolStatus, LLMProfile } from "@/lib/types"; import { cn } from "@/lib/utils"; -// 思考强度档位(仅在开关打开时生效)。 +// 思考开关(thinking.type)与思考强度(reasoning_effort)是两个【互相独立】的字段, +// 各自单独设置——有些接口没有 thinking 字段、只靠强度参数就能激活思考,故需解耦。 +// 存库空字符串 = 该字段【不发送】;Radix Select 不接受空 value,故 UI 用 "none" +// 哨兵表示不发送,存取时与 "" 互转(NONE / fromStore / toStore)。 +const NONE = "none"; +const fromStore = (v?: string) => (v ? v : NONE); +const toStore = (v: string) => (v === NONE ? "" : v); +const THINKING_TYPES: { value: string; label: string }[] = [ + { value: NONE, label: "不发送(默认)" }, + { value: "disabled", label: "关闭" }, + { value: "enabled", label: "开启" }, +]; const EFFORT_LEVELS: { value: string; label: string }[] = [ - { value: "low", label: "低" }, - { value: "medium", label: "中" }, - { value: "high", label: "高" }, - { value: "max", label: "最大" }, + { value: NONE, label: "不发送(默认)" }, + { value: "low", label: "low" }, + { value: "medium", label: "medium" }, + { value: "high", label: "high" }, + { value: "xhigh", label: "xhigh" }, + { value: "max", label: "max" }, ]; -// 思考模式三态 ↔ 存库字符串 reasoning_effort: -// none→""(thinking / reasoning_effort 两个字段都【不发送】,兼容不支持该字段的模型,如 MiniMax) -// off →"off"(发送 thinking:{type:disabled},用于默认开启思考、需要显式关闭的模型) -// on →强度值(发送 thinking:{enabled} + reasoning_effort) -type ThinkMode = "none" | "off" | "on"; -const THINK_MODES: { value: ThinkMode; label: string }[] = [ - { value: "none", label: "不发送(默认)" }, - { value: "off", label: "显式关闭" }, - { value: "on", label: "开启" }, -]; -const toStore = (mode: ThinkMode, effort: string) => (mode === "on" ? effort : mode === "off" ? "off" : ""); -const modeFromStore = (v?: string): ThinkMode => (!v ? "none" : v === "off" ? "off" : "on"); -const effortFromStore = (v?: string) => (v && v !== "off" ? v : "high"); function cooldownText(secs: number) { if (secs <= 0) return ""; @@ -305,8 +305,8 @@ function ProfileSheet({ const [rps, setRps] = React.useState("0"); const [rpm, setRpm] = React.useState("0"); const [cw, setCw] = React.useState("0"); // 上下文窗口(K tokens);0=默认200K - const [thinkMode, setThinkMode] = React.useState("none"); - const [effort, setEffort] = React.useState("high"); + const [thinkingType, setThinkingType] = React.useState(NONE); + const [effort, setEffort] = React.useState(NONE); const [priority, setPriority] = React.useState("0"); // 轮询顺位;越大越先 const [poolExclude, setPoolExclude] = React.useState(false); const [testing, setTesting] = React.useState(false); @@ -327,8 +327,8 @@ function ProfileSheet({ setRps(String(profile?.rate_per_second ?? 0)); setRpm(String(profile?.rate_per_minute ?? 0)); setCw(String(profile?.context_window_k ?? 0)); - setThinkMode(modeFromStore(profile?.reasoning_effort)); - setEffort(effortFromStore(profile?.reasoning_effort)); + setThinkingType(fromStore(profile?.thinking_type)); + setEffort(fromStore(profile?.reasoning_effort)); setPriority(String(profile?.priority ?? 0)); setPoolExclude(profile?.pool_exclude ?? false); setApiKey(""); @@ -365,7 +365,7 @@ function ProfileSheet({ try { // 用配置实际会跑的思考参数来测,这样不支持该字段的模型在这里就失败, // 而不是等到跑任务时才炸。传 profile id:Key 输入框留空时用已存的 Key。 - const r = await api.testLLM(format, model, baseUrl, apiKey, proxy, toStore(thinkMode, effort), profileId); + const r = await api.testLLM(format, model, baseUrl, apiKey, proxy, toStore(thinkingType), toStore(effort), profileId); if (r.ok) toast.success(`连接成功 · ${r.latency_ms ?? "?"}ms · ${r.model ?? model}`); else toast.error(`连接失败:${r.error ?? "未知"}`); } catch (e) { @@ -394,7 +394,8 @@ function ProfileSheet({ rate_per_second: Number(rps) || 0, rate_per_minute: Number(rpm) || 0, context_window_k: Number(cw) || 0, - reasoning_effort: toStore(thinkMode, effort), + thinking_type: toStore(thinkingType), + reasoning_effort: toStore(effort), priority: Number(priority) || 0, pool_exclude: poolExclude, }); @@ -601,18 +602,18 @@ function ProfileSheet({
- +

- 不发送=不带思考字段(兼容 MiniMax 等不支持该字段的模型);显式关闭=发 - disabled(默认开思考的模型才需要);开启=先推理再作答。 + 控制是否发送 thinking 字段。不发送=不带该字段(兼容 MiniMax 等不支持 + 的模型);关闭=发 disabled;开启=发 enabled。与下面的强度互相独立。

- - {THINK_MODES.map((o) => ( + {THINKING_TYPES.map((o) => ( {o.label} @@ -620,28 +621,27 @@ function ProfileSheet({
- {thinkMode === "on" && ( -
-
- -

- 推理投入档位,越高越深入(OpenAI reasoning_effort / Anthropic output_config.effort)。 -

-
- +
+
+ +

+ 独立的强度档位(OpenAI reasoning_effort / Anthropic output_config.effort)。 + 有些接口没有 thinking 字段、只靠强度即可激活思考,故可单独设置、不发送思考开关。 +

- )} + +
diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index bbf67bc..6e4bc40 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -542,6 +542,7 @@ export const api = { rate_per_second?: number; rate_per_minute?: number; context_window_k?: number; + thinking_type?: string; reasoning_effort?: string; }>("/llm"), setLLM: ( @@ -560,6 +561,7 @@ export const api = { base_url: string, api_key: string, proxy = "", + thinking_type = "", reasoning_effort = "", profile_id?: number, ) => @@ -569,6 +571,7 @@ export const api = { base_url, proxy, api_key, + thinking_type, reasoning_effort, profile_id, }), @@ -584,7 +587,8 @@ export const api = { rate_per_second?: number; rate_per_minute?: number; context_window_k?: number; - reasoning_effort?: string; // ""|"off"|"low"|"medium"|"high"|"max" + thinking_type?: string; // ""(不发送)|"disabled"|"enabled" + reasoning_effort?: string; // ""(不发送)|"low"|"medium"|"high"|"xhigh"|"max" priority?: number; // 轮询顺位,越大越先用 pool_exclude?: boolean; // true=不作为故障转移目标 }) => post<{ id: number }>("/llm/profiles", p), diff --git a/web/src/lib/mock/data.ts b/web/src/lib/mock/data.ts index daa2d15..ae772e7 100644 --- a/web/src/lib/mock/data.ts +++ b/web/src/lib/mock/data.ts @@ -617,14 +617,14 @@ export const audit: Audit = { // ── LLM profiles ───────────────────────────────────────────────────────────── export const llmProfiles: LLMProfile[] = [ - { id: "1", name: "Claude Opus 4.8", format: "anthropic", model: "claude-opus-4-8", api_key_hint: "…a3f2", rate_per_second: 0, rate_per_minute: 0, context_window_k: 1000, reasoning_effort: "high", is_default: true, priority: 0, pool_exclude: false }, - { id: "2", name: "DeepSeek V4", format: "openai", base_url: "https://api.deepseek.com", model: "deepseek-v4-flash", api_key_hint: "…9c11", rate_per_second: 0, rate_per_minute: 60, context_window_k: 128, reasoning_effort: "", is_default: false, priority: 10, pool_exclude: false }, + { id: "1", name: "Claude Opus 4.8", format: "anthropic", model: "claude-opus-4-8", api_key_hint: "…a3f2", rate_per_second: 0, rate_per_minute: 0, context_window_k: 1000, thinking_type: "enabled", reasoning_effort: "high", is_default: true, priority: 0, pool_exclude: false }, + { id: "2", name: "DeepSeek V4", format: "openai", base_url: "https://api.deepseek.com", model: "deepseek-v4-flash", api_key_hint: "…9c11", rate_per_second: 0, rate_per_minute: 60, context_window_k: 128, thinking_type: "", reasoning_effort: "", is_default: false, priority: 10, pool_exclude: false }, ]; export const llmConfig = { configured: true, provider: "anthropic", model: "claude-opus-4-8", base_url: "", proxy: "", key_set: true, - rate_per_second: 0, rate_per_minute: 0, context_window_k: 1000, reasoning_effort: "high", + rate_per_second: 0, rate_per_minute: 0, context_window_k: 1000, thinking_type: "enabled", reasoning_effort: "high", }; // ── Agents ─────────────────────────────────────────────────────────────────── diff --git a/web/src/lib/types.ts b/web/src/lib/types.ts index 3d44db2..141aeee 100644 --- a/web/src/lib/types.ts +++ b/web/src/lib/types.ts @@ -623,7 +623,9 @@ export interface LLMProfile { rate_per_second: number; rate_per_minute: number; context_window_k?: number; - // 思考模式: ""=默认(不发送) | "off"=关闭 | "low"/"medium"/"high"/"max"=开启并设强度 + // 思考开关(thinking.type): ""=不发送(默认) | "disabled"=关闭 | "enabled"=开启 + thinking_type?: string; + // 思考强度: ""=不发送(默认) | "low"/"medium"/"high"/"xhigh"/"max" reasoning_effort?: string; is_default: boolean; // 轮询顺位:越大越先被选中。激活配置恒为链首,与本值无关。