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

@@ -1136,3 +1136,49 @@ func TestStreamResponsesToChatTextStillWorks(t *testing.T) {
t.Fatalf("missing finish chunk: %s", out)
}
}
// OpenAI 两协议同形的 opaque 字段(metadata / service_tier / parallel_tool_calls)
// 在 chat↔responses 互转时原样透传——上游可能是另一层中转,会校验/使用它们。
func TestChatToResponsesCarriesOpaque(t *testing.T) {
body := `{"model":"m1","messages":[{"role":"user","content":"hi"}],"service_tier":"priority","metadata":{"session_id":"s1"},"parallel_tool_calls":false}`
out, err := ConvertRequest([]byte(body), ProtoChat, ProtoResponses)
if err != nil {
t.Fatal(err)
}
var m map[string]any
if json.Unmarshal(out, &m) != nil {
t.Fatal("invalid json")
}
if m["service_tier"] != "priority" {
t.Fatalf("service_tier 丢失:%v", m["service_tier"])
}
md, _ := m["metadata"].(map[string]any)
if md == nil || md["session_id"] != "s1" {
t.Fatalf("metadata 丢失:%v", m["metadata"])
}
if m["parallel_tool_calls"] != false {
t.Fatalf("parallel_tool_calls 丢失:%v", m["parallel_tool_calls"])
}
}
func TestResponsesToChatCarriesOpaque(t *testing.T) {
body := `{"model":"m1","input":"hi","service_tier":"flex","metadata":{"installation_id":"i9"},"parallel_tool_calls":true}`
out, err := ConvertRequest([]byte(body), ProtoResponses, ProtoChat)
if err != nil {
t.Fatal(err)
}
var m map[string]any
if json.Unmarshal(out, &m) != nil {
t.Fatal("invalid json")
}
if m["service_tier"] != "flex" {
t.Fatalf("service_tier 丢失:%v", m["service_tier"])
}
md, _ := m["metadata"].(map[string]any)
if md == nil || md["installation_id"] != "i9" {
t.Fatalf("metadata 丢失:%v", m["metadata"])
}
if m["parallel_tool_calls"] != true {
t.Fatalf("parallel_tool_calls 丢失:%v", m["parallel_tool_calls"])
}
}
@@ -39,6 +39,12 @@ type chatReq struct {
MaxTokens *int `json:"max_tokens"`
Stop []string `json:"stop"`
Stream bool `json:"stream"`
// OpenAI 两协议同形的 opaque 字段:转换到 Responses 时原样透传。
// 上游可能是另一层中转,会校验/使用它们(service_tier 影响优先级,
// metadata 承载 Codex 等客户端的会话/安装标识)。
ServiceTier string `json:"service_tier"`
Metadata map[string]string `json:"metadata"`
ParallelToolCalls *bool `json:"parallel_tool_calls"`
}
// chatToMessagesReq 将 OpenAI Chat 请求转为 Anthropic Messages 请求。
@@ -27,6 +27,13 @@ func responsesToChatReq(body []byte) ([]byte, error) {
if v, ok := m["max_output_tokens"]; ok {
out["max_tokens"] = v
}
// OpenAI 两协议同形的 opaque 字段原样透传:metadata(map[string]string)、
// service_tier、parallel_tool_calls——丢了它们,中转上游的校验与路由会失效
for _, k := range []string{"metadata", "service_tier", "parallel_tool_calls"} {
if v := rawJSON(m, k); v != nil {
out[k] = json.RawMessage(v)
}
}
var msgs []any
if ins := str(rawJSON(m, "instructions")); ins != "" {
@@ -199,6 +206,16 @@ func chatToResponsesReq(body []byte) ([]byte, error) {
if req.MaxTokens != nil {
out["max_output_tokens"] = *req.MaxTokens
}
// OpenAI 两协议同形的 opaque 字段原样透传(见 chatReq 注释)
if req.ServiceTier != "" {
out["service_tier"] = req.ServiceTier
}
if len(req.Metadata) > 0 {
out["metadata"] = req.Metadata
}
if req.ParallelToolCalls != nil {
out["parallel_tool_calls"] = *req.ParallelToolCalls
}
var system []string
var input []any