feat: 补齐三个方向的流式工具调用;Anthropic 上游走 x-api-key;禁用模型真正下架

- 流式转换补齐 chat→responses / responses→chat / messages→responses 的工具调用(此前只转文本,SDK 客户端拿不到 tool_calls);completed 事件携带完整 output 数组
- Anthropic 协议上游同时发 x-api-key(官方 API 不接受 API key 走 Bearer)并透传 anthropic-beta
- 请求侧校验模型存在且启用:禁用模型 404 model_not_found,不再只是从 /v1/models 消失、实际照发照扣费;空模型名 400
- 模型在请求中途被删时按 0 记账留痕,不再按硬编码示例价扣用户的钱
- finishUsage 新增一行式请求日志(trace/user/model/channel/tokens/cost/status/latency),记账是异步的,日志是唯一即时排障依据
This commit is contained in:
Sakurasan committed 2026-10-29 02:56:37 +08:00
1 parent 984a31cbc5
commit d6db0f8162
6 files changed
+485 -31

No files matched your search

+15 -1
View File
@@ -10,6 +10,7 @@ import (
"errors"
"fmt"
"io"
"log"
"net/http"
"strconv"
"strings"
@@ -305,7 +306,13 @@ func (g *Gateway) proxyOne(c *gin.Context, ch *store.Channel, plan *upstreamPlan
req.Header.Set("User-Agent", ua)
}
if plan.path == "/messages" {
// Anthropic 官方 API 只认 x-api-key(Bearer 是 OAuth token 的用法),
// 兼容网关两种都收;带上 anthropic-version 与客户端声明的 beta 头。
req.Header.Set("x-api-key", upKey)
req.Header.Set("anthropic-version", "2023-06-01")
if beta := c.GetHeader("anthropic-beta"); beta != "" {
req.Header.Set("anthropic-beta", beta)
}
}
for _, h := range []string{"OpenAI-Organization", "OpenAI-Project", "OpenAI-Beta"} {
if v := c.GetHeader(h); v != "" {
@@ -722,7 +729,9 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
float64(cacheRead)/1e6*model.CacheReadPrice +
float64(cacheCreate)/1e6*model.CacheWritePriceOr()
} else {
cost = float64(in+cacheRead+cacheCreate)/1e6*0.15 + float64(out)/1e6*0.60 // 无定价模型时按示例价
// 模型在记账时已被删除(请求进行中被下架):无法定价,按 0 记账留痕。
// 虚构单价扣用户的钱既不可审计也无法对账。
log.Printf("proxy: trace=%s model %q not found at billing time, cost recorded as 0", trace, mn)
}
// 落库的 input_tokens 存输入总量(含缓存读/写),与上游 message_start 口径一致,便于对账展示。
in += cacheRead + cacheCreate
@@ -793,6 +802,11 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
RawResponse: rawResp,
CreatedAt: time.Now().UTC(),
})
// 请求级一行日志:usage_logs 落库是异步的且可能丢,排障时日志是唯一即时依据
log.Printf("proxy: trace=%s user=%d key=%d model=%q channel=%d proto=%s in=%d out=%d cache_r=%d cache_w=%d cost=%.6f status=%s http=%d latency=%dms err=%s",
traceStr, uidVal, kidVal, mn, chID, p, in, out, cacheRead, cacheCreate, cost, status,
c.Writer.Status(), latency, errCode)
}
// recordError 失败请求的记账(不产生扣费,status=error)。