Files
Cyrene_v2/internal/services/gateway/admin_llm_provider.go
T
AskaEth 1ac02bd77e
CI / Backend (build / vet / test / cross-compile) (push) Has been cancelled
CI / Frontend (tsc + vite build) (push) Has been cancelled
feat(gateway,admin-web): 面板可编辑 provider 推理形状 / 角色级路由 / multimodal 真语义(裁决 03.4.2/03.4.1)
- 后端:AdminLlmProvider 暴露 reasoning{kind}(恒下发,""=未声明=auto)、personaModels、personaProviders;
  顶层新增 reasoningKinds / personaProviderOptions 两个**取值域**(唯一来源= runtimeconfig 导出枚举,
  校验错误文案也由枚举拼接 ⇒ 不可能漂移)
- 读写共用同一映射(admin_llm_provider.go),避免「读侧少一个键 ⇒ 面板一保存就抹掉」
- 写侧语义:缺失=保留、给出=整块/整表替换;非法值一律 runtimeconfig.Group.Validate() ⇒ 400 bad_request
  (实测非法 kind 返回 400 且 models.json 完全不动,而非 500 静默失败)
- 前端:新增 LlmProviderRouting(推理形状下拉 + multimodal 开关 + 按角色路由编辑器);
  目录能力位改三态(true/false/未标注),「未标注」不再谎称「不支持」;目录/角色清单不可用时
  **不阻塞编辑**且与「字段为空」是不同状态;绝无 mock/sample 回退
- 复核:build/vet 绿 + gateway/config 包测试绿 + tsc 0 错 + vite build 成功;2 条变异反证(删 reasoning
  补丁 → 2 例红;删 Group.Validate → 该 400 用例红)已还原
- 注意(主会话已知):npm run build 把新 SPA 写进 frontend/web/dist,而运行中的二进制优先从磁盘读该目录
  ⇒ :10942 目前是「新面板 + 旧后端」,等阶段 5 全绿后按红线「先 build 到位、再一次 restart」切换
2026-10-04 13:06:54 +08:00

68 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,
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
}