feat(llm): 思考开关与思考强度拆成两个独立可配置参数
- 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
This commit is contained in:
1 parent
834e94535a
commit
863e44ec24
9 files changed
+121
-83
No files matched your search
+12
-14
@@ -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}
|
||||
}
|
||||
|
||||
+15
-13
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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),已到期的自然回到"正常",等下一次调用半开试探。
|
||||
|
||||
@@ -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,
|
||||
|
||||
+8
-1
@@ -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 取。
|
||||
|
||||
@@ -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<ThinkMode>("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({
|
||||
<div className="grid gap-3 rounded-lg border p-3">
|
||||
<div className="flex items-center justify-between gap-4">
|
||||
<div className="grid gap-0.5">
|
||||
<Label className="text-sm">思考模式 · Extended Thinking</Label>
|
||||
<Label className="text-sm">思考开关 · thinking.type</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
不发送=不带思考字段(兼容 MiniMax 等不支持该字段的模型);显式关闭=发
|
||||
disabled(默认开思考的模型才需要);开启=先推理再作答。
|
||||
控制是否发送 thinking 字段。不发送=不带该字段(兼容 MiniMax 等不支持
|
||||
的模型);关闭=发 disabled;开启=发 enabled。与下面的强度互相独立。
|
||||
</p>
|
||||
</div>
|
||||
<Select value={thinkMode} onValueChange={(v) => setThinkMode(v as ThinkMode)}>
|
||||
<Select value={thinkingType} onValueChange={setThinkingType}>
|
||||
<SelectTrigger className="w-32 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{THINK_MODES.map((o) => (
|
||||
{THINKING_TYPES.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
@@ -620,28 +621,27 @@ function ProfileSheet({
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{thinkMode === "on" && (
|
||||
<div className="flex items-center justify-between gap-4 border-t pt-3">
|
||||
<div className="grid gap-0.5">
|
||||
<Label className="text-sm">思考强度</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
推理投入档位,越高越深入(OpenAI reasoning_effort / Anthropic output_config.effort)。
|
||||
</p>
|
||||
</div>
|
||||
<Select value={effort} onValueChange={setEffort}>
|
||||
<SelectTrigger className="w-28 shrink-0">
|
||||
<SelectValue placeholder="强度" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{EFFORT_LEVELS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<div className="flex items-center justify-between gap-4 border-t pt-3">
|
||||
<div className="grid gap-0.5">
|
||||
<Label className="text-sm">思考强度 · reasoning_effort</Label>
|
||||
<p className="text-muted-foreground text-xs">
|
||||
独立的强度档位(OpenAI reasoning_effort / Anthropic output_config.effort)。
|
||||
有些接口没有 thinking 字段、只靠强度即可激活思考,故可单独设置、不发送思考开关。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
<Select value={effort} onValueChange={setEffort}>
|
||||
<SelectTrigger className="w-32 shrink-0">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{EFFORT_LEVELS.map((o) => (
|
||||
<SelectItem key={o.value} value={o.value}>
|
||||
{o.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+5
-1
@@ -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),
|
||||
|
||||
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -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;
|
||||
// 轮询顺位:越大越先被选中。激活配置恒为链首,与本值无关。
|
||||
|
||||
Reference in new issue
Block a user