feat: 三协议互转网关 + 鉴权修复 + 管理端增强

后端
- 新增 proxy/convert 三协议(chat/messages/responses)请求、响应与 SSE 流式互转,
  以 Chat 为中间模型;usage.go 统一提取三协议 token 用量(含单测)
- gateway: 跨协议调度(渠道未声明客户端协议时转为渠道首选格式),
  streamResponse 按 \n\n 分块逐行转换直通,bufferResponse 转换失败时剥非 JSON 前缀
- gateway: 新增 SetUsageRecorder 注入异步用量记录器
- auth_llm: 修复 key_prefix 查询长度错配([:8] vs 存储的 [:12])导致全部 401;
  修复长度 8-11 的 key 切片越界 panic;统一 unauthorized 响应
- usage: 日报表改为增量累加 upsert,避免多次 flush 互相清零;记录协议/错误码/时延等字段
- channel: 新增渠道并发槽 TryAcquire;健康检查支持可配置参数
- api: 新增 admin 渠道/模型/系统配置管理端点(旧端点保留兼容)

前端
- 新增渠道管理、模型管理、系统配置视图与 ChannelModelsDrawer
- 新增 ui 基础组件(Button/Badge/Input/Modal)与 protocol.ts
- 调整 Toast 样式、密钥页、路由菜单;dev 代理默认指向 3000 端口
This commit is contained in:
Sakurasan
2026-08-31 22:29:09 +08:00
parent e472ed93d5
commit f81b364436
34 changed files with 5171 additions and 179 deletions
+131
View File
@@ -0,0 +1,131 @@
package convert
import (
"encoding/json"
)
// TokenUsage 从上游响应提取的 token 用量。
// 三种协议的字段名不同,此处统一为:input / output / cache_read / cache_creation,
// 供用量记录与计费使用。
type TokenUsage struct {
InputTokens int
OutputTokens int
CacheReadTokens int
CacheCreationTokens int
}
// has 判断是否真的拿到了非零用量(过滤掉没有 usage 字段的响应)。
func (u *TokenUsage) has() bool {
return u.InputTokens > 0 || u.OutputTokens > 0 ||
u.CacheReadTokens > 0 || u.CacheCreationTokens > 0
}
// mergeJSON 把一张 usage 对象并入累计值。proto 决定字段名(chat/responses 与 messages 不同)。
func (u *TokenUsage) mergeJSON(raw map[string]any, proto string) {
switch proto {
case ProtoChat, ProtoResponses:
in, _ := raw["prompt_tokens"].(float64)
out, _ := raw["completion_tokens"].(float64)
if in == 0 && out == 0 {
in, _ = raw["input_tokens"].(float64)
out, _ = raw["output_tokens"].(float64)
}
u.InputTokens += int(in)
u.OutputTokens += int(out)
if d, ok := raw["prompt_tokens_details"].(map[string]any); ok {
if c, _ := d["cached_tokens"].(float64); c > 0 {
u.CacheReadTokens += int(c)
}
}
if d, ok := raw["input_tokens_details"].(map[string]any); ok {
if c, _ := d["cached_tokens"].(float64); c > 0 {
u.CacheReadTokens += int(c)
}
}
case ProtoMessages:
in, _ := raw["input_tokens"].(float64)
out, _ := raw["output_tokens"].(float64)
u.InputTokens += int(in)
u.OutputTokens += int(out)
if c, _ := raw["cache_read_input_tokens"].(float64); c > 0 {
u.CacheReadTokens += int(c)
}
if c, _ := raw["cache_creation_input_tokens"].(float64); c > 0 {
u.CacheCreationTokens += int(c)
}
}
}
// ExtractUsageJSON 从完整非流式响应体中提取用量。proto 为上游协议。
// 返回 (用量, 是否有效)。
func ExtractUsageJSON(body []byte, proto string) (TokenUsage, bool) {
var top map[string]any
if err := json.Unmarshal(body, &top); err != nil {
return TokenUsage{}, false
}
var u TokenUsage
if usage, ok := top["usage"].(map[string]any); ok {
u.mergeJSON(usage, proto)
}
return u, u.has()
}
// StreamUsageAccum 流式用量累计器。逐行喂入上游 SSE 的 data 载荷,
// 按协议分别取各事件里的 usage 字段(各事件只会携带一部分字段,取最大值合并)。
type StreamUsageAccum struct {
u TokenUsage
}
// NewStreamUsageAccum 创建一个流式用量累计器。
func NewStreamUsageAccum() *StreamUsageAccum {
return &StreamUsageAccum{}
}
// Feed 喂入一行 SSE data 载荷(不含 "data:" 前缀与换行)。
func (a *StreamUsageAccum) Feed(payload []byte, proto string) {
var top map[string]any
if json.Unmarshal(payload, &top) != nil {
return
}
var t TokenUsage
switch proto {
case ProtoChat:
if usage, ok := top["usage"].(map[string]any); ok {
t.mergeJSON(usage, proto)
}
case ProtoResponses:
// response.completed 事件把用量放在 response.usage 下。
if resp, ok := top["response"].(map[string]any); ok {
if usage, ok := resp["usage"].(map[string]any); ok {
t.mergeJSON(usage, proto)
}
}
case ProtoMessages:
// message_start: {message: {usage: {input_tokens, cache_*}}}
// message_delta: {usage: {output_tokens}}
if msg, ok := top["message"].(map[string]any); ok {
if usage, ok := msg["usage"].(map[string]any); ok {
t.mergeJSON(usage, proto)
}
}
if usage, ok := top["usage"].(map[string]any); ok {
var t2 TokenUsage
t2.mergeJSON(usage, proto)
t.InputTokens = max(t.InputTokens, t2.InputTokens)
t.OutputTokens = max(t.OutputTokens, t2.OutputTokens)
t.CacheReadTokens = max(t.CacheReadTokens, t2.CacheReadTokens)
t.CacheCreationTokens = max(t.CacheCreationTokens, t2.CacheCreationTokens)
}
default:
return
}
a.u.InputTokens = max(a.u.InputTokens, t.InputTokens)
a.u.OutputTokens = max(a.u.OutputTokens, t.OutputTokens)
a.u.CacheReadTokens = max(a.u.CacheReadTokens, t.CacheReadTokens)
a.u.CacheCreationTokens = max(a.u.CacheCreationTokens, t.CacheCreationTokens)
}
// Usage 返回当前累计用量。
func (a *StreamUsageAccum) Usage() TokenUsage {
return a.u
}