模型支持协议扩展参数,用于关闭思考模式

DeepSeek 现役的 v4 系列全部默认开启思考模式,没有单独的非思考型模型
可换;官方给的关闭方式是在请求里带 thinking={"type":"disabled"}。

ai_models 表早有 extra_json 字段但从未接线,这里接通:模型配置里的
JSON 对象会合并进每次调用的请求体,因此任何协议级参数都能在管理端
配置,不必改代码。合并不会覆盖 model/messages/max_tokens/temperature/
stream —— 预算和结构由代码掌握,重试逻辑才不会被配置绕过。
写入前校验 JSON,避免非法值存进 JSON 列后每次调用才报错。

线上模型已配置为关闭思考:同一会话下由 10133 ms / 1024 输出 tokens、
回复被截断,变为 669 ms / 10 tokens、回复完整。max_tokens 相应回调至 512。

管理端模型表单新增「协议参数」输入框(该目录尚未纳入版本库,仅已发布)。

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Your Name
2026-09-04 11:19:17 +08:00
co-authored by Claude Opus 5
parent 812d8032e1
commit 151834caac
3 changed files with 116 additions and 9 deletions
+49 -7
View File
@@ -5,6 +5,7 @@ import (
"crypto/sha256"
"database/sql"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"net/http"
@@ -27,18 +28,44 @@ type aiModelRequest struct {
TimeoutMS *int `json:"timeoutMs"`
Status *int `json:"status"`
IsDefault *bool `json:"isDefault"`
// Raw provider fields merged into every call, e.g. {"thinking":{"type":"disabled"}}
// to stop a model that reasons by default from spending the budget thinking.
Extra *string `json:"extra"`
}
const aiModelColumns = `id,name,protocol,base_url,model_name,api_key_cipher,temperature,max_tokens,timeout_ms,status,is_default,created_at,updated_at`
// The column is JSON, so an invalid value has to be refused before it is stored
// and starts failing every call at read time instead.
func aiModelExtra(value *string, fallback any) (any, error) {
if value == nil {
return fallback, nil
}
trimmed := strings.TrimSpace(*value)
if trimmed == "" {
return nil, nil
}
var parsed map[string]any
if err := json.Unmarshal([]byte(trimmed), &parsed); err != nil {
return nil, fmt.Errorf("扩展参数必须是 JSON 对象")
}
return trimmed, nil
}
const aiModelColumns = `id,name,protocol,base_url,model_name,api_key_cipher,temperature,max_tokens,timeout_ms,status,is_default,extra_json,created_at,updated_at`
func (a *App) scanAIModel(scan func(dest ...any) error) (aiModel, error) {
var model aiModel
var cipher string
var extra sql.NullString
var created, updated time.Time
if err := scan(&model.ID, &model.Name, &model.Protocol, &model.BaseURL, &model.ModelName, &cipher,
&model.Temperature, &model.MaxTokens, &model.TimeoutMS, &model.Status, &model.IsDefault, &created, &updated); err != nil {
&model.Temperature, &model.MaxTokens, &model.TimeoutMS, &model.Status, &model.IsDefault, &extra, &created, &updated); err != nil {
return aiModel{}, err
}
if strings.TrimSpace(extra.String) != "" {
if err := json.Unmarshal([]byte(extra.String), &model.Extra); err != nil {
return model, fmt.Errorf("模型 %s 的扩展参数不是合法 JSON", model.Name)
}
}
model.HasAPIKey = cipher != ""
if cipher != "" {
plain, err := a.decryptSecret(cipher)
@@ -187,6 +214,11 @@ func (a *App) adminCreateAIModel(w http.ResponseWriter, r *http.Request) {
status = *req.Status
}
isDefault := req.IsDefault != nil && *req.IsDefault
extra, err := aiModelExtra(req.Extra, nil)
if err != nil {
fail(w, http.StatusBadRequest, 20001, err.Error())
return
}
tx, err := a.db.BeginTx(r.Context(), nil)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "保存模型失败")
@@ -200,9 +232,9 @@ func (a *App) adminCreateAIModel(w http.ResponseWriter, r *http.Request) {
}
}
result, err := tx.ExecContext(r.Context(), `INSERT INTO ai_models
(name,protocol,base_url,model_name,api_key_cipher,temperature,max_tokens,timeout_ms,status,is_default)
VALUES(?,?,?,?,?,?,?,?,?,?)`,
req.Name, req.Protocol, req.BaseURL, req.ModelName, cipher, temperature, maxTokens, timeout, status, btoi(isDefault))
(name,protocol,base_url,model_name,api_key_cipher,temperature,max_tokens,timeout_ms,status,is_default,extra_json)
VALUES(?,?,?,?,?,?,?,?,?,?,?)`,
req.Name, req.Protocol, req.BaseURL, req.ModelName, cipher, temperature, maxTokens, timeout, status, btoi(isDefault), extra)
if err != nil {
if isDuplicateAIModelName(err) {
fail(w, http.StatusBadRequest, 20001, "模型名称已存在")
@@ -270,6 +302,16 @@ func (a *App) adminUpdateAIModel(w http.ResponseWriter, r *http.Request) {
if req.IsDefault != nil {
isDefault = *req.IsDefault
}
var existingExtra any
if len(existing.Extra) > 0 {
encoded, _ := json.Marshal(existing.Extra)
existingExtra = string(encoded)
}
extra, err := aiModelExtra(req.Extra, existingExtra)
if err != nil {
fail(w, http.StatusBadRequest, 20001, err.Error())
return
}
tx, err := a.db.BeginTx(r.Context(), nil)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "保存模型失败")
@@ -282,9 +324,9 @@ func (a *App) adminUpdateAIModel(w http.ResponseWriter, r *http.Request) {
return
}
}
args := append([]any{req.Name, req.Protocol, req.BaseURL, req.ModelName, temperature, maxTokens, timeout, status, btoi(isDefault)}, cipherArgs...)
args := append([]any{req.Name, req.Protocol, req.BaseURL, req.ModelName, temperature, maxTokens, timeout, status, btoi(isDefault), extra}, cipherArgs...)
args = append(args, id)
if _, err = tx.ExecContext(r.Context(), `UPDATE ai_models SET name=?,protocol=?,base_url=?,model_name=?,temperature=?,max_tokens=?,timeout_ms=?,status=?,is_default=?`+cipherClause+` WHERE id=?`, args...); err != nil {
if _, err = tx.ExecContext(r.Context(), `UPDATE ai_models SET name=?,protocol=?,base_url=?,model_name=?,temperature=?,max_tokens=?,timeout_ms=?,status=?,is_default=?,extra_json=?`+cipherClause+` WHERE id=?`, args...); err != nil {
if isDuplicateAIModelName(err) {
fail(w, http.StatusBadRequest, 20001, "模型名称已存在")
return