# AI Gateway Backend

AI 网关后端服务，提供统一的大模型 API 代理接口。

## 功能特性

- 支持 OpenAI 协议（`/openai/...`，例如 `/openai/chat/completions`）
- 支持 Anthropic 协议（`/anthropic/v1/...`）
- 支持 Hub-and-Spoke 跨协议双向转换（OpenAI ↔ Anthropic）
- 同协议透传（跳过 Canonical 全量转换，保持协议语义）
- 支持流式响应（SSE）
- 支持 Function Calling / Tools
- 支持 Thinking / Reasoning
- 支持扩展层接口（Models、Embeddings、Rerank）
- 多供应商配置和路由
- 用量统计
- 结构化日志（zap + lumberjack + 模块标识）
- YAML 配置管理
- 请求验证
- 中间件支持（请求 ID、日志、恢复、CORS）

## 日志规范

### 模块标识

每个模块通过依赖注入获取带模块标识的 logger：

```go
func NewProxyHandler(..., logger *zap.Logger) *ProxyHandler {
    return &ProxyHandler{
        logger: pkglogger.WithModule(logger, "handler.proxy"),
    }
}
```

输出格式：
- Console: `INFO  [handler.proxy]  处理请求  method=POST path=/v1/chat`
- JSON: `{"level":"info","logger":"handler.proxy","msg":"处理请求"}`

### 模块命名规范

| 模块 | 命名 |
|------|------|
| ProxyHandler | `handler.proxy` |
| ProviderHandler | `handler.provider` |
| Provider Client | `provider.client` |
| ConversionEngine | `conversion.engine` |
| RoutingCache | `service.routing_cache` |
| StatsBuffer | `service.stats_buffer` |
| Database | `database` |

### 标准字段

使用 `pkg/logger/field.go` 中定义的字段构造函数：

```go
logger.Info("请求开始",
    pkglogger.Method("POST"),
    pkglogger.Path("/v1/chat"),
    pkglogger.RequestID("xxx"),
)
```

### GORM 日志

GORM 日志自动桥接到 zap，SQL 查询映射到 Debug 级别。

## 技术栈

- **语言**: Go 1.26+
- **HTTP 框架**: Gin
- **ORM**: GORM
- **数据库**: SQLite / MySQL
- **日志**: zap + lumberjack
- **配置**: Viper + pflag（多层配置：CLI > 环境变量 > 配置文件 > 默认值）
- **验证**: go-playground/validator/v10
- **迁移**: goose

## 项目结构

```
backend/
├── cmd/
│   └── server/
│       └── main.go              # 主程序入口（依赖注入）
├── internal/
│   ├── config/                  # 配置管理
│   │   ├── config.go            # Viper 多层配置加载/验证
│   │   └── models.go            # GORM 数据模型
│   ├── domain/                  # 领域模型
│   │   ├── provider.go
│   │   ├── model.go
│   │   ├── stats.go
│   │   └── route.go
│   ├── handler/                 # HTTP 处理器
│   │   ├── middleware/           # 中间件
│   │   │   ├── request_id.go
│   │   │   ├── logging.go
│   │   │   ├── recovery.go
│   │   │   └── cors.go
│   │   ├── proxy_handler.go     # 统一代理处理器
│   │   ├── provider_handler.go
│   │   ├── model_handler.go
│   │   └── stats_handler.go
│   ├── conversion/              # 协议转换引擎
│   │   ├── canonical/           # Canonical Model
│   │   │   ├── types.go         # 核心请求/响应类型
│   │   │   ├── stream.go        # 流式事件类型
│   │   │   └── extended.go      # 扩展层 Models
│   │   ├── openai/              # OpenAI 协议适配器
│   │   │   ├── types.go
│   │   │   ├── adapter.go
│   │   │   ├── decoder.go
│   │   │   ├── encoder.go
│   │   │   ├── stream_decoder.go
│   │   │   └── stream_encoder.go
│   │   ├── anthropic/           # Anthropic 协议适配器
│   │   │   ├── types.go
│   │   │   ├── adapter.go
│   │   │   ├── decoder.go
│   │   │   ├── encoder.go
│   │   │   ├── stream_decoder.go
│   │   │   └── stream_encoder.go
│   │   ├── adapter.go           # ProtocolAdapter 接口 + Registry
│   │   ├── stream.go            # StreamDecoder/Encoder/Converter
│   │   ├── middleware.go        # Middleware 接口和 Chain
│   │   ├── engine.go            # ConversionEngine 门面
│   │   ├── errors.go            # ConversionError
│   │   ├── interface.go         # InterfaceType 枚举
│   │   └── provider.go          # TargetProvider
│   ├── provider/                # 供应商客户端
│   │   └── client.go
│   ├── repository/              # 数据访问层
│   │   ├── provider_repo.go     # 接口定义
│   │   ├── provider_repo_impl.go
│   │   ├── model_repo.go
│   │   ├── model_repo_impl.go
│   │   ├── stats_repo.go
│   │   └── stats_repo_impl.go
│   └── service/                 # 业务逻辑层
│       ├── provider_service.go       # 接口定义
│       ├── provider_service_impl.go
│       ├── model_service.go
│       ├── model_service_impl.go
│       ├── routing_service.go
│       ├── routing_service_impl.go
│       ├── stats_service.go
│       └── stats_service_impl.go
├── pkg/                         # 公共包
│   ├── errors/                  # 结构化错误
│   │   ├── errors.go
│   │   └── wrap.go
│   ├── logger/                  # 日志系统
│   │   ├── logger.go            # 核心初始化
│   │   ├── field.go             # 标准字段定义
│   │   ├── module.go            # 模块日志器
│   │   ├── context.go           # Context 辅助函数
│   │   ├── gorm.go              # GORM 适配器
│   │   ├── minimal.go           # 最小化 logger
│   │   └── rotate.go            # 日志轮转
│   ├── modelid/                 # 统一模型 ID 工具包
│   │   ├── model_id.go
│   │   └── model_id_test.go
│   └── validator/               # 验证器
│       └── validator.go
├── migrations/                  # 数据库迁移
│   └── 20260421000001_initial_schema.sql
├── tests/                       # 集成测试
│   ├── helpers.go               # 测试辅助函数
│   ├── config/                  # 测试配置
│   ├── integration/             # 集成测试
│   │   └── e2e_conversion_test.go  # E2E 协议转换测试
│   └── mocks/                   # Mock 实现
├── Makefile
├── go.mod
└── README.md
```

## 架构

采用三层架构（handler → service → repository），通过依赖注入连接：

```
handler（HTTP 请求处理）
  → service（业务逻辑）
    → repository（数据访问）
```

代理请求通过 ConversionEngine 进行协议转换：

```
Client Request (clientProtocol)
  → ProxyHandler 路由到上游 provider
    → ConversionEngine 请求转换 (clientProtocol → providerProtocol)
    → ProviderClient 发送请求
    → ConversionEngine 响应转换 (providerProtocol → clientProtocol)
  → Client Response
```

同协议时自动透传，跳过序列化开销。

## 协议转换架构

### Canonical Model 中间表示

所有协议转换都经过 Canonical Model 中间表示层，实现 Hub-and-Spoke 架构：

```
OpenAI Request → Canonical Request → Anthropic Request
                  (中间表示)
OpenAI Response ← Canonical Response ← Anthropic Response
```

**CanonicalRequest 核心字段**：
- `Model` - 统一模型 ID
- `Messages` - 消息列表（支持 text、tool_use、tool_result、thinking 类型）
- `Tools` - 工具定义
- `Thinking` - 推理配置（`budget_tokens`、`effort`）
- `Parameters` - 通用参数（`max_tokens`、`temperature`、`top_p` 等）

### Smart Passthrough 机制

同协议请求走 Smart Passthrough 路径，不进入 Canonical 全量转换：

```
1. 检测 clientProtocol == providerProtocol
2. 仅改写请求体中的 model 字段：unified_id → upstream_model_name
3. 直接转发请求到上游
4. 响应中仅改写 model 字段：upstream_model_name → unified_id
```

Smart Passthrough 保持未改写 JSON 字段的内容和类型不变，不承诺保留原始字节顺序、空白或对象字段顺序。

### 流式转换器层次

```
StreamConverter (接口)
├── PassthroughStreamConverter      # 直接透传，无任何处理
├── SmartPassthroughStreamConverter # 同协议 + 按 SSE frame 改写 data JSON model
└── CanonicalStreamConverter        # 跨协议完整转换（decode → encode）
```

### InterfaceType 枚举

| 类型 | 说明 |
|------|------|
| `CHAT` | 对话补全（chat/completions、messages） |
| `MODELS` | 模型列表 |
| `MODEL_INFO` | 模型详情 |
| `EMBEDDINGS` | 嵌入接口 |
| `RERANK` | 重排序接口 |
| `PASSTHROUGH` | 未知接口，直接透传 |

## 协议适配器特性

### OpenAI 适配器

**特有字段支持**：
- `reasoning_effort` - 映射到 Canonical Thinking 配置（`none` → 禁用，其他 → `effort`）
- `reasoning_content` - 非标准字段，映射到 Canonical thinking 块
- `max_completion_tokens` - 新字段，优先于 `max_tokens`
- `refusal` - 非标准字段，作为 text 块处理

**废弃字段兼容**：
- `functions` / `function_call` - 自动转换为 `tools` / `tool_choice`

**消息处理**：
- 合并连续同角色消息（Anthropic 不支持连续同角色）
- 工具选择映射：`any` → `required`

### Anthropic 适配器

**特有字段支持**：
- `thinking` - 推理配置（`type: enabled`、`budget_tokens`、`effort`）
- `output_config` - 结构化输出配置
- `disable_parallel_tool_use` - 禁用并行工具调用
- `container` - 工具容器字段

**不支持的功能**：
- Embeddings 接口（返回 `INTERFACE_NOT_SUPPORTED` 错误）

### 跨协议转换注意事项

| 源协议 | 目标协议 | 转换说明 |
|--------|----------|----------|
| OpenAI | Anthropic | `reasoning_effort` → `thinking`，消息角色合并 |
| Anthropic | OpenAI | `thinking` 块 → `reasoning_content`，工具选择转换 |

## 错误码

### ConversionError 错误码

| 错误码 | 说明 |
|--------|------|
| `INVALID_INPUT` | 输入数据无效 |
| `MISSING_REQUIRED_FIELD` | 缺少必填字段 |
| `INCOMPATIBLE_FEATURE` | 功能不兼容（如跨协议不支持某特性） |
| `FIELD_MAPPING_FAILURE` | 字段映射失败 |
| `TOOL_CALL_PARSE_ERROR` | 工具调用解析错误 |
| `JSON_PARSE_ERROR` | JSON 解析错误 |
| `STREAM_STATE_ERROR` | 流式状态错误 |
| `UTF8_DECODE_ERROR` | UTF-8 解码错误（流式 chunk 截断） |
| `PROTOCOL_CONSTRAINT_VIOLATION` | 协议约束违反 |
| `ENCODING_FAILURE` | 编码失败 |
| `INTERFACE_NOT_SUPPORTED` | 接口不支持（如 Anthropic Embeddings） |
| `UNSUPPORTED_MULTIMODAL` | 跨协议暂不支持多模态内容 |

### AppError 预定义错误

| 错误 | HTTP 状态码 | 说明 |
|------|-------------|------|
| `ErrModelNotFound` | 404 | 模型未找到 |
| `ErrModelDisabled` | 404 | 模型已禁用 |
| `ErrProviderNotFound` | 404 | 供应商未找到 |
| `ErrInvalidProviderID` | 400 | 供应商 ID 格式无效 |
| `ErrDuplicateModel` | 409 | 同一供应商下模型名称重复 |
| `ErrImmutableField` | 400 | 不可修改字段（如供应商 ID） |

## 运行方式

### 安装依赖

```bash
go mod download
```

### 启动服务

```bash
go run cmd/server/main.go
```

服务将在端口 9826 启动。首次启动会自动创建配置文件和运行数据库迁移。

## 配置

配置支持多种方式：配置文件、环境变量、命令行参数，优先级为：**CLI 参数 > 环境变量 > 配置文件 > 默认值**

### 配置文件

配置文件位于 `~/.nex/config.yaml`，首次启动自动生成。

```yaml
server:
  port: 9826
  read_timeout: 30s
  write_timeout: 30s

database:
  driver: sqlite               # sqlite 或 mysql
  path: ~/.nex/config.db       # SQLite 数据库文件路径
  # --- MySQL 配置（driver=mysql 时生效）---
  # host: localhost
  # port: 3306
  # user: nex
  # password: ""
  # dbname: nex
  max_idle_conns: 10
  max_open_conns: 100
  conn_max_lifetime: 1h

log:
  level: info
  path: ~/.nex/log
  max_size: 100        # MB
  max_backups: 10
  max_age: 30          # 天
  compress: true
```

### 环境变量

所有配置项都支持环境变量，使用 `NEX_` 前缀：

```bash
export NEX_SERVER_PORT=9000
export NEX_DATABASE_PATH=/data/nex.db
export NEX_LOG_LEVEL=debug

# MySQL 模式
export NEX_DATABASE_DRIVER=mysql
export NEX_DATABASE_HOST=db.example.com
export NEX_DATABASE_PORT=3306
export NEX_DATABASE_USER=nex
export NEX_DATABASE_PASSWORD=secret
export NEX_DATABASE_DBNAME=nex
```

命名规则：配置路径转大写 + 下划线 + `NEX_` 前缀（如 `server.port` → `NEX_SERVER_PORT`）。

### 命令行参数

```bash
./server --server-port 9000 --log-level debug --database-path /tmp/test.db
```

命名规则：配置路径转 kebab-case + `--` 前缀（如 `server.port` → `--server-port`）。

完整参数列表：

```
服务器:   --server-port, --server-read-timeout, --server-write-timeout
数据库:   --database-driver, --database-path, --database-host, --database-port, --database-user, --database-password, --database-dbname, --database-max-idle-conns, --database-max-open-conns, --database-conn-max-lifetime
日志:     --log-level, --log-path, --log-max-size, --log-max-backups, --log-max-age, --log-compress
通用:     --config (指定配置文件路径)
```

### 使用示例

```bash
# 默认配置
./server

# 临时修改端口
./server --server-port 9000

# 测试场景
./server --database-path /tmp/test.db --log-level debug

# Docker 部署
docker run -d -e NEX_SERVER_PORT=9000 -e NEX_LOG_LEVEL=info nex-server

# MySQL 模式
./server --database-driver mysql --database-host db.example.com --database-user nex --database-password secret --database-dbname nex

# 自定义配置文件
./server --config /path/to/custom.yaml
```

数据文件：
- `~/.nex/config.yaml` - 配置文件
- `~/.nex/config.db` - SQLite 数据库（MySQL 模式下不使用本地数据库文件）
- `~/.nex/log/` - 日志目录

**MySQL 连接说明**：MySQL 连接使用 DSN 格式: `user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=true&loc=Local`，最低支持 MySQL 8.0+。

## 测试

```bash
# 运行所有测试
make test

# 生成覆盖率报告
make test-coverage
```

## 数据库迁移

```bash
# 使用 Makefile
make migrate-up DB_PATH=~/.nex/config.db
make migrate-down DB_PATH=~/.nex/config.db
make migrate-status DB_PATH=~/.nex/config.db

# 创建新迁移
make migrate-create

# 或直接使用 goose
goose -dir migrations sqlite3 ~/.nex/config.db up
```

## API 文档

### 代理接口

使用 `/{protocol}/*path` URL 前缀路由。网关只剥离第一段协议前缀，不统一添加或移除 `/v1`；剩余 path 是协议原生 nativePath，由对应 adapter 识别和组合上游 URL。

#### OpenAI 协议

```
POST /openai/chat/completions
GET  /openai/models
POST /openai/embeddings
POST /openai/rerank
```

#### Anthropic 协议

```
POST /anthropic/v1/messages
GET  /anthropic/v1/models
```

**协议转换**：网关支持任意协议间的双向转换。客户端使用 OpenAI 协议请求，上游供应商可以是 Anthropic 协议（反之亦然）。同协议时自动透传或 Smart Passthrough，跳过 Canonical 全量转换。

**统一模型 ID**：代理请求中的 `model` 字段使用 `provider_id/model_name` 格式（如 `openai/gpt-4`），网关据此路由到对应供应商。同协议时自动改写为上游 `model_name`，跨协议时通过全量转换处理。

**base_url 约定**：
- OpenAI 供应商配置到版本路径一级，例如 `https://api.openai.com/v1`。
- Anthropic 供应商配置到域名级，例如 `https://api.anthropic.com`。

**模型提取边界**：只有 adapter 明确适配的 Chat、Embeddings、Rerank 等接口会提取 `model` 并尝试统一模型 ID 路由。未知接口不做顶层 `model` 猜测，直接按无 model 透传。

**流式透传边界**：同协议无响应 model 改写时 raw passthrough，保留 SSE frame 边界和 `[DONE]`；同协议需要改写时按 SSE frame 解析 `data` JSON，仅改写 `model`；跨协议继续使用 StreamDecoder → CanonicalStreamConverter → StreamEncoder。

**错误边界**：网关层代理错误返回 `{"error":"...","code":"..."}`。已收到上游 HTTP 响应时，非 2xx status、过滤 hop-by-hop header 后的 headers 和 body 直接透传；没有收到上游响应的连接/DNS/TLS/超时错误返回 `UPSTREAM_UNAVAILABLE`。

### 管理接口

#### 供应商管理

- `GET /api/providers` - 列出所有供应商
- `POST /api/providers` - 创建供应商
- `GET /api/providers/:id` - 获取供应商
- `PUT /api/providers/:id` - 更新供应商
- `DELETE /api/providers/:id` - 删除供应商

```json
{
  "id": "openai",
  "name": "OpenAI",
  "api_key": "sk-...",
  "base_url": "https://api.openai.com/v1",
  "protocol": "openai"
}
```

**Protocol 字段**：标识上游供应商使用的协议类型，可选值 `"openai"`（默认）、`"anthropic"`。

**base_url 说明**：
- OpenAI 协议：配置到 API 版本路径，如 `https://api.openai.com/v1`、`https://open.bigmodel.cn/api/paas/v4`
- Anthropic 协议：配置到域名，不包含版本路径，如 `https://api.anthropic.com`

**对外 URL 格式**：
- OpenAI 协议：`/{protocol}/{endpoint}`，如 `/openai/chat/completions`、`/openai/models`、`/openai/embeddings`
- Anthropic 协议：`/{protocol}/v1/{endpoint}`，如 `/anthropic/v1/messages`、`/anthropic/v1/models`

#### 模型管理

- `GET /api/models` - 列出模型（支持 `?provider_id=xxx` 过滤）
- `POST /api/models` - 创建模型
- `GET /api/models/:id` - 获取模型
- `PUT /api/models/:id` - 更新模型
- `DELETE /api/models/:id` - 删除模型

**创建请求**（id 由系统自动生成 UUID）：

```json
{
  "provider_id": "openai",
  "model_name": "gpt-4"
}
```

**响应示例**：

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "provider_id": "openai",
  "model_name": "gpt-4",
  "unified_id": "openai/gpt-4",
  "enabled": true,
  "created_at": "2026-04-21T00:00:00Z"
}
```

**统一模型 ID**：`unified_id` 字段为 `provider_id/model_name` 格式，用于代理请求的 `model` 参数。

#### 统计查询

- `GET /api/stats` - 查询统计
- `GET /api/stats/aggregate` - 聚合统计

查询参数：`provider_id`、`model_name`、`start_date`（YYYY-MM-DD）、`end_date`、`group_by`（provider/model/date）

#### 健康检查

- `GET /health` - 返回 `{"status": "ok"}`

## 开发

```bash
make build    # 构建
make lint     # 代码检查
make deps     # 整理依赖
```

环境要求：Go 1.26 或更高版本

## 公共库使用指南

### pkg/errors — 结构化错误

```go
import (
    "errors"
    pkgErrors "nex/backend/pkg/errors"
)

return pkgErrors.ErrRequestSend.WithCause(err)

var appErr *pkgErrors.AppError
if errors.As(err, &appErr) {
    // appErr.Code, appErr.HTTPStatus, appErr.Message
}
```

### pkg/logger — 日志系统

构造函数接受 `*zap.Logger` 参数，nil 时回退到 `zap.L()`：

```go
func NewMyService(repo Repository, logger *zap.Logger) *MyService {
    if logger == nil {
        logger = zap.L()
    }
    return &MyService{repo: repo, logger: logger}
}
```

### pkg/validator — 请求验证

```go
import "nex/backend/pkg/validator"

v := validator.Get()
err := v.Validate(myStruct)
```

## 编码规范

- **JSON 解析**：使用 `encoding/json` 标准库，不手动扫描字节
- **字符串拼接**：使用 `strings.Join`，不手写循环拼接
- **错误判断**：使用 `errors.Is` / `errors.As`，不使用字符串匹配（lint 强约束：errorlint 禁止 `err ==` 直接比较和 `err.(*T)` 直接断言）
- **日志使用**：通过依赖注入 `*zap.Logger`，不直接调用 `zap.L()`
- **日志 error 字段**：使用 `zap.Error(err)`，不使用 `zap.String("error", err.Error())` 手工字符串化
- **字符串分割**：使用 `strings.SplitN` 等精确分割，不使用索引切片
