前段时间,我开了一个中转站。使用new-api部署的,放在了我的 macmini 上面。
正巧那段时间我有在用备受大家吹捧的 opencode go 套餐,当时这个 opencode go 简直就是第二家 deepseek。开一个这个,有用不完的 deepseek v4 flash。
差不多就是在涨价通知发下来那会儿吧,我搭建了一个中转站给我所在的两个团队免费使用,为了让大家更愉快的使用,我一口气买了5个 opencode 账号,聚合在了那个中转站上。
但是问题很快就出现了,中转站出现了两个严重的问题,一个是缓存命中率变得很低,一个是出口供应商的选择飘忽不定:可能你让ai完成一个工作,这个ai中途可能调用了一大堆账号的key,每个账号平均花费了一些token。虽然平均下来比单个账号可能看起来用量低,但实际上这个用量大了很多,这样也会导致同样的缓存在deepseek的服务器上缓存了很多份,浪费了缓存命中率。更致命的问题是,这个行为可能还会导致账号违反他们的规定导致封号。
强制会话亲和,防止请求漂移
如果你经常使用各类harness做开发的话,你肯定清楚 session 的概念。session字面意思,就是一个会话。我们在实际和ai供应商进行通信的时候,最常见的一种通信方式是把整个session的所有历史消息全部发送过去。因为api接口本身是不存在记忆的,即使到现在也不存在。
据我所知,如果模型供应商对我们的会话在模型层面进行亲和的话,可以复用一些缓存,这样就有了缓存命中率的概念。但是命中缓存的前提是,你需要让供应商知道你这个新发来的请求,它之前处理过。这样才可以命中那个缓存(把这次新请求和之前做的缓存绑定起来就行了)。所以缓存命中的关键就是让模型供应商能够区分你发来的这些请求属于一个会话。
9月6日,opencode给我发了一封邮件,我估计很多使用opencode的开发者也收到了,具体内容如下:
Hey there,
Some of your requests to OpenCode Go are missing an x-opencode-session header. If we don't have this we cannot properly optimize our service. Starting 09/06 requests missing this header may error.
Here are your useragents that are missing this header:
Bun fetch
Add headers: {'x-opencode-session': '<stable-id-per-conversation>'} to your fetch calls.
ai/6.0.185 ai-sdk/provider-utils/4.0.40 runtime/node.js/24
We don't recognize this client — add x-opencode-session (one stable ID per conversation) or ask its maintainer to.
Thank you.
大概的意思就是我们需要在我们的请求里面携带请求头 x-opencode-session。
其实这里很容易就能理解opencode的意图,它想提高缓存命中率,降低运维成本。携带一个session标记,这个会话里面的所有请求全部携带这个标记,那么这些请求都可以复用模型供应商服务器里面的一些缓存资源,达到降低成本的效果。至于opencode为什么这么强硬,我也不太清楚。
所以,这就是会话亲和。
之前的封号
大概8月20多,我在全国最大黑市(闲鱼)买了5个opencode账号,然后搭建了 new-api 中转站使用这五个账号。但是只用了一天,5个账号有3个账号被封禁了。这让我有点害怕,但是我也没有立刻去整改,而是继续用剩下的两个账号,也挺奇怪的,剩下的两个账号依然使用之前的聚合稳定用到现在了。
暂且不管导致封号的具体原因是什么。总之这种多账号聚合的方案在很多渠道供应商是禁止的,opencode的用户协议里明确写了。不过嘛,为了方便自己的使用,聚合几个账号也不是什么严重的事情,只要尽量做到看起来没什么大问题就行。
综合来看,实现一个良好的会话亲和策略就很重要了。
new-api会话亲和与亲和网关
首先我查找new-api,看看它自己有没有自带会话亲和的功能。实际上确实是有的,不过我感觉功能上有一点不太好操作,这个会话亲和主要的设计是根据发来的请求的请求头单一的匹配一个渠道,后续的所有请求都走这个渠道,中间不会更改渠道。同时也设有redis缓存过期时间,并不会长期使用一个渠道不切换。
这个设计确实不错,但是它自由度并没有特别高,因为我实测发现,有的harness是不发送一些常规请求头的,比如x-session-id,看起来这个请求头非常正经,理应现在所有的ai调用的请求都应该携带。但是实际并不是,omp就没有携带这个session-id,甚至好像大多数常规的标记都没有(如果我没记错的话)。
更重要的是对于这次 opencode 发送邮件提示携带x-opencode-session的要求。new-api更是无能为力了。
所以我基于caddy开发了一个会话亲和网关,可以自由设置入站的亲和机制。目前我为了简单,只支持了硬亲和(匹配常规请求头,发现没有的直接拒绝响应)。他确实实现了良好的会话亲和。
网关的逻辑是根据入站请求的多样化标记,特定的标记一个又一个会话,然后给他们生成一个内部标记的session-affinity-id。这个id只在网关和网关下游的模型中转站使用。与此同时,new-api的会话亲和部分只需要识别session-affinity-id就可以实现非常良好的会话亲和了。至于harness的适配,出站的逻辑,都交给了亲和网关。
至于出站逻辑,我使用lua语言做插件,插件的功能就是根据实际的渠道供应商的要求,改写我们的出站请求,比如opencode,既然opencode强制要求携带x-opencode-session,那么我们就编写一个特定的插件,只针对opencode.ai这个渠道供应商。插件会把session-affinity-id去掉,然后按照特定的规则生成一个与这个会话关联的固定的一个x-opencode-session改写请求头,发送出去,这样就符合了opencode的需求。
同时也能做一些高级操作,比如伪造请求,看起来是从opencode官方harness发送出来的一样。
这个项目我用了快一周了,效果不错。未来我计划加入软亲和机制,就是提供一个不主动拒绝的开关。网关可以通过请求里面的具体信息实行亲和,比如历史对话记录什么的。这样哪怕发送请求的是什么东西也没有携带的完全没有任何标记的请求,我们也能把它本身当成一个标记,特异性的把它以及它后面的对话作为一个session来亲和。
大概就是这样。我会持续优化。