Repository navigation
feat: 渠道配置没法表达「这几条渠道共用上游缓存」(暂缓) #22
Description
状态
暂缓。这是个真缺口(下面有代码与上游两边的证据),但它值不值取决于部署形态,
粒度也还没有真实部署来定。决定与理由在本文件最后两节,先记下来免得忘。
现象
亲和的单位是单条渠道。server/affinity.go 记的是「这个会话上一轮由哪条渠道服务」,
命中之后 orderByChannel 把那条渠道排到候选最前,池内其余候选保持配置顺序
(这个函数的注释就是这么写的:「把某个渠道的候选排到最前,池内其余候选保持配置顺序」)。
于是「换一条渠道」被当成「缓存必丢」。而上游的提示缓存不是按 key 隔离的。
Anthropic 官方文档(Prompt caching)原话:
Prompt caching uses workspace-level isolation. Caches are isolated per workspace,
ensuring data separation between workspaces within the same organization. This
applies to the Claude API, Claude Platform on AWS, and Microsoft Foundry; Bedrock
and Google Cloud maintain organization-level cache isolation.
即:缓存按 workspace 隔开,同一个组织里不同 workspace 互不可见;Claude API、
Claude Platform on AWS 与 Microsoft Foundry 都是这个粒度,Bedrock 与 Google Cloud
只有组织级隔离。
按这段:同一个 workspace 里的两把 key 用的是同一份缓存;Bedrock 与 Google Cloud
更粗,整个组织共用;不同组织之间永不共享。
后果两条:
- 该换的时候不换。 同一个 workspace 里的第二条 key 本来能顶上(缓存还在),
但亲和只认记下来的那一条,把它排在最前;它不可用时退到的是配置顺序里的下一条,
而不是「同一缓存域的兄弟」。 - 不该钉的时候硬钉。 同一个域内的渠道在缓存上等价,现在没有地方能表达这件事,
只能靠配置顺序偶然对上。
仓库里现在没有这个概念,RelayKit 里也没有(搜过)。
期望
- 配置能表达「这几条渠道共用同一份上游缓存」。
- 亲和排序用上它:记下来的那条优先,其次是同域的其他渠道,最后才是别的域。
- 不写时行为与 v0.5.0 相同(每条渠道自成一个域)。
形状(已倾向)
config.Channel加一个域标签字段,同名视为同一域。不写时每条渠道自成一个域,
与 v0.5.0 行为逐字相同 —— 纯增量,标了才起作用,没标过的部署升级后什么都不变。- 只改排序,不改别的:记下来的那条 → 同域的兄弟 → 别的域。
- 粒度按渠道(现在一条渠道一把凭据)。
已经能答的几条口径
- 同域内按配置顺序排;不要把域塞进哈希。 冷启动时哈希的职责是摊负载,
而同域内缓存本来就共用,摊开是免费的;按域去哈希会让同域的渠道打成平手、
全落到配置里第一条上,把负载重新集中起来,正好把池子废掉。 - 不动「前缀短到进不了上游缓存就不做亲和」那道闸(
server/affinity.go里
minimum > 0 && affinity.PrefixBytes(entries) < minimum那句)。那条闸的判据是
「钉住只剩负载集中的坏处」,与域无关:前缀进不了缓存时,钉住仍然是纯害。 - 不让它进
Outcome。 域标签是宿主自己写进配置的,它本来就知道,
不需要内核再回报一次。 - 粒度按渠道,但留一句:若将来要支持「一条渠道多把 key」(上游 new-api 就是这么做的,
见下),标签要挪到 key 上;标在渠道上就错了。现在只写进注释与文档。
验收条件
- 写了域标签时,亲和给出的候选顺序是:记下来的那条 → 同域的其他渠道 → 别的域。
- 不写时排序与 v0.5.0 逐条相同(有测试对照)。
- 标注错了不影响转发的可用性:某条渠道失败时的重试路径照旧。
make check全绿。
上游 new-api 的情况(查证过)
它有渠道亲和,而且配置面比 core 现在这套大得多;但它没有缓存域这一层。
cache_scope、CacheScope、共用缓存、缓存隔离 四个词在它整个仓库零命中。
| 项 | 内容 |
|---|---|
| 代码 | service/channel_affinity.go、setting/operation_setting/channel_affinity_setting.go、一套管理界面与 API |
| 规则匹配 | model_regex、path_regex、user_agent_include |
| 会话键来源 | key_sources 四种类型:context_int / context_string / request_header / gjson,可配多条、按顺序取第一个非空 |
| 每条规则 | value_regex、ttl_seconds、session_mode(off / prefer / strict / inherit)、skip_retry_on_failure、include_using_group、include_model_name、include_rule_name、param_override_template |
| 全局 | enabled、switch_on_success、keep_on_channel_disabled、max_entries、default_ttl_seconds |
| 内置两条规则 | codex cli trace(取 gjson: prompt_cache_key)、claude cli trace(取 gjson: metadata.user_id) |
| 缓存载体 | cachex.HybridCache[int]:内存 LRU + TTL + janitor,可选 Redis 共享(多实例) |
| 管理 | GET / DELETE /api/option/channel_affinity_cache,带审计条目 |
两条值得记住的:
- 它的单位也是单条渠道。
getChannelAffinityCache() *cachex.HybridCache[int],
缓存里存的值就是渠道 ID;命中只返回这一个 ID。它和 core 一样把「换渠道」当成
「缓存必丢」。 - 它的多密钥轮换把同一个问题放大了一层。 一条渠道里可以放多把 key,
MultiKeyModeRandom(随机)或MultiKeyModePolling(轮询)挑一把;而亲和钉的是
渠道 ID、不是 key。所以钉住渠道之后渠道内部的 key 还在轮换,那几把 key 若分属不同
workspace,缓存照样丢,它所有的亲和设置都挡不住。
它顺手做了「用命中率验证亲和有没有用」那件事:usageCacheSignals(usage) 返回
(hit, cachedTokens, promptCacheHitTokens),按规则名、分组、键指纹统计。
价值上限(为什么暂缓)
- 只有「同一个缓存域里配了多条渠道」才吃得到;省下的只是「记下来的那条不可用时」
那部分命中率。 - 上游的多密钥轮换说明,真实部署里「一条渠道装多把 key」比「多条渠道装同一账号的
key」更常见。真要是这样,这个字段的粒度一开始就不对。 - 所以粒度等真实部署来定,比现在按猜测把公开面钉死强。
相关零件
- 亲和的三个部分:
internal/affinity的累积哈希链、server/affinity.go的
orderByChannel与orderByHash、进程内记忆表(FIFO 淘汰)。 - 会话标识:请求头优先,OpenAI 的
prompt_cache_key兜底
(server/affinity.go的explicitSessionKey)。 - 命中率的分子与分母都有了:
server.Usage.PromptTokens与CachedTokens。
后者在 v0.5.0 之前跨方言路径上恒为 0(issue Usage.CachedTokens 在跨方言路径上恒为 0,且没有缓存写字段 #18),现在交得对,
所以域标签有没有用可以直接看命中率,不用凭感觉。
没查的
- new-api 自己的
docs/里有没有篇章讲亲和的用途(只做了关键词检索,没通读)。 - 它多密钥轮换时是否在别处考虑了缓存(没找到任何迹象,也没穷尽整条 key 选择路径)。
- 它是否在 issue 或 PR 里讨论过缓存域(没翻它的 issue 列表)。
环境
core v0.5.0(dd1c7ab)。函数名与行号取自这一版。
Activity
tokenmp commentedon Oct 10, 2026
一条影响粒度前提的补充(查证)
Messages API 参考页的 Headers 节里有这么一条:
也就是:同一把凭据可以作用于多个 workspace,选哪个是每请求用头指定的。
这对本 issue 有两处影响:
而不是「渠道」。上面「形状(已倾向)」里的第 3 条(粒度按渠道)要打个问号。
中继可以通过设这个头把请求指到同一个 workspace,就不需要靠标签来表达域了。
所以先确认真实部署里是「一个账号多把 key」还是「一把 key 多个 workspace」,
再决定要不要这个字段。
这条头该由中继或宿主决定,不从客户端放行 —— 让客户端选上游 workspace,等于让
客户端选计费归属。这一句同时记在 issue #23(客户端协议头放行名单)里。