Files
Cyrene_v2/internal/services/gateway/admin_llm_provider.go
T
AskaEth 47e56ca13c feat(admin): 面板暴露 contextWindow(可见可改)+ DocumentRef 语义注释同步
- 面板读写面与 maxTokens 逐处对齐:读 DTO 增 contextWindow(omitempty)、写请求 ContextWindow *int(缺失=保留、显式 0=未声明同义、负数 400)、回写 models.json;负数红线仍由 runtimeconfig.Group.Validate 唯一裁决,网关守卫只做「更早失败 + 面板可读字段名」。前端:类型 + 表单字段(提交前 /^\d+$/ 校验)+ 分组行角标 + Notice + 目录候选用早已解析却没人用的 contextLimit 预填。文案写明「重启后生效」(推导只在进程启动时算一次)。

- 顺手修既有红线违反:ProviderFields 87→76(抽 ApiKeyAndBudgetField)、LlmProviderForm 113→74(buildRequest 提为模块级纯函数)——本次碰过的文件里已无 >80 的函数,行为/文案逐字未变。

- DocumentRef 的 chars/truncated 注释从「抽取侧」改为「本回合注入侧」(全文长度只在 document-read 的 totalChars、extractTruncated 专指撞 8 MiB 安全上限);机械校验为空 ⇒ 代码零改动。

- 主会话独立验收:隔离 worktree Go 侧 build/vet + gateway/history 两包绿;主树 npx tsc --noEmit exit 0、npm run build 成功并 copy 到 internal/webui/dist;注释机械校验为空;变异由子代理复现(写路径不认该键 ⇒ 3 红;去掉网关负数守卫 ⇒ 400 变 500)。
2026-10-04 18:11:43 +08:00

69 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package gateway
import (
"fmt"
"strings"
"git.yeij.top/AskaEth/Cyrene_v2/internal/config/runtimeconfig"
)
// LLM provider 分组的线上投影:读(GET /admin/llm)与写(POST /admin/llm 的
// 响应)**必须同一套映射**。面板是「读回来 → 改一处 → 整组提交」,两个方向
// 一旦不一致(例如读侧漏了 reasoning),面板保存一次就会把那把键抹掉。
// adminLlmProviderDTO 把存储分组映射成线上的 AdminLlmProvider。API key 绝不
// 进入线上结构。reasoning 无条件下发(kind 可能为空串 = 未声明形状),
// persona 路由表按原值深拷贝,使调用方拿到的结构不会被后续改动影响。
func adminLlmProviderDTO(g runtimeconfig.Group, name string, enabled bool) AdminLlmProvider {
return AdminLlmProvider{
ID: g.ID,
Name: name,
Provider: g.Provider,
BaseURL: g.BaseURL,
Model: g.Model,
Multimodal: g.Multimodal,
Enabled: enabled,
Fallback: g.Fallback,
MaxTokens: g.MaxTokens,
ContextWindow: g.ContextWindow,
Reasoning: &AdminLlmReasoning{Kind: strings.TrimSpace(g.Reasoning.Kind)},
PersonaModels: cloneStringMap(g.PersonaModels),
PersonaProviders: cloneStringMap(g.PersonaProviders),
}
}
// cloneStringMap 浅拷贝一张 persona 路由表。nil 保持 nil,因此 omitempty 仍会
// 省略该键——「该组没有 per-persona 绑定」与「绑定表为空对象」在线上是同一种
// 状态(都表示没有路由),不需要两套表示。
func cloneStringMap(m map[string]string) map[string]string {
if m == nil {
return nil
}
out := make(map[string]string, len(m))
for k, v := range m {
out[k] = v
}
return out
}
// normalizePersonaMap 规整面板提交的 persona 路由表:键与值去除首尾空白。
// 只做规整,绝不丢弃或改写成别的值——空键/空值/未知协议名一律留给
// runtimeconfig.Group.Validate 报错(写端点据此回 400),因此非法输入
// 永远不会被静默吸收。规整的理由是路由查找用的是 trim 后的 persona id,
// 带空白的键会变成永远匹配不上的静默死配置;两个键在 trim 后撞到一起
// (" cyrene " 与 "cyrene")则直接报错,而不是悄悄让其中一个消失。
func normalizePersonaMap(field string, m map[string]string) (map[string]string, error) {
if m == nil {
return nil, nil
}
out := make(map[string]string, len(m))
for k, v := range m {
key := strings.TrimSpace(k)
if _, dup := out[key]; dup {
return nil, fmt.Errorf("%s: persona id %q 去除首尾空白后与另一条目重复", field, key)
}
out[key] = strings.TrimSpace(v)
}
return out, nil
}