doc:更新文档

This commit is contained in:
chengqian committed 2026-09-24 10:21:31 +08:00
1 parent d9630d5b99
commit 4ce2fdf55e
6 files changed
+62 -579

No files matched your search

+53 -80
View File
@@ -28,77 +28,50 @@ team-api/
│ ├── docs/v1/ # OpenAPI 文档 API
│ ├── open/v1/ # 开放平台 API
│ ├── payment/v1/ # 支付回调 API
│ └── settings/v1/ # 公开设置 API(IsPublic 设置项、公开公告,无需认证)
├── hack/
│ └── config.yaml # gf CLI 脚手架配置(gen dao 等工具的数据库连接和生成规则)
├── internal/
│ ├── cmd/ # 入口命令、服务启动、路由注册
│ │ ├── cmd.go # main 包注册,路由分组定义
│ │ └── cmd_reset_pwd.go # 密码重置命令
│ ├── consts/ # 常量定义(业务状态码、枚举等)
│ ├── controller/ # 控制器(由 gf gen ctrl 从 api/ 自动生成,禁止手动修改)
│ │ ├── admin/ # 管理后台控制器
│ │ ├── tenant/ # 租户控制台控制器
│ │ ├── captcha/ # 验证码控制器
│ │ ├── docs/ # 文档控制器
│ │ ├── open/ # 开放平台控制器
│ ├── handler/ # 特殊端点处理器(不走 Controller 链路,见下方说明)
│ │ ├── relay/ # AI 代理端点处理器
│ │ ├── public/ # 支付回调端点处理器
│ │ └── setup/ # 系统初始化向导处理器
│ ├── logic/ # 业务逻辑实现(核心代码所在)
│ │ ├── admin/ # 管理后台业务逻辑
│ │ ├── tenant/ # 租户控制台业务逻辑
│ │ ├── billing/ # 计费引擎
│ │ ├── common/ # 公共逻辑(缓存、配置、安全、邮件、验证码、JWT 等)
│ │ ├── docs/ # OpenAPI 文档生成
│ │ ├── monitor/ # 监控告警引擎
│ │ ├── notification/ # 通知服务
│ │ ├── open/ # 开放平台业务逻辑
│ │ ├── payment/ # 支付逻辑(回调处理、订单履约)
│ │ ├── relay/ # Relay 业务逻辑(亲和性、健康检查、缓存)
│ │ └── task/ # 异步任务框架
│ ├── model/ # 数据模型(由 gf gen dao 自动生成,禁止手动修改)
│ │ ├── do/ # Domain Objects(查询条件、输入结构体)
│ │ └── entity/ # Entity(与数据库表 1:1 映射的数据实体)
│ ├── packed/ # 打包资源(内嵌静态文件、编译时生成)
│ ├── dao/ # 数据访问对象(由 gf gen dao 自动生成,禁止手动修改)
│ ├── dispatchadapter/ # 渠道调度适配层(桥接 relaykit/dispatch 与宿主:渠道目录、调度配置、Redis/本地健康状态)
│ ├── handler/ # 特殊端点处理器(不走 Controller 链路,见下方说明)
│ ├── logic/ # 业务逻辑实现(核心代码所在)
│ ├── middleware/ # 中间件
│ ├── model/ # 数据模型(由 gf gen dao 自动生成,禁止手动修改)
│ ├── packed/ # 打包资源(内嵌静态文件、编译时生成)
│ ├── plugin/ # 插件框架(接口定义、注册表、生命周期、钩子、引导)
│ ├── response/ # 统一响应工具包(Success/Error 封装)
│ ├── service/ # 业务接口定义(由 gf gen service 从 logic/ 自动生成,禁止手动修改)
│ ├── middleware/ # 中间件
│ ├── testutil/ # 测试工具(GoFrame 测试服务器封装)
│ └── utility/ # 工具函数
│ ├── crypto/ # 加密工具
│ ├── export/ # 数据导出
│ ├── totp/ # TOTP 两步验证
│ └── turnstile/ # Cloudflare Turnstile 验证
├── relay/ # AI 模型代理层(顶级模块,独立于 GoFrame 脚手架生成目录)
│ ├── channel/ # 供应商适配器(registry.go + 24 个子目录,每个供应商一个)
│ │ └── openai/ claude/ gemini/ ...
│ ├── channel/ # 供应商适配器(registry.go + 23 个子目录,每个供应商一个)
│ │ └── openai/ claude/ gemini/ vertex/ codex/ ali/ ...
│ ├── common/ # Relay 层共享类型和工具
│ ├── constant/ # Relay 层常量(供应商类型、模式、错误码)
│ ├── dto/ # 数据传输对象(OpenAI/Claude/Gemini/Realtime/Task 等格式定义)
│ ├── handler/ # Relay 请求处理器(chat/claude/gemini/audio/rerank/task/realtime 等)
│ ├── helper/ # Relay 辅助函数(流式处理、状态码映射、系统提示词、thinking 处理)
│ ├── dto/ # 兼容层 DTO(主体已迁 relaykit/dto,余 Responses DTO 与 relaykit 类型桥接)
│ ├── handler/ # Relay 请求处理器(chat/claude/gemini/audio/rerank/task/realtime/videos/responses/passthrough 等)
│ ├── helper/ # Relay 辅助函数(错误信息、状态码映射、模型名处理、供应商原生格式判定、脱敏)
│ ├── override/ # 请求/响应覆盖(Header 改写、参数覆盖)
│ ├── scheduler/ # 渠道调度器(调度、亲和性、重试)
│ └── taskchannel/ # 异步任务渠道适配器
│ ├── registry.go # 任务渠道注册表
│ ├── kling/ # 可灵视频生成
│ ├── midjourney/ # Midjourney 图像生成
│ ├── sora/ # Sora 视频生成
│ └── suno/ # Suno 音乐生成
├── manifest/
│ ├── config/ # 应用运行时配置(GoFrame 标准路径)
│ ├── deploy/ # 部署配置(K8s/Compose 等)
│ ├── docker/ # Docker 构建配置
│ ├── i18n/ # 国际化资源文件
│ └── protobuf/ # Protobuf 定义文件
│ ├── relaykit_bridge/ # relaykit 协议转换桥(格式路由分发、响应/流式回接)
│ └── taskchannel/ # 异步任务渠道适配器(registry.go + helpers.go + 8 个子目录)
│ └── kling/ midjourney/ sora/ suno/ ali/ gemini/ minimax/ volcengine/
├── relaykit/ # 协议转换 + 渠道调度(独立 Go module,不依赖 GoFrame)
├── plugins/ # 插件实现目录(registry.go 汇总注册,当前含 example 示例插件)
├── scripts/ # 辅助脚本(wrk 压测、Turnstile 测试、渠道测试缓存清理)
├── tests/
│ └── integration/ # 集成测试(admin/ tenant/,起真实服务做端到端断言)
├── manifest/ # GoFrame相关配置
├── migrations/ # goose 数据库迁移脚本(六位序号编号:000001_xxx.sql)
├── web/ # 前端
│ ├── admin/ # 管理后台
│ └── tenant/ # 租户控制台
│ ├── tenant/ # 租户控制台
│ ├── embed.go # 前端打包产物内嵌(embedweb 构建标签)
│ └── embed_stub.go # 非 embedweb 构建的空实现
├── docs/ # 项目文档
│ ├── 开发计划-v2/ # 分周期开发计划(周期一~六)
│ └── 协议文档/ # 协议相关文档
├── main.go # 应用入口
├── go.mod
└── Makefile
@@ -107,11 +80,17 @@ team-api/
### Relay 层设计说明
Relay 层作为顶级 `relay/` 模块独立于 GoFrame 脚手架生成目录(`internal/` 下的 `controller/service/dao/model`),原因:
- **文件量大**:24 个供应商适配器 + 4 个异步任务适配器 + 共享模块,放在 `internal/logic/` 下层级过深
- **文件量大**:23 个供应商适配器 + 8 个异步任务适配器 + 共享模块,放在 `internal/logic/` 下层级过深
- **职责独立**:Relay 是纯粹的代理转发层,不依赖 GoFrame ORM/DAO,与 `internal/` 下的业务逻辑解耦
- **参考对齐**:目录结构与 new-api 的 `relay/` 保持一致,降低移植和理解成本
`internal/logic/` 中的业务逻辑(如计费、渠道调度)通过 Go import 调用 `relay/` 包,而非将 relay 嵌入 logic 内部。
`internal/logic/` 中的业务逻辑(如计费)通过 Go import 调用 `relay/` 包,而非将 relay 嵌入 logic 内部。
### relaykit 独立模块(协议转换 + 渠道调度)
`relaykit/` 是自带 go.mod 的独立 Go module(`github.com/qianfree/team-api/relaykit`,不依赖 GoFrame),承载两块核心能力,`relay/` 下已不再有 scheduler:
- **协议转换**(`relayconvert/`):12 方向协议互转的注册表 + 引擎,`relay/handler` 经 `relay/relaykit_bridge` 调用;原 `relay/dto` 主体已迁入 `relaykit/dto`
- **渠道调度**(`dispatch/`):渠道选择(HRW 一致性哈希)、错误分类、熔断、健康分、会话亲和;`internal/dispatchadapter` 负责桥接宿主的渠道目录/调度配置/Redis 健康状态
### GoFrame 代码生成与手动编写分界
@@ -133,6 +112,10 @@ Relay 层作为顶级 `relay/` 模块独立于 GoFrame 脚手架生成目录(`
| `internal/utility/` | 手动编写 | 是(工具函数:加密、导出、TOTP、Turnstile) |
| `manifest/config/` | 手动编写 | 是(运行时配置) |
| `relay/` | 手动编写 | 是(AI 模型代理层,独立于 GoFrame 脚手架) |
| `relaykit/` | 手动编写 | 是(独立 Go module:协议转换 + 渠道调度) |
| `internal/dispatchadapter/` | 手动编写 | 是(渠道调度适配层,桥接 relaykit/dispatch) |
| `internal/plugin/` `plugins/` | 手动编写 | 是(插件框架与插件实现) |
| `internal/testutil/` `scripts/` `tests/` | 手动编写 | 是(测试与辅助脚本) |
### internal/handler/ 目录说明
@@ -143,6 +126,8 @@ Relay 层作为顶级 `relay/` 模块独立于 GoFrame 脚手架生成目录(`
| `handler/relay/` | `/v1/*` AI 代理端点(OpenAI/Claude/Gemini 兼容格式)- 需要直接操作 `http.ResponseWriter` 进行流式转发 |
| `handler/public/` | `/api/payment/*` 支付回调端点 - 需要返回非 JSON 格式的响应(如支付宝的表单重定向) |
| `handler/setup/` | `/api/setup/*` 系统初始化向导端点 - 首次部署时的系统配置页面和初始化接口 |
| `handler/admin/` | `/api/admin/files/*` 管理后台文件服务 - 应用层代理下载(local 存储无预签名 URL 时),直写二进制流 |
| `handler/landing/` | `/` 后端根路径落地页 - 非 embedweb 构建时返回项目介绍页,引导进入两套控制台 |
**为什么不用 Controller**:
- **流式响应**:AI 代理端点需要实时转发上游的 SSE 流,Controller 的响应包装会破坏流式传输
@@ -194,28 +179,18 @@ cmd(路由注册)
← controller 格式化响应(Res 结构体)
```
## 参考项目(只读,禁止修改)
## 参考项目(本地可选,当前未放置)
项目根目录下有 `new-api/` 和 `sub2api/` 两个参考项目,它们是大模型网关领域的成熟实现。
大多数情况下不需要查看这2个目录下的代码,除非我主动提及或者涉及到大模型相关的实现逻辑。
`new-api/` 和 `sub2api/` 是大模型网关领域的成熟实现,曾作为只读参考放在项目根目录(gitignore 未跟踪,不随仓库分发),**当前本地已移除**;需要参考时自行克隆回来即可。三项目渠道调度的横向对比结论沉淀在 `docs/reference/多项目渠道调度对比.md`(注意:其 team-api 侧描述为调度重写前的旧实现,现行调度以 `relaykit/dispatch` 为准)。
### new-api 重点参考
### 参考要点
- **Relay 代理层架构**:`controller/relay/`,适配器模式、请求转发、响应解析
- **供应商适配器**:`relay/channel/` 下 40+ 供应商的适配器实现,优先复用
- **协议转换**:OpenAI ↔ Claude ↔ Gemini 消息格式互转
- **渠道调度**:优先级/权重选择、自动重试、Key 轮询
- **计费模型**:预扣/结算/退款、梯度定价、Token 计量
### sub2api 重点参考
- **渠道亲和性**:用户+模型→渠道映射缓存策略
- **运维监控**:仪表盘设计、健康监控页面
- **用户体系**:前端布局和组件设计
- **new-api**:Relay 代理层架构(适配器模式、请求转发、响应解析)、40+ 供应商适配器、协议转换、渠道调度(优先级/权重选择、自动重试、Key 轮询)、计费模型(预扣/结算/退款、梯度定价、Token 计量)
- **sub2api**:渠道亲和性(用户+模型→渠道映射缓存策略)、运维监控(仪表盘设计、健康监控页面)、用户体系(前端布局和组件设计)
### 参考原则
参考不等于照搬。new-api 使用 Gin + GORM,我们需要适配到 GoFrame 框架。接口设计和核心算法可以参考,但代码需要重新组织以符合 GoFrame 标准结构(`api → controller → service → logic → dao` 分层)和项目的多租户架构。注意 GoFrame 的 `model/do/`、`model/entity/`、`dao/`、`service/`、`controller/` 均由脚手架自动生成,业务代码只写在 `api/`、`logic/`、`cmd/`、`handler/`、`middleware/`、`response/`、`utility/`、`consts/` 和顶级 `relay/` 中。
参考不等于照搬。new-api 使用 Gin + GORM,需适配到 GoFrame 框架,代码重新组织以符合 GoFrame 标准结构(`api → controller → service → logic → dao` 分层)和项目的多租户架构。注意 GoFrame 的 `model/do/`、`model/entity/`、`dao/`、`service/`、`controller/` 均由脚手架自动生成,业务代码只写在 `api/`、`logic/`、`cmd/`、`handler/`、`middleware/`、`response/`、`utility/`、`consts/`、顶级 `relay/` 和独立模块 `relaykit/` 中;协议转换与渠道调度已沉淀为自有实现,优先复用 `relaykit/`。
## 核心架构决策
@@ -267,7 +242,7 @@ cmd(路由注册)
### 钱包状态:Redis 权威 + DB 物化(强约束)
钱包资金状态采用 **Redis 唯一实时权威 + DB 滞后物化视图** 架构(详见 [`docs/钱包Redis权威化设计.md`](docs/钱包Redis权威化设计.md)):
钱包资金状态采用 **Redis 唯一实时权威 + DB 滞后物化视图** 架构:
- **Redis 权威**:`wallet:v2:{tenant_id}` hash(balance / frozen_balance / total_consumed / ver,整数 micro-USD)是余额的唯一实时真相。所有资金变动(预扣冻结、结算扣款、解冻、充值/退款/调账)一律通过 `internal/logic/billing/wallet.go` 的 Lua 脚本原子完成,**任何 Go 代码不得直接读改写 Redis 钱包 hash 或从 DB 反向重建它**。
- **累计消费(total_consumed)只随结算事件变动**:`settleClaimLua` 与扣款同一原子 `HINCRBY` 递增;结算记流水失败的补偿逆转走专用 `ReverseConsumeRedis`(余额与计数同一原子回退)。充值/兑换/调账/退款/预扣/解冻一律**不得**触碰该计数(`CreditWalletRedis`/`DebitWalletRedis` 不感知计数)。字段基线由三层补种保证(boot 种子 `SeedWalletTotalConsumed` / 灾备重建 `rebuildWalletFromDB` / 物化器自愈 `materializeOneWallet`),存量历史从 `bil_transactions(type=consume)` 回填(幂等可重放)。
@@ -374,7 +349,7 @@ cmd(路由注册)
### Go 后端
- **缩进统一使用 tab**:所有 Go 代码的缩进必须使用 tab,与 `go fmt` 自动格式化保持一致,禁止混用空格和 tab 缩进
- **金额用 `decimal.Decimal`,不用 `float64`(强约束)**:所有资金列(`NUMERIC(20,10)`)在 Go 层映射为 `github.com/shopspring/decimal` 的 `decimal.Decimal`(gf 全局 `typeMapping: numeric → decimal.Decimal`)。**多步金额运算(加/减/乘/除、循环累加、倍率×折扣链)一律用 decimal**,禁止用 float64 做中间计算——IEEE double 会在链式运算中累积漂移。工具函数在 `internal/logic/billing/money.go`(`AddMoney`/`SubtractMoney`/`MultiplyMoney`/`DivideMoney`/`RoundMoney`/`Max`/`Min`/`IsPositive` 等),比较用 `.GreaterThan(billing.Zero)`/`.IsZero()`/`.Equal()`,不要用 `== 0`。判断非金额小数(评分/利用率/延迟/告警阈值)保持 `float64`——判据见 [`docs/float64-decimal-field-classification.md`](docs/float64-decimal-field-classification.md)(「这个数最终会不会变成钱」)。
- **金额用 `decimal.Decimal`,不用 `float64`(强约束)**:所有资金列(`NUMERIC(20,10)`)在 Go 层映射为 `github.com/shopspring/decimal` 的 `decimal.Decimal`(gf 全局 `typeMapping: numeric → decimal.Decimal`)。**多步金额运算(加/减/乘/除、循环累加、倍率×折扣链)一律用 decimal**,禁止用 float64 做中间计算——IEEE double 会在链式运算中累积漂移。工具函数在 `internal/logic/billing/money.go`(`AddMoney`/`SubtractMoney`/`MultiplyMoney`/`DivideMoney`/`RoundMoney`/`Max`/`Min`/`IsPositive` 等),比较用 `.GreaterThan(billing.Zero)`/`.IsZero()`/`.Equal()`,不要用 `== 0`。判断非金额小数(评分/利用率/延迟/告警阈值)保持 `float64`——判据是「这个数最终会不会变成钱」。
- **边界转换(只在出入口调用,禁止用于计算链路)**:
- **出口 → float64**:API `Res` 响应、内存缓存结构体、遗留 float64 接口,用 `billing.InexactFloat64(d)`(前端 JSON number 契约不变)。
- **入口 ← float64**:API `Req` 里的 float64 金额,用 `billing.NewFromFloat(f)` 转进 decimal 再运算。
@@ -466,12 +441,10 @@ cmd(路由注册)
| 文档 | 路径 | 用途 |
|------|------|------|
| 产品需求文档 | `docs/team-api产品需求文档.md` | 功能需求的最终依据 |
| 原始开发计划 | `docs/team-api开发计划.md` | S01-S26 迭代拆分 |
| new-api 调研 | `docs/new-api调研报告.md` | 参考项目能力盘点 |
| sub2api 调研 | `docs/sub2api调研报告.md` | 参考项目能力盘点 |
| 对比分析 | `docs/new-api-vs-sub2api对比分析报告.md` | 两者差异 |
| 大模型实施方案 | `docs/大模型完整实施方案-v2.1.md` | 大模型代理层设计 |
| relaykit 迁移状态 | `docs/relaykit迁移工作状态.md` | relay/ → relaykit 协议转换迁移进度、覆盖矩阵与守卫说明 |
| 渠道调度对比 | `docs/reference/多项目渠道调度对比.md` | new-api/sub2api/team-api 调度方案对比(team-api 侧描述已过时,以 relaykit/dispatch 为准) |
| Docker 部署 | `docs/deployment-docker.md` | Docker 部署指引 |
| 协议文档 | `docs/协议文档/` | 各供应商官方协议参考(OpenAI/Claude/Gemini/智谱/豆包/阿里等系列) |
| API 格式参考 | `docs/reference/api-format-reference.md` | JSON 示例、SSE 格式、错误映射、中间件差异 |
| 协议转换语料采集 | `docs/协议转换语料采集手册.md` | `corpus-extract` 命令用法:从渠道调试日志提取真实流量语料喂给协议转换测试 |
| GoFrame 使用规范 | `docs/reference/goframe-conventions.md` | 框架使用规范 + 已修复的框架 bug 记录 |
+9 -9
View File
@@ -4,7 +4,7 @@
**多租户 大模型 API 网关 SaaS 平台**
统一接入 OpenAI、Claude、Gemini 等 25+ 大模型供应商,提供计费、限流、监控和多租户管理能力。
统一接入 OpenAI、Claude、Gemini 等大模型供应商,提供计费、限流、监控和多租户管理能力。
[![Go](https://img.shields.io/badge/Go-1.25-00ADD8?style=flat-square&logo=go)](https://go.dev/)
[![GoFrame](https://img.shields.io/badge/GoFrame-v2.10-blue?style=flat-square)](https://goframe.org/)
@@ -20,18 +20,18 @@
## 功能特性
- **多租户架构** — 行级租户隔离,管理后台与租户控制台双独立用户体系
- **25+ 大模型供应商** — OpenAI、Claude、Gemini、DeepSeek、通义千问、智谱、Ollama 等
- **适配多家大模型供应商** — OpenAI、Claude、Gemini、DeepSeek、通义千问、智谱、Ollama 等
- **OpenAI 兼容 API** — 无缝替换 OpenAI API,支持对话补全、向量嵌入、图像生成、语音、实时通信等
- **智能渠道调度** — 优先级/权重路由、自动故障转移、健康监控、渠道亲和性
- **五层额度模型** — 租户钱包 → 套餐额度 → 成员额度 → 项目预算 → Key 额度
- **三层额度模型** — 租户钱包 → 项目预算/成员额度 → Key 额度
- **实时计费引擎** — 预扣 → 转发 → 结算 → 退款,Redis 原子操作保证并发安全
- **双控制台** — 管理后台(Naive UI)用于平台运营 + 租户控制台(TailwindCSS)面向终端用户
- **双控制台** — 管理后台用于平台运营 + 租户控制台面向终端用户
- **全链路可观测** — 请求日志、操作审计、监控告警,Request ID 贯穿全链路
> 在线演示地址:
>
> 管理端:https://team-api.net/admin/, 为做好数据安全措施,暂不公开登录方式
> 管理端:https://team-api.net/admin/, 未做好数据安全措施,暂不公开登录方式
>
> 用户端:https://team-api.net, 用户名:liu@163.com, 密码:Demo123456
@@ -383,8 +383,8 @@ make migrate-status # 查看迁移状态
1. ~~在线升级(已完成)~~
2. ~~支付模块(Easy Pay 已支持)~~
3. ~~图片生成同步转异步~~
4. 完善大模型支持,特别是图像和视频的支持
5. 角色管理,方便运营团队内部协作,预设角色(管理员,销售,技术支持)
4. ~~完善大模型支持,特别是图像和视频的支持~~
5. ~~角色管理,方便运营团队内部协作,预设角色(管理员,销售,技术支持)~~
6. 插件功能(定制功能通过插件实现,确保不与主线代码冲突)
7. 开放平台(对接OA的能力,方便企业对接管理)
@@ -399,7 +399,7 @@ make migrate-status # 查看迁移状态
请遵循项目现有代码风格和 [Conventional Commits](https://www.conventionalcommits.org/zh-hans/) 提交规范。
项目当前由 qianfree 单独维护。未来如接受外部贡献,将在合并贡献前提供贡献者许可协议(CLA),以确保 AGPL 与商业授权的权利链清晰完整。
项目当前由 qianfree 进行维护。未来如接受外部贡献,将在合并贡献前提供贡献者许可协议(CLA),以确保 AGPL 与商业授权的权利链清晰完整。
## 在线交流
欢迎加QQ群聊天吹水:1095286563
@@ -409,7 +409,7 @@ make migrate-status # 查看迁移状态
## 许可证
Copyright (C) 2026 qianfree。本项目社区版采用 [AGPL v3.0 或更高版本](LICENSE) 许可证。
Copyright (C) 2026 qianfree。 本项目社区版采用 [AGPL v3.0 或更高版本](LICENSE) 许可证。
### 核心要求
-2
View File
@@ -176,5 +176,3 @@ go list -m -f "{{if not .Main}}{{.Path}}|{{.Version}}|{{.Dir}}{{end}}" all
# 前端依赖(在各 web/* 目录下):
# bunx license-checker --summary / npx license-checker --json
```
> ⚠️ 本文件列出版本为生成时快照。每次发布商业版本前应重新扫描,重点核对**新增依赖**是否引入 copyleft 类许可证。
@@ -1,183 +0,0 @@
# 渠道调度策略:三项目对比(new-api / sub2api / team-api)
> **用途**:在 [`new-api-channel-scheduling-analysis.md`](./new-api-channel-scheduling-analysis.md) 与 [`sub2api-channel-scheduling-analysis.md`](./sub2api-channel-scheduling-analysis.md) 两份单独分析的基础上,补充 team-api 自身的实现剖析,并给出三者横向对比与 team-api 的改进建议。
>
> **路径约定**:
> - team-api 引用相对于**本项目根目录**(`D:\github\team-api`)
> - new-api 引用相对于 `new-api/` 参考项目根
> - sub2api 引用相对于 `sub2api/backend/`
>
> **分析时间**:2026-07-30。
> ⚠️ **team-api 部分已过时(2026-08-27 标注)**:本文描述的是渠道调度重写**之前**的实现,
> 文中引用的 `relay/scheduler/scheduler.go`、`internal/logic/relay/health.go` 均已删除。
> 特别注意第 5 节的「4 因子加权健康度」公式(成功率×0.40 + 延迟分×0.25 + 稳定性×0.20 +
> 连续失败分×0.15)**已废止**,现行健康体系为:Redis 渠道×模型 EWMA 为唯一真相,
> 健康分 = `avg(succ_ewma)^α × 100`,调度权重见 `relaykit/dispatch/score.go` 的
> `EffectiveWeight`,落盘见 `internal/dispatchadapter/maintenance.go`。
> new-api / sub2api 两侧的分析不受影响,横向对比结论仍有参考价值。
---
## 目录
1. [team-api 当前调度实现](#一team-api-当前调度实现)
2. [三项目对比总表](#二三项目对比总表)
3. [关键差异深度对比](#三关键差异深度对比)
4. [team-api 现状评估与改进建议](#四team-api-现状评估与改进建议)
---
## 一、team-api 当前调度实现
team-api 的调度核心在两处:算法层 `relay/scheduler/scheduler.go`,编排层 `internal/logic/relay/provider.go` + `relay/handler/relay_handler.go`。
### 1. 调度入口与候选构建
`DataProviderImpl.GetChannelForModel`(`internal/logic/relay/provider.go:98`)→ `selectChannelFromDB`(`provider.go:120`):
- 从 `chn_abilities`(模型→渠道映射)JOIN `chn_channels` + `chn_health_scores` 拉候选,过滤 `enabled=true`、`status='active'`、租户渠道范围、排除已失败渠道(`excludeChannelIDs`)。
- 有 feature flag **`channel_scheduler_v2_enabled`** 控制两套路径:
- **关闭(legacy)**:按 `priority DESC, weight DESC` 排序,取第一个能取到上游 key 的渠道(`provider.go:162-195`)。纯优先级首选,无随机、无健康度。
- **开启(v2)**:构建 `[]scheduler.ChannelCandidate`,交给调度引擎(`provider.go:197-279`)。
### 2. 优先级 + 权重 + 健康度选择算法(`relay/scheduler/scheduler.go`)
`Select`(`scheduler.go:49`)算法注释明确:
1. 排除 `Weight <= 0` 的渠道;
2. 排除 `HealthScore < 20` 的渠道(若全不健康则保留全部,避免饿死);
3. 按优先级分组,**取最高优先级组**(`groupByPriority` 升序排,`groups[len-1]` 即数值**大**优先——与 new-api 一致,与 sub2api 相反);
4. 组内**加权随机**(`weightedRandomSelect`)。
**健康度降权**(`effectiveWeight`,`scheduler.go:185`):健康度 50-79 权重减半,20-49 降至 1/4,最低保 1。即不健康渠道不直接剔除,而是降低被选概率。
**`SelectStable`**(`scheduler.go:65`):在最高优先级组内,**优先用亲和渠道**(`preferredChannelID` 命中即直接返回),否则用 **Rendezvous 一致性哈希**(`weightedRendezvousSelect`,`scheduler.go:164`)做加权选择——相同 seed + 候选集在**不同进程/实例得到相同结果**,适合多实例部署的冷请求。seed = `tenant:user:apikey:model` + 时间桶(桶长 = 亲和 TTL)。
### 3. 渠道亲和性(`internal/logic/relay/affinity.go`)
- **key**:`sha256(tenantID:userID:apiKeyID:modelName)` → Redis 存 `channelID`,TTL 默认 **1800s**(`channel_affinity_ttl_seconds` 可配)。
- **反向索引**:`relay:affinity:v2:channel:<channelID>` 维护该渠道绑定的所有 key 集合,渠道失效时 `InvalidateChannelAffinities`(`affinity.go:80`)用 Lua 脚本批量清除——避免脏亲和。
- **feature flag** `channel_affinity_enabled` 控制。
- **流程**:`selectChannelFromDB` 先 `getAffinity` 拿 preferredChannelID → `SelectStable` 在最高优先级组内优先命中;成功后 `SetChannelAffinity` 写回;硬失败 `DeleteChannelAffinity`(`provider.go:222-275`)。
- **关键限制**:亲和**只在最高优先级组内生效**。若亲和渠道不在当前最高优先级组,会被降级处理(`preferredChannelID=0` 或删除亲和,`provider.go:239-246`)。
### 4. 失败重试(`relay/handler/relay_handler.go:417`)
```
maxRetries = 3 // 即最多 4 次尝试(attempt 0..3)
for attempt := 0; attempt <= maxRetries; attempt++ {
selection = GetChannelForModel(..., excludeChannelIDs) // 排除已失败渠道
...转发...
失败 → excludeChannelIDs = append(..., selection.ChannelID) → continue
}
```
- **跨渠道重试**:失败渠道累积进 `excludeChannelIDs`,下次选号排除(`relay_handler.go:472/499/561`)。
- **容量溢出不消耗重试次数**:`acquireChannelLease` 失败时 `attempt--` 再 continue(`relay_handler.go:473`)。
- **重试触发判定**:
- DoRequest 失败用 `IsRetryableForRequest`(`relay/constant/errors.go`):状态码 **429/500/502/503/504** 或网络瞬时错误;**昂贵非幂等生成(图片/视频)对「模糊送达」错误不重试**,避免上游重复生成 + 重复计费。
- DoResponse 失败:流式 / 不可重试 / `attempt>=maxRetries` / **`IsResponseWritten`(响应已写给客户端)** 任一满足即终止(`relay_handler.go:569`)——防止成功体追加到已写错误体后造成响应污染。
- **每次失败副作用**:`UpdateChannelHealth(false)` + `IncrementConsecutiveFailure` + `DeleteChannelAffinity`。
### 5. 健康度模型(`internal/logic/relay/health.go`)
- **综合健康度公式**(`calcHealthScore`,`health.go:96`):
`健康度 = 成功率×0.40 + 延迟分×0.25 + 稳定性×0.20 + 连续失败分×0.15`(0-100)
- **EWMA 平滑**:成功率 α=0.1(`new=old×0.9+sample×0.1`),延迟 α=0.3(`new=old×0.7+sample×0.3`)。
- **连续失败**:失败 +1,成功归零;连续失败分 = `100 - consecutiveFailures×20`。
- **自动禁用**:`checkAutoDisable`(`health.go:91`)连续失败达阈值自动禁用渠道。
- 这是个**混合模型**:EWMA 平滑的成功率/延迟 + 离散的连续失败计数,比 new-api 精细,但比 sub2api 的纯 EWMA(错误率 + TTFT)多了离散阈值。
### 6. 容量控制(`relay/handler/channel_lease.go`)
- `AcquireChannelSlot`:Redis 原子占并发槽,`MaxConcurrency` 超限则失败(Redis 异常降级放行)。
- `channelLease`:长请求 30s ticker 刷新租约(`refreshLoop`),结束 `Release`。
---
## 二、三项目对比总表
| 维度 | new-api | sub2api | **team-api** |
|------|---------|---------|--------------|
| 调度单位 | channel | account(订阅账户) | channel |
| 框架 | Gin + GORM | Gin + ent | GoFrame v2 + gdb |
| 优先级语义 | 数值**大**优先,严格分层 | 数值**小**优先;通用严格分层 / OpenAI 软因子 | 数值**大**优先,严格分层(v2) |
| 层内分配 | 加权随机(weight) | 通用 LRU;OpenAI Top-K 加权随机 | 加权随机 / Rendezvous 一致性哈希 |
| 重试计数 vs 优先级 | retry 当优先级层下标(耦合) | 完全解耦 | 完全解耦(excludeChannelIDs 独立) |
| 健康感知 | 仅 auto-ban(401 禁用) | **EWMA 错误率 + TTFT**,纳入评分 + 粘性逃逸 | **4 因子加权健康度**(成功率/延迟/稳定性/连续失败)+ 降权 + 自动禁用 |
| 亲和性定位 | 规则引擎(model/path/UA + KeySource),**为 prompt cache** | session hash + previous_response_id,**为对话 KV**,带健康度逃逸 | `tenant:user:apikey:model → channel`,Redis + 反向索引,**为路由稳定** |
| 亲和作用域 | 全局规则匹配 | 会话级(可跨优先级,带逃逸) | **仅最高优先级组内** |
| 重试上限 | `RetryTimes`(默认 **0**!) | 换号 **10**(Gemini 3)+ 同号 3 + HTTP 5 次/10s | **`maxRetries=3`**(4 次尝试) |
| 重试触发码 | 可配状态码范围(默认 4xx 除 400/408、5xx 除 504/524) | 401/403/429/529/≥500 换号 | 429/500/502/503/504 + 网络瞬时 |
| 流式安全 | 无特殊处理 | 流式已写禁止 failover | `ResponseWritten` 禁止重试 |
| 容量/并发控制 | 无显式 | 并发槽 + 等待队列 + wait plan | Redis 并发槽 + 租约刷新 |
| 跨实例稳定性 | 内存缓存(每实例独立) | 确定性种子 RNG | **Rendezvous 一致性哈希**(多实例对齐) |
| 非幂等保护 | 无 | 无显式 | **昂贵生成模糊送达不重试**(防重复计费) |
| 失败渠道处理 | auto-ban 移出缓存 | 临时封禁 + 换号 | 健康度降权 + 连续失败自动禁用 |
---
## 三、关键差异深度对比
### 1. 优先级模型:严格分层 vs 软因子
- **new-api / team-api**:严格分层——最高优先级组独占流量,层内才看权重/健康。设计简单、主备语义清晰。
- **sub2api(OpenAI 路径)**:优先级只是多维评分里的一个归一化因子,和负载/错误率/延迟平等竞争。更灵活(高优先级但高负载的渠道会被降级),但行为更难预测。
- **team-api 特点**:严格分层 + Rendezvous 哈希,是三者中**唯一原生支持多实例确定性选择**的(new-api 靠每实例内存缓存、sub2api 靠确定性种子,team-api 用一致性哈希更优雅)。
### 2. 健康感知:三者代际差异明显
- **new-api**:最粗糙——只有 401 auto-ban,无实时健康度。
- **team-api**:中间代——4 因子加权健康度 + EWMA 平滑 + 降权(不剔除)+ 连续失败自动禁用。粒度合理。
- **sub2api**:最精细——纯 EWMA(错误率 + TTFT)实时反馈,且**健康度参与粘性逃逸决策**(健康恶化主动跳船),这是 team-api 当前缺的能力。
### 3. 亲和性定位完全不同
- **new-api**:为 **prompt cache 命中**(绑定上游 KV 缓存),规则引擎匹配,全局生效。
- **sub2api**:为 **对话状态/KV**(订阅账户状态私有),会话级 + previous_response_id,**带健康度逃逸**。
- **team-api**:为 **路由稳定**(让同一租户/用户/key/模型尽量落同一渠道),简单直接,但**只限最高优先级组内**——若渠道分布在多优先级层,亲和效果会被优先级分层削弱。
### 4. 重试与 failover 的层次
- **new-api**:单层 `retry`,且默认 `RetryTimes=0`(不重试),耦合优先级层下标。
- **sub2api**:三层独立有界(同号 HTTP 5/10s → 同号业务 3 → 跨号 10),最细致,有流式安全阀。
- **team-api**:单层 `maxRetries=3`,但有三个优秀细节——容量溢出不消耗重试次数、`ResponseWritten` 禁止重试、昂贵非幂等生成的模糊送达不重试。整体介于 new-api 和 sub2api 之间。
---
## 四、team-api 现状评估与改进建议
### 现状评分
team-api 的调度实现**整体处于三者中游偏上**:
- ✅ **优于 new-api**:有真实健康度模型、响应已写安全阀、非幂等保护、跨实例一致性哈希、Redis 并发控制。
- ✅ **优于 sub2api**:多实例确定性(Rendezvous)、非幂等生成的重复计费保护。
- ⚠️ **弱于 sub2api**:缺粘性逃逸、重试层次单一、亲和受限于最高优先级组。
- ⚠️ **共性短板**:legacy/v2 双路径并存(`channel_scheduler_v2_enabled` 关闭时退化成纯优先级无健康度),需确保生产开 v2。
### 可落地的改进建议(按优先级)
| # | 建议 | 借鉴自 | 预期收益 |
|---|------|--------|---------|
| 1 | **亲和性加健康度逃逸**:亲和渠道 `HealthScore < 阈值` 时主动放弃亲和、降级选号,避免坏渠道粘死会话 | sub2api `shouldEscapeStickyAccount` | 防止一个故障渠道拖垮一批固定用户 |
| 2 | **亲和性作用域放宽**:允许亲和渠道跨优先级层命中(带降级权重),而非仅限最高优先级组 | sub2api 会话粘性跨优先级 | 提升亲和命中率,prompt cache/路由稳定收益更大 |
| 3 | **重试分层**:把「同渠道瞬时重试」与「跨渠道 failover」拆成两层独立计数,同渠道先短退避重试 1-2 次(429/503 常是瞬时的) | sub2api 三层 failover | 减少不必要的换渠道,降低抖动 |
| 4 | **健康度引入 TTFT**:在 4 因子里加首 token 延迟(流式场景比平均延迟更影响体感),或提高延迟权重 | sub2api EWMA TTFT | 流式体验优化 |
| 5 | **退避策略**:当前重试无退避(立即换渠道),高频 429 风暴下可能放大上游压力;加指数退避 | sub2api `retryBackoffDelay` / new-api | 保护上游、降低连环失败 |
| 6 | **下线 legacy 路径**:`channel_scheduler_v2_enabled` 关闭时退化为无健康度调度,建议确认生产已开并择机移除双路径 | — | 降低维护成本、避免误配置 |
### 关键源码索引(team-api)
| 关注点 | 文件:函数 |
|--------|----------|
| 调度算法入口 | `relay/scheduler/scheduler.go:49` — `Select`;`:65` — `SelectStable` |
| 候选过滤 + 最高优先级组 | `relay/scheduler/scheduler.go:83` — `highestPriorityCandidates` |
| 健康度降权 | `relay/scheduler/scheduler.go:185` — `effectiveWeight` |
| Rendezvous 一致性哈希 | `relay/scheduler/scheduler.go:164` — `weightedRendezvousSelect` |
| 选号编排(候选构建 + 亲和) | `internal/logic/relay/provider.go:120` — `selectChannelFromDB` |
| 亲和性读写 | `internal/logic/relay/affinity.go` — `getAffinity`/`setAffinity`/`InvalidateChannelAffinities` |
| 重试主循环 | `relay/handler/relay_handler.go:417` — `for attempt` |
| 重试判定 | `relay/constant/errors.go` — `IsRetryable`/`IsRetryableForRequest`/`IsResponseWritten` |
| 健康度计算 | `internal/logic/relay/health.go:22` — `UpdateHealthScore`;`:96` — `calcHealthScore` |
| 容量租约 | `relay/handler/channel_lease.go` — `acquireChannelLease`/`channelLease` |
-305
View File
@@ -1,305 +0,0 @@
# relay → relaykit 协议转换迁移 · 工作状态
> 记录时间:2026-09-10(上一版 2026-09-09)
> 分支:`feat/relaykit`(基于 main,领先 2 个已提交 commit + 大量未提交工作区改动)
> 状态:**未提交**。全仓 `go build ./...` + `go vet` 通过;relaykit 模块测试全绿;
> relay 侧仅剩既有失败(见 §四)。§5.1 已修,另发现并修复 **2 个本次迁移引入的回归**(见 §3.6)。
---
## 一、本次任务目标
把 relay 层残留的「纯协议转换」代码全部迁进独立模块 `relaykit/`,让 relaykit 成为唯一转换路径,
宿主 `relay/` 只保留 HTTP/SSE/计费/调度等宿主职责。
## 二、开工前定下的 4 个决策(用户已拍板)
| 决策点 | 选定方案 |
|--------|----------|
| 注册表改造 | **允许单侧注册**(请求侧或响应侧可单独为空),反向方向才注册得进去 |
| 跨原生方向表达 | **中枢链式 + 直连覆盖**:请求走 `A→OpenAI→B` 步骤链,响应用直连转换器(保真度高)。<br>后修订:Claude→Gemini 请求侧改为直连(见 §六.11) |
| legacy 兜底策略 | **立即 hard-fail**:不再"解析失败回退旧实现",legacy 随迁随删 |
| 迁移范围 | 全选:4 个跨原生响应桥 + openai/converter.go 反向方向 + o2c/o2g 双实现清理 + 小方向补全 |
## 三、已完成的工作
### 3.1 relaykit 注册表基建(新增/改造)
| 文件 | 说明 |
|------|------|
| `relaykit/relayconvert/text_converter_registry.go` | 放宽双侧非空约束 → 单侧注册(两侧都空才 panic),只把已配置的一侧写进对应注册表 |
| `relaykit/relayconvert/engine.go`(新增) | `ConvertRequestByID` / `ExecuteRequestConversion`:统一执行直连转换器与 StepConverters 步骤链,调用方无需区分 |
| `relaykit/relayconvert/stream_event.go`(新增) | `StreamEvent{Event, Data, Usage *dto.UsageWithDetails}`:非 OpenAI 客户端方向的通用流式帧封装(Event 非空=事件帧,空=纯 data 帧) |
| `relaykit/relayconvert/errors.go`(新增) | `ErrStatefulResponsesUnsupported` 哨兵(顶层包,宿主可 `errors.Is`) |
| `relaykit/relayconvert/convmeta/responses_stash.go`(新增) | `ResponsesStash` 能力接口:Responses 请求快照 stash/读取 |
| `request_registry.go` / `response_registry.go` | 补齐全部方向的 converter ID 常量(含跨原生、反向、Responses 双向、流式) |
**注意**:`StreamEvent.Usage` 中途从 `*dto.Usage` 升级为 `*dto.UsageWithDetails`(瘦类型会丢缓存明细 → 影响计费),
所有转换器已按新契约适配。
### 3.2 转换器迁移(4 个并行子任务,全部完成且测试通过)
| 子任务 | 产出包 | 内容 |
|--------|--------|------|
| A | `relaykit/relayconvert/internal/oai_chat/` | Claude 入站方向:`ClaudeToOpenAIRequestConverter`、`OpenAIToClaudeResponseConverter`、`OpenAIToClaudeStreamConverter` |
| B | `relaykit/relayconvert/internal/oai_gemini/` | Gemini 入站方向:`GeminiToOpenAIRequestConverter`、`OpenAIToGeminiResponseConverter`、`OpenAIToGeminiStreamConverter` |
| C | `relaykit/relayconvert/internal/oai_responses/`(新包)+ `relaykit/dto/openai_responses.go`(新) | Responses 双向 6 个转换器;Responses DTO 下沉到 relaykit,`relay/dto/openai_responses.go` 改为纯别名文件 |
| D | `internal/claude_gemini/` + `internal/native_responses/`(新包) | 8 个跨原生响应转换器(Claude↔Gemini、Claude/Gemini→Responses,各含流式) |
### 3.3 注册接线
`relaykit/relayconvert/register/register.go` 现注册 **12 个方向**:
- OpenAI 入站 → Claude / Gemini / Ollama(chat)(原有)
- 反向:Claude / Gemini / Responses 入站 → OpenAI 上游(新)
- OpenAI chat → Responses 上游(ChatViaResponses 桥接,新)
- 跨原生:Claude→Gemini、Gemini→Claude、Responses→Claude、Responses→Gemini
(新;响应侧全部直连,请求侧 Claude→Gemini 已改直连,其余仍为步骤链)
### 3.4 宿主桥接层改造
| 文件 | 说明 |
|------|------|
| `relay/relaykit_bridge/route.go`(**新增**) | **唯一权威方向矩阵**:`KitFormat`(宿主↔relaykit 格式名映射,Responses 命名不同需显式转)、`EffectiveUpstreamFormat`(含 UseResponsesAPI / UpstreamSpeaksResponses 覆盖)、`RequestConverterIDForRoute`、`ResponseConverterIDForRoute`。另在此包 blank-import `register` 触发注册(放桥接包而非某调用方,保证所有 channel 包及其单测都能拿到注册结果) |
| `relay/relaykit_bridge/response.go` | 签名改 4 值 `(converted, usage, handled, err)`,hard-fail 语义;按格式解析上游响应(新增 OpenAI/Responses 两种) |
| `relay/relaykit_bridge/stream.go` | chunkWriter 支持 `StreamEvent`(事件帧/data 帧分派 + usage 明细捕获);**收尾策略按客户端格式**:OpenAI/Gemini 客户端写 `[DONE]`(OpenAI 另补终止 chunk),Claude/Responses 客户端不写(终止语义由 message_stop / response.completed 承载) |
| `relay/handler/relaykit_bridge.go` | 重写:走统一矩阵、按入站格式解析请求、hard-fail、OpenAI 上游后处理(stream_options / reasoning_effort 注入,均"客户端未显式设置时"语义)、哨兵错误映射 |
| `relay/handler/relay_handler.go` | `convertRequestBody` 改三态处理(handled+err / handled / 未接管走 adaptor) |
| `relay/common/relay_info.go` | `*RelayInfo` 实现 `convmeta.ResponsesStash` |
### 3.5 宿主 legacy 删除(hard-fail 接线)
已改为纯桥接外壳并删除旧转换实现:
- `relay/channel/claude/response.go`(handleNonStreamToOpenAI / handleStreamToOpenAI,删约 270 行流式循环)
- `relay/channel/gemini/response.go`(同上,删约 300 行)
- `relay/channel/ollama/adaptor.go`(chat 非流式+流式,删约 150 行)
- `relay/channel/openai/claude_response.go`(重写为桥接外壳)
- `relay/channel/openai/gemini_response.go`(重写为桥接外壳,保留 `writeOpenAIErrorAsGemini` 等错误映射)
- 4 个跨原生桥 `claude/gemini_bridge.go`、`claude/responses_bridge.go`、`gemini/claude_bridge.go`、`gemini/responses_bridge.go`(由子任务改造,**见待办**)
---
## 四、当前验证状态
```
GOTOOLCHAIN=go1.25.8 go build ./... ✅ 通过
GOTOOLCHAIN=go1.25.8 go vet ./relay/... ✅ 通过
cd relaykit && GOWORK=off go test -count=1 ./... ✅ 全绿(14 个包)
GOTOOLCHAIN=go1.25.8 go test ./relay/... ✅ 仅 relay/helper 既有失败(见 §六.7)
GOTOOLCHAIN=go1.25.8 go test ./internal/... ⚠️ 3 个既有失败,与本次迁移无关:
· internal/logic/admin TestBuiltinRoleDefaultsMatchMigration
—— 找不到 000020_admin_role_management.sql(迁移已重编号为 000020_v0_2_17_version.sql)
· internal/dispatchadapter TestProbeFailure_DoesNotDecayHealthEwma —— 健康分 EWMA
· internal/logic/billing TestGenerateBillingSummary_TimeRule —— 账单文案时段行
(三处工作区均无改动,勿误当回归)
gofmt -l relay/ relaykit/ ⚠️ 剩 10 个**存量**未格式化文件
(ali/volcengine converter_test、relaykit dto/audio、goldentest、benchmark_test 等,
本次迁移触及的 5 个文件已 gofmt -w)
```
---
## 五、重启后的待办
### 5.1 ~~修复 3 个流式错误传播测试~~ ✅ 已完成(2026-09-10)
**上一版留下的坑**:这 3 个测试当时是被「反转断言」改绿的——把「应返回错误」改成
「不返回错误、只查 StreamStatus」。但 `StreamEndReasonError` **不在** `IsPartialStreamEnd()`
集合内(`relay/common/stream_status.go:79`),于是 `DoResponse` 返回 `(usage, nil)`,
落到 `relay_handler.go` 的**成功路径**:`sess.Finish(true)` 让调度 FSM 收到「成功」、
熔断/健康分拿不到上游故障信号,并按正常成功结算(usage 近零)。旧实现是返回 error 的。
**已按原文档倾向的方案 1 修正**:
- `TryConvertStreamViaRelaykit` 签名改三态 `(*common.Usage, bool, error)`;
`ok=true,err!=nil` = 已写出 SSE 但流以错误/中断结束,调用方原样 `return usage, err`。
- 新增 `streamConvertError`(`relay/relaykit_bridge/stream.go`)做错误分类,
一律置 `ResponseWritten`(SSE 头与 200 已提交,不得改状态码/换渠道重试)。
- 客户端断开分支直接透出 `common.ErrStreamInterrupted`,9 个调用点不再各自
读 `IsPartialStreamEnd()` 判定,统一收敛到桥接层。
- 3 个测试断言改回「应返回错误」,并加断分类与收尾事件。
**顺带修正的分类错误**:安全拦截(Gemini `promptFeedback.blockReason`)原先会被映射成
502 上游错误 → 正常渠道因用户发违规提示词被扣健康分乃至熔断。现新增
`relaykit/relayconvert.ErrContentBlocked` 哨兵(6 个 Gemini 出站转换器 wrap 返回),
宿主 `errors.Is` 识别后映射为请求类 4xx。非流式侧同理,新增
`relaykit_bridge.ResponseConvertError` 供 3 个 Gemini 上游非流式站点统一分类。
6 个方向(→Claude / →Responses / →OpenAI × 流式/非流式)均已有测试覆盖。
### 5.2 ~~核对子任务 D 的跨原生桥接线成果~~ ✅ 已核对(结论与担心相反)
`geminiToClaudeResponse` / `buildResponsesBodyFromGemini` **仍在且不能删**:
它们是 Code Assist 强制流式聚合路径(`gemini/adaptor.go:514,520`
`handleCodeAssistAggregatedStream`)复用的非流式装配函数,各文件头注释已写明。
计费 usage 已确认仍从**原始上游响应体**提取。
### 5.3 ~~收尾清理~~ ✅ 已完成(2026-09-10,legacy 删除落地)
删除过程中发现并补齐了 2 处迁移遗漏(见 §七的「删除时补的接线」)。
### 5.4 提交前检查
- `gofmt -l` 本次触及文件已清空(存量 10 个未动,避免污染本次 diff)
- `go vet ./relay/... ./internal/...` ✅
- 提交信息不得包含 AI 工具身份标识(项目规范)
- 建议拆 commit:注册表基建 / 转换器迁移 / 宿主接线 / legacy 删除 /
**本次新增:流式错误传播 + 安全拦截分类 / Anthropic 端点与 provider 后处理回归修复**
---
## 3.6 本次新发现并修复的 2 个回归(重点,评审必看)
两者同一个根因:**relaykit 的转换矩阵按「协议格式」裁决、与供应商无关**,
命中后 `adaptor.ConvertRequest` 整个不再被调用。而 §3.3 新增的「非 OpenAI 入站 →
OpenAI 上游」三个反向方向,把原本归 adaptor 的路径整片接管走了。
### 回归 A:Anthropic 兼容端点渠道的 Claude 入站双向断链(严重)
ali / zhipu / deepseek / moonshot / volcengine / minimax 这 6 家主协议是 OpenAI,
但对 `RelayModeClaudeMessages` **另挂独立的 Anthropic 兼容端点**
(`/anthropic/v1/messages`、`/apps/anthropic/v1/messages`、`/api/coding/v1/messages`),
Claude 入站时上游**实际说 Claude**:请求体原样发、响应体也是 Claude
(其 `DoResponse` 正是委托给 `claude.Adaptor`)。
但 `helper.ProviderNativeFormat` 只按渠道类型返回单一格式(这几家 → openai),
且 `inboundMatchesChannelNative` 因此判 false 使 `canPassThrough` 也不兜底,于是:
- 请求侧:Claude 体被 relaykit 转成 OpenAI chat,POST 到 Anthropic 端点 → 上游 400;
- 响应侧:返回的 Claude 响应体被当 OpenAI 解析 → 转换失败。
**修复**:新增 `constant.HasNativeClaudeEndpoint(providerType)`,在
`relaykit_bridge.EffectiveUpstreamFormat` 与 `handler.inboundMatchesChannelNative`
两处叠加判定 —— Claude 入站 + 该供应商 ⇒ 有效上游格式 = Claude ⇒ 同格式、矩阵返回空串
⇒ 不转换,回到 adaptor / 直连原路径。
> 登记判据:`GetRequestURL` 对 `ClaudeMessages` 返回了与 chat 端点**不同**的地址。
> 共用同一端点的(aws / baidu_v2 / mistral / openai / xai / xunfei)上游只说 OpenAI,
> 不属于本集合,Claude 入站仍须转换。新增此类供应商时必须同步登记。
### 回归 B:5 个供应商的私有请求后处理被绕过
xai / baidu_v2 / zhipu / ali / deepseek 在格式转换之外还有私有请求适配,
relaykit 只补了通用的 `model` / `stream_options` / `reasoning_effort`:
| provider | 丢失的后处理 | 后果 |
|---|---|---|
| xai | `-search`/`-high`/`-low` 后缀剥离 + `search_parameters`/`reasoning_effort` | 未配映射时 `grok-4-search` 原样发上游 → 400 |
| baidu_v2 | `-search` 后缀剥离 + `web_search` 注入 | 同上 |
| zhipu | `applyGLMCompatibility`(top_p 裁剪、图片前缀剥离)+ 思考参数方言 | GLM 参数越界、思考失效 |
| ali | DashScope 按上游模型剥离 `thinking_budget` | 上游拒绝不支持该参数的模型 |
| deepseek | `thinking` 对象(V3 `type` + R1 `reasoning_effort:"none"`) | `-thinking`/`-nothinking` 静默失效 |
模型名那条最实际:未配映射时 `UpstreamModel = row.ModelName`
(`internal/dispatchadapter/catalog.go:443`)**带着后缀**,relaykit 无条件写
`GetUpstreamModelName()`,而 adaptor 本来会剥掉后缀再写回。
**修复**:新增可选能力接口 `common.RequestPostProcessor`
(`PostProcessConvertedRequest(ctx, info, body) ([]byte, error)`),
`handler.convertRequestBody` **仅在 relaykit 接管时**调用(走 adaptor 那条路径的
`ConvertRequest` 内部已含同一段,重复执行会双写)。5 家各把自己 `ConvertRequest`
的尾段抽成 `postProcessRequest`,与钩子共用同一实现,杜绝两份漂移。
已核对**不受影响**:minimax / moonshot 的 Claude 入站分支只做模型名映射(relaykit 已覆盖);
其余约 15 个 OpenAI 兼容 provider 的 `ConvertRequest` 无格式外后处理。
---
## 七、legacy 删除完成记录(2026-09-10 已全部落地)
净删约 **6400 行**(git diff --stat:+530 / -6907)。逐项:
| 项 | 结果 |
|----|------|
| `openai/converter.go` o2r 段(`ConvertOpenAIToResponses` + c2r 助手) | ✅ 删除(chat→Responses 请求转换由 relaykit `ConverterOpenAIChatToOpenAIResponses` 接管,handler 桥注入 reasoning_effort) |
| `openai/converter.go` c2o/g2o/r2o(`ConvertClaudeToOpenAI` 等) | ⚠️ **保留**:Ollama 渠道的 Claude/Gemini/Responses 入站走 `ConvertToOpenAI`→OpenAI→Ollama 链(矩阵未注册 Claude→Ollama 方向,属「未迁方向保留」设计) |
| `openai/responses.go` `handleResponsesInboundStream`/`NonStream`(约 600 行) | ✅ 改为 relaykit 桥接壳(**这是迁移漏掉的接线**——转换器早注册了但 openai adaptor 一直在调 legacy 实现);孤儿助手 `chatCompletionToResponsesResponse`/`extractStreamEmbeddedError` 删除,`extractResponsesRequestEcho` 保留(`BuildResponsesObjectMap` 导出依赖) |
| `claude/converter.go`(446 行)+ `converter_test.go` | ✅ 整文件删除(o2c 请求侧全套) |
| `gemini/converter.go`(686 行)+ `converter_test.go` | ✅ 整文件删除(o2g 请求侧全套) |
| 18 个 adaptor 的 `ConvertToOpenAI` 守卫 | ✅ 删 17 个(死代码:非 OpenAI 入站→OpenAI 上游已全由矩阵接管);**ollama 保留**(见上) |
| claude/gemini/openai adaptor 的入站格式 switch | ✅ 删除,改为同格式路径 + **hard-fail 守卫**(矩阵意外未接管时显式报错,不静默错格式透传) |
| `deepseek` `convertResponsesToChatForDeepSeek` | ✅ 删除,分支改 hard-fail(relaykit Responses→OpenAI + PostProcess 钩子接管) |
| stale 注释(passthrough/bridge 头注释中的旧函数名) | ✅ 更新为 relaykit 表述 |
**删除时补的接线**(不做这两步直接删会断链):
1. **openai adaptor responses 入站桥接**:迁移只注册了转换器、没改 adaptor 调用点。已把两个 inbound 处理器改为调 `TryConvertStream/ResponseViaRelaykit`(非 200 透传语义保留,计费仍取上游原始 chat 体口径)。
2. **Responses 客户端流错误收尾**:桥接层在转换器以错误退出且未产出任何终止态事件时补发 `response.failed`(否则 200 SSE 静默结束、codex 等客户端挂起);转换器已自行收尾的(如 Gemini SAFETY 发 completed 拒答)不追加第二终止事件。相关测试已按新契约改写。
**验证**:`go build ./...` ✅、`go vet ./relay/... ./internal/...` ✅、relaykit 模块全绿 ✅、
relay 测试仅剩 §六.7 既有失败 ✅、触及文件 gofmt 干净 ✅。
## 六、重要设计约束备忘(重启后容易忘)
1. **格式名两侧不同**:宿主 `constant.RelayFormatResponses = "responses"`,relaykit `types.RelayFormatOpenAIResponses = "openai_responses"`。
跨模块必须走 `relaykit_bridge.KitFormat()`,**不能直接强转**(其余格式字符串值相同可强转)。
2. **计费 usage 一律取上游原始口径**,不从转换后的响应体取。OpenAI 上游方向需置
`CacheIncludedInPrompt = true`(prompt 含 cached,计费按明细扣减,避免双重计费)。
3. **转换器只能依赖 `convmeta.Meta` 接口**,禁止 import 宿主 `relay/...` 或 GoFrame。
需要宿主状态时走能力接口(如 `ResponsesStash`)+ 类型断言,断言失败静默降级。
4. **`relay/dto` 是 `relaykit/dto` 的类型别名**(`relaykit_types.go` + `openai_responses.go`),两侧同型,边界零转换。
5. **转换器 ID 元数据方法(ID/From/To/Quality)与注册用的配对 spec ID 不是一回事**:
注册以 `RegisterTextConverter` 传入的 spec.ID 为准,元数据方法只是自描述。
6. relaykit 是独立 module:测试要 `cd relaykit && GOWORK=off go test ./...`;
宿主构建要 `GOTOOLCHAIN=go1.25.8`(否则 stdlib 重编译失败)。
7. `relay/helper` 的 `TestSafeUpstreamErrorMessage_*` 失败是**既有问题**(中英文案期望不一致),
与本次迁移无关,不要误当回归去修。
8. **矩阵按格式裁决、与供应商无关**——这是本次两个回归的共同根因。新增/修改方向时必须自问:
① 该供应商对这个 relay mode 是否另有原生端点(→ `constant.HasNativeClaudeEndpoint` 类判定)?
② 该供应商的 `ConvertRequest` 里除格式转换外是否还有私有适配
(→ 实现 `common.RequestPostProcessor`)?漏掉任一条都是**静默**降级或断链。
9. **`ProviderNativeFormat` 是「按渠道类型的单一格式」,不是全部真相**:多协议供应商的
有效上游格式取决于 (供应商, relay mode),权威计算在 `relaykit_bridge.EffectiveUpstreamFormat`,
`handler.inboundMatchesChannelNative`(直连判定)必须与它保持同步,否则直连与转换两条路会对同一
请求给出不同格式结论。
10. **provider 后处理钩子只在 relaykit 路径调用**:adaptor 路径的 `ConvertRequest` 内部已含同一段,
两处都调会双写参数。5 家的实现均把尾段抽成 `postProcessRequest` 与钩子共用,改动时不要只改一处。
11. **跨原生请求侧优先直连,不要新增中枢步骤链**:Claude→Gemini 已由
`internal/claude_gemini/ClaudeToGeminiRequestConverter` 直连。链式(`StepConverters`)会让
中间格式承载不了的字段静默消失——实测丢 thinking 签名、精确 thinking budget(5000 被放大成 8192)、
`top_k`、`tool_use.id` 四项,见 `claude_to_gemini_request_test.go` 的
`TestClaudeToGeminiRequest_PreservesWhatChainLoses`。新增跨原生方向时先写直连转换器,
链式只作过渡手段。Gemini 的 JSON Schema 关键字白名单在 `internal/shared.CleanGeminiToolParams`,
OpenAI→Gemini 与 Claude→Gemini 两条路径共用一处,改白名单是两边同时生效。
---
## 八、模型名后缀(虚拟模型)机制移除(2026-09-10)
经用量核实无任何带后缀模型(`-thinking`/`-nothinking`/`-none`/`-minimal`/`-low`/`-medium`/
`-high`/`-xhigh`/`-max`/`-search`)在用,整套「模型名后缀 → 虚拟模型」机制作为技术债务移除。
思考/搜索能力此后一律由**客户端请求参数**表达(`thinking` 对象、`reasoning_effort`、
`reasoning.effort` 等),网关只做协议转换不做能力注入。
### 删除清单
**宿主侧:**
- `relay/helper/thinking.go`(`ParseThinkingSuffix` + `ThinkingInfo`)整文件删
- `resolveRelayModel` 简化为纯目录校验(不再「字面优先 + 剥后缀回退」两段查找)
- `RelayInfo` 删 `ThinkingEnabled`/`ThinkingDisabled`/`ReasoningEffort` 三字段
及 `Get/SetReasoningEffort`(convmeta.Meta 相应方法一并删,本就零调用方)
- 各注入器删除:claude `injectClaudeThinking/Effort`、gemini `injectGeminiThinking`、
openai `injectReasoningEffort/injectResponsesReasoning`、deepseek 三个 inject* +
`isDeepSeekV4Model`、zhipu `injectThinkingParams`
- `canPassThrough` 删 thinking 后缀排除条件;handler 桥删两处 effort 注入(stream_options 保留)
- xai/baidu_v2 的 `-search`/`-high`/`-low` 剥离删除,两家 + deepseek 的
`RequestPostProcessor` 钩子实现删除(剩余职责 relaykit 已覆盖);**钩子接口保留**,
现存实现方仅 zhipu(GLM 参数兼容)与 ali(DashScope 参数裁剪)
**relaykit 侧:**
- `internal/shared/thinking_adapter.go`、`reasoning/` 包整体删除
- `convmeta.Options` 删 `PreserveThinkingSuffix`;Claude/Gemini Options 删
`ThinkingAdapter*` 字段(宿主侧写死 TODO 的配置一并清)
- oai_chat 请求转换器不再解析上游模型名后缀
### 保留(勿误删)
- **客户端参数转换线**:`c2oConvertThinkingToReasoningEffort` / `g2oConvertThinkingConfig`
(Claude thinking 对象 / Gemini thinkingConfig → chat `reasoning_effort` 字段)——
这是协议转换不是后缀注入,宿主 converter.go 与 relaykit oai_chat 各一份
- `bil_usage_logs.reasoning_effort` 列与 `UsageRecord.ReasoningEffort` 字段保留
(dao 生成 + 历史数据),此后恒为空串
- `RelayInfo.BaseModelName` 字段保留(responses 路由记录在用),现恒等于 OriginModelName
### 行为变化
- 请求带旧后缀名(如 `gpt-4o-thinking`)→ 目录查不到 → **404 model not found**
(原为剥后缀回退命中 + 注入思考参数)
- 名字本身含 `-max` 等字样的真实目录模型(qwen-max)不受影响(本就字面优先)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB