后端 - 新增 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 端口
132 lines
4.1 KiB
Go
132 lines
4.1 KiB
Go
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
|
||
}
|