会话保持与供应商粘性
1. 当前行为
动态会话偏好优先保持同一供应商和上游部署,提高上游 prompt 缓存命中机会;新会话按“模型默认配置 → 当前模板的模型规则与路由组”选路。调用记录按会话归组。
会话粘性是动态路由的优先选择,不是永久绑定。原目标合法时优先保持,可重放的临时失败先在原部署完成配置次数的重试,再尝试其他部署和回退目标;冷却或配置变化使目标失效时重新选路。成功切换后优先复用新目标。会话 ID 不因供应商切换改变。
复用现有 affinity、Redis/内存缓存、路由模板、模型与端点目录和调用日志。粘性状态不新增数据库表,日志与费用沿用现有持久化链。
上游 prompt 缓存通常与供应商连接、上游型号和请求前缀有关。因此优先保留同一条部署,而不是只保持供应商品牌。粘性提高缓存复用机会,实际命中仍取决于上游缓存有效期、前缀、参数和服务实现;动态粘性缓存与上游 prompt 缓存分别管理。
四份设计使用同一组职责:
| 设计 | 负责的规则 |
|---|---|
| 模型与端点 | 部署身份、连接与执行能力,模型默认权重和回退 |
| 路由模板 | 当前身份使用的策略、分配、重试与冷却,以及组和回退覆盖 |
| 路由组与回退 | 候选池展开、组与成员的规则,以及失败后的目标顺序 |
| 会话保持与供应商粘性 | 会话识别、当前合法目标的优先复用、成功后更新偏好及日志归组 |
2. 会话识别
后端统一解析一次会话身份,供选路、响应续接和调用日志使用。解析顺序如下:
| 优先级 | 来源 | 行为 |
|---|---|---|
| 1 | 明确会话 ID | 仅标准 X-Session-Id、body session_id |
| 2 | previous_response_id |
按调用方、请求模型和端点查原响应;必须校验明确 ID 一致,最终继承父响应 ID 并记为 parent |
| 3 | prompt_cache_key |
作为原生缓存信号,哈希后生成 cache:v1:<hash> |
| 4 | prompt 指纹 | 提取稳定内容,生成 prompt:v1:<hash> |
| 5 | 无有效信号 | 生成新 UUID,并在响应中返回 |
多个明确来源同值则合并,不同值返回 400;信号须为非空字符串,最多 256 字节,无控制字符或首尾空白。即使已传明确 ID,也必须验证父响应;未知或无权访问的父响应按续接契约拒绝,不能降级为 prompt 猜测。会话 ID 不能替代历史读取权限。
session_source 表示最终会话身份的来源:explicit 为明确传入,parent 为父响应继承,cache_key 为缓存键信号哈希,prompt 为稳定内容指纹,generated 为随机 UUID。明确 ID 与父响应一致时仍记录 parent,因为本轮实际沿用父响应归属;来源不表示供应商或选路策略。
完成会话解析的数据面响应返回 X-Session-Id,CORS 暴露该响应头;鉴权、正文解析或会话冲突等提前拒绝不保证携带会话头。Playground 在首请求前生成 UUID,与历史一起持久化;刷新与工具往返复用,新建或清空对话更换 ID。
prompt 指纹
通过稳定内容识别会话,再查询它上次使用的目标:
stable prompt → session fingerprint → cached deployment → current route validation
- 使用开头的 system/developer 指令和首条用户消息;Anthropic 顶层
system、Responsesinstructions作为对应指令,字符串input作为用户输入。 - 按结构规范序列化并计算完整 SHA-256,保留角色、内容块类型与顺序;原始 prompt 不写入粘性缓存或诊断日志。
- 后续追加的用户/assistant 消息、工具调用、工具结果和 tools 定义不参与。模型与端点不参与会话指纹,单独作为选路范围。
- 必须存在有意义的用户内容,不能仅凭公共 system 或模型名合并请求。多模态保留可稳定表示的媒体标识或内容摘要;无法完整表示时生成新 ID,不能只取共用文本。
第一轮 Hello,下一轮携带完整历史 Hello → assistant reply → Explain more,两次指纹相同。仅发送增量 Explain more 且没有会话 ID/父响应,则无法关联前轮。开头指令变化、历史裁剪也可能改变推断结果。
相同开头的独立对话或复用的 cache key 可能被归到同组,这是内容推断的边界。请求日志保留 explicit / parent / cache_key / prompt / generated 来源,界面标明推断会话;准确区分新对话使用明确 ID。
3. 模型、端点与粘性范围
供应商连接定义地址和认证,部署定义公开模型、上游型号、连接引用与执行协议;端点决定本次请求可用的执行能力。实际选路目标是一条部署,不按供应商品牌或显示名称识别。
| 标识 | 用途 |
|---|---|
scope + session_id |
日志归组;scope 来自鉴权,不能由客户端指定 |
scope + session_id + requested_model_or_group + endpoint |
动态粘性缓存键,区分当前模型/路由组与入口 |
deployment_id |
缓存中的实际目标,使用现有稳定部署 ID |
scope + requested_model + endpoint + response_id |
响应归属查询范围,记录创建时的实际部署和 session,独立于动态偏好 |
同一模型组的不同成员、同品牌不同连接和相同型号的不同部署都分别保留真实身份。运行时冷却、并发和计量继续使用现有运行身份,不把包含地址或定价信息的拼接串作为新的会话标识。
切换模型或端点时,在对应合法候选中重新选路,复用该范围已有的粘性记录;没有记录则按“模型默认配置 → 当前模板的模型规则与路由组”选择。切回原模型/端点且缓存仍有效时,优先复用原目标。模型、端点或供应商变化不拆分明确会话的日志组。
4. 动态选路与故障切换
每次请求都读取当前生效的完整路由模板,按 key → team → organization 选第一份完整模板,无绑定时使用平台执行参数和模型默认,不另设会话专用故障切换开关。
执行流程:
- 鉴权、解析会话,并按现有链完成模型权限、护栏和额度检查。
- 按模型或组展开部署,校验端点、transport、凭据、启用状态、权重和冷却,形成合法候选。
- 动态会话偏好优先:有合法偏好时将对应部署排在第一位,即使其他部署更便宜或权重更高。新会话、偏好过期或失效时,以模型默认配置为基础,应用当前完整模板的精确模型规则与路由组,再按生效策略选择;首次并发请求可原子申请临时候选。
- 为保留供应商和缓存,可重放的通用失败先在当前部署按
retry_policy.max_attempts完成重试,耗尽后再尝试池内其他部署,池耗尽或无候选时进入通用回退;上下文超限和上游内容策略错误存在专用链时可直接进入对应回退。 - 成功后比较更新实际目标并续期;全部失败不提交成功偏好,也不改变 session。首次临时候选可能留到过期,下一请求仍须重新验证资格。
| 情况 | 路由行为 |
|---|---|
| 价格、延迟、负载或正权重改变 | 原目标仍合法时优先复用,其他候选按当前策略排序 |
| 部署停用、删除、traffic-split 权重归零、冷却或能力不匹配 | 跳过原目标,使用当前合法候选 |
| 429、5xx、可重放的网络错误 | 先在同一部署重试,次数耗尽后尝试其他部署,再进入通用回退 |
| 上下文超限 | 使用 context_window_fallbacks |
| 上游内容策略拒绝 | 使用 content_policy_fallbacks |
| 本地鉴权、预算、护栏拒绝或普通参数错误 | 返回现有错误,不借切换绕过检查 |
| 已向客户端输出流内容 | 按协议终止,不换目标拼接回答 |
| 父响应续接或结果不确定的有副作用请求 | 遵守原目标与重放约束,不进入普通跨目标回退 |
retry_policy.max_attempts 是每条部署的总尝试次数,包含首次调用;例如 3 表示首次加 2 次重试。它同时用于普通选路和会话偏好,不增加会话专用重试参数。一次请求中失败触发的被动冷却不截断当前部署已分配的重试次数,后续请求重新过滤冷却部署。关闭跨模型回退或没有配置回退链时,可安全重放的同步对话仍在同一部署重试。
池内候选切换与跨模型 fallback 分别沿用现有规则。三类回退省略或空表 [] 继承默认,某个源映射到 [] 禁用该源、该类别;组回退源及顺序见路由组与回退。disable_fallbacks: true 禁止跨模型回退和跨模型粘性复用,池内尝试仍受入口重试及重放安全约束。每个新目标发送前重新检查权限、端点能力、凭据和预算。
回退模型成功时,将实际部署记在本次原始请求模型/组及端点的粘性记录中。下一轮仅当该目标仍可由当前模板的对应回退链合法到达、且请求可重放时优先复用;链路删除立即使偏好失效;目标权限或预算拒绝则返回现有错误,不扩大调用范围。
traffic-split 权重决定没有合法偏好时的选择及其余候选顺序,不承诺粘性会话每轮或 token 的精确分流比例。精确模型规则优先于组策略;普通模型无显式分配时继承默认,显式空分配等权;组使用自身分配,不叠加成员默认权重。分配中未列出的合法部署权重为 1;成员请求继承组策略时仍只选择自身部署。
5. 缓存与并发
粘性复用现有 affinity 存储,有 Redis 时共享,无 Redis 时使用最多 8192 条记录的进程内缓存。记录保存 deployment_id、实际 model、pool、有序回退 path 和比较更新 revision,不保存供应商配置副本。申请临时候选时建立一小时 TTL,成功比较更新后续期;过期、缓存丢失或本机重启丢失内存记录后按正常路由重新选择,日志会话身份不受影响。
首次无记录时可用 Redis 原子申请一个临时偏好;并发请求读取赢家并验证资格后执行。成功时比较读取到的旧值再更新,避免迟到请求覆盖已经切换的新偏好;失败不无条件删除或覆盖其他请求的记录。进程内实现使用同样的比较规则。
原子操作只协调缓存偏好,不锁住整个会话或上游执行。并发故障切换可能短暂调用不同合法目标,后续请求收敛到当前偏好;这是动态粘性的契约,不承诺强绑定或请求串行。
Redis 异常时普通对话继续正常路由,可保留本机短期偏好,并记录降级原因;恢复后以共享缓存为准,不回灌本地旧值。多实例缓存不可用期间不保证目标一致。续接归属读取失败必须明确拒绝,不能按普通缓存 miss 随机选供应商。
6. 响应续接与日志
previous_response_id 使用响应创建时保存的实际部署和 session,优先级高于当前粘性缓存。原部署不可用或上下文过期时按现有续接契约报错,不能把游标转给其他供应商。响应成功后保存真实归属;失败或未完成流不发布成功上下文。普通粘性切换不修改已有响应的归属。
已识别会话的调用日志按 scope + session_id 归组,供应商切换保持同组。日志模型保留客户端请求的模型/组别名,实际部署、供应商和上游型号单独记录;沿用现有端点、状态、用量和费用字段,在现有 usage_events 增加 session_source 保存识别来源。粘性缓存不建表。重试和费用记录保持现有调用/尝试粒度;提前拒绝不保证已建立会话记录,缓存异常通过调试日志记录降级原因。
会话列表由后端按组分页,详情展示各轮实际目标和失败。费用沿用现有调用/尝试结算,聚合不重复计费。响应缓存命中标明未调用上游,不伪造执行目标或更新供应商成功偏好。没有会话标识的记录不按相似 prompt 推断归组。
新增页面文案使用项目中英文 i18n,示例提示词用英文,用户输入保持原文。
7. 集成与验收
| 模块 | 集成职责 |
|---|---|
internal/gateway/affinity |
统一会话解析、prompt 指纹、动态偏好与响应归属 |
internal/live |
共享 TTL 缓存及原子申请、比较更新,明确区分 miss 与存储错误 |
internal/router、internal/dataplane/routing |
合法候选中优先复用,统一应用模型、组、端点及回退规则 |
internal/dataplane/execution |
通过现有 RoutePlan 传递身份,执行重试/回退,成功后提交实际目标 |
| Playground、spend 查询与日志页面 | 持久化客户端 ID,按会话归组,展示实际目标与识别来源 |
| 测试层 | 验证要求 |
|---|---|
| 单元 | ID 优先级、冲突与输入校验;prompt 追加历史保持、工具变化、结构边界、增量/多模态限制;模型/端点缓存隔离;原子比较及过期行为 |
| 后台 regression | 真实网关与 Redis 下并发申请、同部署重试次数、故障切换和迟到更新;模板/权重/冷却变化即时生效;池内切换与三类 fallback;回退成功后复用及链路撤销;跨调用方隔离;缓存降级和续接拒绝;日志与费用正确 |
| 浏览器 E2E | Playground 多轮/工具往返 → 刷新 → 日志同组;清空后相同输入新组;无 ID 完整历史推断分组;模型/端点切换同组;路由配置 → 上游故障 → 合法回退 → 下一轮复用新目标;中英文状态和错误提示 |
| 协议回归 | Chat Completions、Responses、Anthropic Messages 的 JSON/SSE 身份与选路一致;流中断不拼接;父响应续接保持实际归属 |
自动化测试使用真实后台、隔离数据库/Redis 与本地受控上游,验证页面、接口、实际调用次数和数据;结束清理自建数据。供应商 prompt 缓存通过独立 live regression 测量,不能用网关响应缓存或耗时推断命中。
真实供应商的 prompt 缓存命中与有效期属于外部服务边界,需使用独立 live regression 验证。
8. 参考
统一 Responses 入口的可迁移历史
统一 /v1/responses 返回 X-XHub-History-Mode: portable。客户端提交完整历史时,
网关只清理本次请求副本中的 reasoning 密文、来源推理 ID 和供应商输出状态。
同步 JSON、流式 item 及终态快照均只发布可读推理,Chat 桥接遵守相同规则。
用户消息、助手正文、公开摘要和工具调用/结果保留原内容及 call_id,避免模型或供应商
切换因外部密文归属而失败,也避免向客户端历史新增其他账户的密文。
该模式会损失无法跨连接迁移的隐藏推理状态,不会恢复密文原文。原生上游完整输出仍保留在
原有的临时、调用方隔离历史中,供显式 previous_response_id 续接使用。跨用户、过期游标、
原连接变更仍然拒绝。原生 /bypass/openai/v1/responses 继续完整透传并执行严格密文归属校验。
网关不会改写客户端磁盘上的对话,也不能还原只剩不透明 compaction 数据的上下文。