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:
1 parent
e7f8a5e5ad
commit
04dfb4285d
14 files changed
+541
-38
No files matched your search
@@ -0,0 +1,84 @@
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 会话亲和(借鉴 magpie 的 cache-economics 思路,做服务端可用的简化版):
|
||||
// 同一会话粘住上次应答的渠道,让上游的提示词缓存(整段上下文每轮重发)被再次
|
||||
// 命中,而不是被加权随机打散后全价重算。
|
||||
//
|
||||
// 会话身份 = 密钥 + 模型 + 请求文本稳定前缀的摘要。编码代理每轮重发全量上下文,
|
||||
// 系统提示与最早的用户消息不变,取前缀即可区分会话;不需要解析各家客户端的
|
||||
// 会话头。命中亲和的渠道仍必须在候选集里(健康、未冷却、认识该模型)——
|
||||
// 冷却或下线后自然回退到加权随机。
|
||||
//
|
||||
// TTL 默认 5 分钟(对齐 Anthropic 提示词缓存窗口),每次成功应答刷新;
|
||||
// 上游不报缓存的(OpenAI 自动前缀缓存)同样受益:粘住就是省。
|
||||
|
||||
const (
|
||||
// affinityMax 亲和表上限:超过先清过期,仍超则不再新增(防御性,正常流量到不了)。
|
||||
affinityMax = 8192
|
||||
// affinityPrefix 会话身份摘要取请求文本的前缀字节数。
|
||||
affinityPrefix = 512
|
||||
)
|
||||
|
||||
type affinityTable struct {
|
||||
mu sync.Mutex
|
||||
m map[string]affinityEntry
|
||||
}
|
||||
|
||||
type affinityEntry struct {
|
||||
channelID uint64
|
||||
until time.Time
|
||||
}
|
||||
|
||||
func newAffinityTable() *affinityTable {
|
||||
return &affinityTable{m: map[string]affinityEntry{}}
|
||||
}
|
||||
|
||||
// get 返回该会话上次应答的渠道;过期条目顺手清理。
|
||||
func (t *affinityTable) get(key string) (uint64, bool) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
e, ok := t.m[key]
|
||||
if !ok {
|
||||
return 0, false
|
||||
}
|
||||
if time.Now().After(e.until) {
|
||||
delete(t.m, key)
|
||||
return 0, false
|
||||
}
|
||||
return e.channelID, true
|
||||
}
|
||||
|
||||
// put 记录(或刷新)会话→渠道的绑定。
|
||||
func (t *affinityTable) put(key string, channelID uint64, ttl time.Duration) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
if len(t.m) >= affinityMax {
|
||||
now := time.Now()
|
||||
for k, e := range t.m {
|
||||
if now.After(e.until) {
|
||||
delete(t.m, k)
|
||||
}
|
||||
}
|
||||
if len(t.m) >= affinityMax {
|
||||
return // 表满且全是活的:放弃本次记录,旧绑定继续服务
|
||||
}
|
||||
}
|
||||
t.m[key] = affinityEntry{channelID: channelID, until: time.Now().Add(ttl)}
|
||||
}
|
||||
|
||||
// affinityKey 会话身份:密钥 + 模型 + 请求文本稳定前缀的摘要。
|
||||
func affinityKey(keyID uint64, model, firstText string) string {
|
||||
if len(firstText) > affinityPrefix {
|
||||
firstText = firstText[:affinityPrefix]
|
||||
}
|
||||
h := sha256.Sum256([]byte(firstText))
|
||||
return fmt.Sprintf("%d|%s|%s", keyID, model, hex.EncodeToString(h[:8]))
|
||||
}
|
||||
Reference in new issue
Block a user