修复流式请求记账为 0 消耗:支持 SSE 多行块 usage 提取 + 无上游 usage 时 tiktoken 估算
- sseContentText/scanUsage 兼容 event:+data: 多行块(转换器 eventLine 产出) - usageFromMap 新增 delta.usage 支持(Anthropic message_delta 标准格式) - finishUsage: 流式成功但上游不返回 usage(如火山方舟)时按内容 tiktoken 估算,in/out 独立估算 - .gitignore 忽略 server/web 本地静态托管软链
This commit is contained in:
@@ -7,6 +7,7 @@ dist/
|
||||
# Go
|
||||
server/bin/
|
||||
server/data/
|
||||
server/web/ # 本地静态托管软链(指向 ../web/dist),不入库
|
||||
scripts/mockupstream/bin/
|
||||
|
||||
# TypeScript 增量构建产物
|
||||
|
||||
@@ -101,10 +101,14 @@ func requestText(body []byte) string {
|
||||
return strings.Join(parts, "\n")
|
||||
}
|
||||
|
||||
// sseContentText 提取一条 SSE data 行中的内容文本(chat delta.content / responses delta / messages delta.text)。
|
||||
// sseContentText 提取一条 SSE 中的内容文本(chat delta.content / responses delta / messages delta.text)。
|
||||
// 兼容单 data: 行与 event:+data: 多行块(转换器 eventLine 产出的块)。
|
||||
func sseContentText(line []byte) string {
|
||||
s := string(line)
|
||||
if strings.HasPrefix(s, "data: ") {
|
||||
// 多行块:取最后一个 data: 行(event: 头 + data: 载荷拼在一个 []byte 里)
|
||||
if idx := strings.LastIndex(s, "\ndata: "); idx >= 0 {
|
||||
s = s[idx+len("\ndata: "):]
|
||||
} else if strings.HasPrefix(s, "data: ") {
|
||||
s = strings.TrimPrefix(s, "data: ")
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
@@ -375,7 +379,10 @@ func scanUsage(line []byte) json.RawMessage {
|
||||
if !strings.Contains(s, `"usage"`) {
|
||||
return nil
|
||||
}
|
||||
if strings.HasPrefix(s, "data: ") {
|
||||
// 兼容单 data: 行与 event:+data: 多行块(转换器 eventLine 产出的块)
|
||||
if idx := strings.LastIndex(s, "\ndata: "); idx >= 0 {
|
||||
s = s[idx+len("\ndata: "):]
|
||||
} else if strings.HasPrefix(s, "data: ") {
|
||||
s = strings.TrimPrefix(s, "data: ")
|
||||
}
|
||||
s = strings.TrimSpace(s)
|
||||
@@ -400,7 +407,11 @@ func scanUsage(line []byte) json.RawMessage {
|
||||
return nil
|
||||
}
|
||||
|
||||
// usageFromMap 从 map 顶层或 message 子对象中取 usage。
|
||||
// usageFromMap 从 map 顶层、message 或 delta 子对象中取 usage。
|
||||
// 覆盖三种形态:
|
||||
// - OpenAI 流式末块顶层 usage
|
||||
// - Anthropic 非流式 / message_start 的 message.usage
|
||||
// - Anthropic 流式 message_delta 的 delta.usage(真实 token 计数所在)
|
||||
func usageFromMap(m map[string]json.RawMessage) json.RawMessage {
|
||||
if u, ok := m["usage"]; ok && string(u) != "null" {
|
||||
return u
|
||||
@@ -413,6 +424,14 @@ func usageFromMap(m map[string]json.RawMessage) json.RawMessage {
|
||||
}
|
||||
}
|
||||
}
|
||||
if deltaRaw, ok := m["delta"]; ok {
|
||||
var delta map[string]json.RawMessage
|
||||
if json.Unmarshal(deltaRaw, &delta) == nil {
|
||||
if u, ok := delta["usage"]; ok && string(u) != "null" {
|
||||
return u
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -499,11 +518,15 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
|
||||
modelName, _ := c.Get("model_name")
|
||||
mn, _ := modelName.(string)
|
||||
|
||||
// 流式中断(canceled):上游最终 usage 可能未返回,按已生成内容用 tiktoken 估算
|
||||
if status == store.UsageStatusCanceled {
|
||||
// 上游未返回真实 usage 时估算(tiktoken):
|
||||
// - 流式中断(canceled):上游最终 usage 可能未返回
|
||||
// - 流式正常结束(success):部分上游(如火山方舟 Anthropic 流式)message_start.usage 恒为 0、
|
||||
// message_delta 不带 usage,只能按已收发内容估算,否则记账为 0 消耗
|
||||
// 非流式上游必返回 usage,此处 in/out 非 0 不受影响。
|
||||
if status == store.UsageStatusCanceled || status == store.UsageStatusSuccess {
|
||||
if in == 0 {
|
||||
if est, ok := c.Get("est_input_text"); ok {
|
||||
if v, ok2 := est.(string); ok2 {
|
||||
if v, ok2 := est.(string); ok2 && v != "" {
|
||||
in = int64(tokenizer.Count(v, mn))
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user