feat: 会话亲和粘住缓存渠道;透传 opaque 字段;用量记思考/1h 缓存写/实际模型

会话亲和(proxy/affinity.go):同一会话粘住上次应答的渠道,命中上游提示词
缓存而非被加权随机打散后全价重算。会话身份 = 密钥 + 模型 + 请求文本稳定前缀
摘要,不依赖各客户端会话头;TTL 默认 5m(OT_PROXY_AFFINITY_TTL,0=关闭),
成功应答刷新。亲和渠道必须在候选集内(健康/未冷却/绑定),否则回退加权随机。
proxyOne 增加 served 返回值区分「应答成功」与「已终结但失败」。

opaque 透传(convert):chat↔responses 互转原样携带 metadata、service_tier、
parallel_tool_calls。此前逐字段重建请求体会丢它们,而中转上游会校验/使用。

用量细化(proxy+store):usage_logs 新增 reasoning_tokens(展示口径,不重复
计费)、cache_write_1h_tokens(Anthropic 1h 缓存写按 2× 输入价,此前统一
1.25× 少算 60%)、served_model(上游实际应答模型,映射/聚合商场景对账)。
schema v6→v7。

验证:go vet ./... 干净;go test ./... 与 go test -race ./... 全绿;
新增测试覆盖亲和粘性与冷却回退、opaque 双向透传、reasoning 不重复计费、
1h/5m 缓存写分开计价、served_model 提取(非流式与流式)。
This commit is contained in:
Sakurasan committed 2026-10-30 11:41:51 +08:00
1 parent e7f8a5e5ad
commit 04dfb4285d
14 files changed
+541 -38

No files matched your search

+3
View File
@@ -82,6 +82,7 @@ type ProxyConfig struct {
FailoverBudget time.Duration // 故障转移给"第二个及以后的渠道"留的时间预算,0=不限
StreamHeartbeat time.Duration // 流式空闲多久发一次 SSE 注释心跳,0=关闭
FirstTokenWait time.Duration // 流式放行前等内容的最长时间,0=不限;超时未出内容换下一个渠道
AffinityTTL time.Duration // 会话亲和 TTL,0=关闭;同一会话粘住上次应答的渠道以命中上游缓存
HealthInterval time.Duration // 渠道健康检查周期
HealthFailThreshold int // 连续失败 N 次进 cooldown
LogRaw bool // 记录管理员原始请求体+响应到 usage_logs(调试用,默认关)
@@ -150,6 +151,7 @@ func Load() (*Config, error) {
v.SetDefault("proxy.failover_budget", "30s")
v.SetDefault("proxy.stream_heartbeat", "15s")
v.SetDefault("proxy.first_token_wait", "0s")
v.SetDefault("proxy.affinity_ttl", "5m")
v.SetDefault("proxy.health_interval", "60s")
v.SetDefault("proxy.health_fail_threshold", 2)
v.SetDefault("proxy.log_raw", false)
@@ -200,6 +202,7 @@ func Load() (*Config, error) {
FailoverBudget: v.GetDuration("proxy.failover_budget"),
StreamHeartbeat: v.GetDuration("proxy.stream_heartbeat"),
FirstTokenWait: v.GetDuration("proxy.first_token_wait"),
AffinityTTL: v.GetDuration("proxy.affinity_ttl"),
HealthInterval: v.GetDuration("proxy.health_interval"),
HealthFailThreshold: v.GetInt("proxy.health_fail_threshold"),
LogRaw: v.GetBool("proxy.log_raw"),