AI网关新增TTS/重排/审核端点,xAI工具扩展,swagger修缺
CI / test (push) Successful in 32s
Release / release (push) Successful in 1m4s

- 新端点 /ai/v1/audio/speech(xai.grok-tts)、/rerank(cohere.rerank-v4)、/moderations(OCI Guardrails)
- Responses 放行 code_interpreter 与远程 mcp 工具,web_search/x_search 解除仅非流式限制
- 模型能力映射扩展:TEXT_RERANK→RERANK、TEXT_TO_AUDIO→TTS
- AI 网关文档独立 docs/ai-gateway.md,字段兼容矩阵只列支持项;README 精简引用
- swagger 修缺:135 处响应注解具体化,RawMessage/联合类型统一渲染 AnyJSON,overrides 迁至 docs/.swaggo
- CHANGELOG 0.4.0,版本段不再记日期;DASH_VERSION v0.4.0
This commit is contained in:
2026-07-13 20:17:06 +08:00
parent 9309ad1ffc
commit 0a86b5a291
50 changed files with 10699 additions and 1371 deletions
+8 -194
View File
@@ -197,206 +197,20 @@ server {
## AI 网关
AI 网关使用面板创建的独立密钥鉴权,支持 `Authorization: Bearer sk-...` `x-api-key: sk-...`。密钥可绑定渠道分组和模型白名单;全局模型黑名单会从模型列表、路由和探测候选中同时排除目标模型
面板内置 OpenAI / Anthropic 兼容的 GenAI 网关:独立密钥鉴权`Authorization: Bearer sk-...` `x-api-key`),支持渠道分组、加权路由、熔断探测、模型黑白名单、内容日志与调用日志
| 端点 | 定位 | 流式 |
| --- | --- | --- |
| `POST /ai/v1/responses` | OpenAI Responses,无状态主接口 | SSE;服务端工具除外 |
| `POST /ai/v1/chat/completions` | OpenAI Chat Completions,存量客户端兼容层 | SSE |
| `POST /ai/v1/responses` | OpenAI Responses,无状态主接口xAI 服务端工具 / MCP | SSE |
| `POST /ai/v1/chat/completions` | OpenAI Chat Completions 兼容层 | SSE |
| `POST /ai/v1/messages` | Anthropic Messages 转换层 | SSE |
| `POST /ai/v1/embeddings` | OpenAI Embeddings | 否 |
| `POST /ai/v1/audio/speech` | 文本转语音(xAI Voice | 否 |
| `POST /ai/v1/rerank` | 文档重排(Cohere Rerank | 否 |
| `POST /ai/v1/moderations` | 内容审核(OCI Guardrails | 否 |
| `GET /ai/v1/models` | 当前密钥可见的模型列表 | 否 |
兼容边界
- 对话请求统一转发 OCI OpenAI 兼容面,当前供给以 `xai.`、`meta.`、`openai.` 前缀模型为主;Cohere Embeddings 不受该对话模型范围影响
- 网关不保存会话历史,客户端需要携带完整上下文;Responses 拒绝非空 `previous_response_id`、非 `null` `conversation` 和 `background:true`
- Responses 支持 xAI Grok `web_search` / `x_search` 服务端工具,但仅限非流式请求
- Responses 的 `reasoning.effort`、Messages 的 `output_config.effort` 和 Chat Completions 的 `reasoning_effort` 会传给上游,实际档位和效果由模型决定
- Chat Completions 只承担协议转换与兼容修复;新能力优先在 Responses 和 Messages 提供
- 单次请求最多尝试三个渠道;可重试错误会切换渠道,流式响应建立后不会换渠道重试
### 已知上游限制:大 system 区流式断流
实测(2026-07-13OCI 兼容面对 `instructions` 与 `tools` 合计超约 64.5KB 的**流式**请求会在发出少量事件后静默断开连接(无任何错误事件;同请求非流式正常),与模型、字符集、消息正文大小均无关——消息正文(`input`)不计入该限制。Chat Completions 与 Messages 的 system/developer 提示会转换为 `instructions`,因此 Claude Code 等自带大体量系统提示与工具定义的客户端极易触发。
网关侧应对:
- Messages 与 Chat Completions 的流式请求在客户端尚未收到任何输出时遭遇上游断流,会自动降级为非流式重做,并按标准事件/chunk 序列一次推送;调用日志记 `retries=1` 与降级标记
- Responses 直通因初始事件已转发、协议上无法透明降级,调用日志记「上游流提前终止」,客户端需自行回退非流式
- 应急规避:将超长 system 内容移入首条 user 消息正文可绕过该限制(正文不计入),但语义有别,根治有待上游修复
这里提供的是兼容接口而非 OpenAI / Anthropic 协议的完整实现。OCI OpenAI 兼容面的部分行为来自实测,未见 Oracle 文档承诺,可能随上游调整。路由与鉴权定义以 [Swagger YAML](docs/swagger.yaml) 或运行时 Swagger UI 为准;无法由 OpenAPI 完整表达的兼容边界列于上方。
### 字段兼容矩阵
以下矩阵以 2026-07-13 的 [OpenAI Responses](https://developers.openai.com/api/reference/resources/responses/methods/create)、[Chat Completions](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create)、[Embeddings](https://developers.openai.com/api/reference/resources/embeddings/methods/create) 和 [Anthropic Messages](https://platform.claude.com/docs/en/api/messages/create) 为标准基线,并与当前实现逐项核对。
这是一份兼容性快照,不替代 Swagger。标准接口和 OCI 上游都可能变化,最终行为以当前版本代码与实测为准。
| 标记 | 含义 |
| :---: | --- |
| ✅ | 网关直接支持 |
| ➡️ | 网关原样透传;是否生效由 OCI 上游决定 |
| 🔄 | 网关进行字段或协议转换后支持 |
| ◐ | 部分支持、存在前置条件或语义降级 |
| ⚠️ | 请求可被接受,但字段会被忽略 |
| ❌ | 网关在请求到达上游前拒绝 |
<details>
<summary><code>POST /ai/v1/responses</code> 对比 OpenAI Responses</summary>
Responses 是“原始 JSON 直通 + 本地门禁”。除 `store` 外,网关不会重建请求体;未知顶层字段也会保留并送往 OCI。
| 标准字段 | 状态 | 网关行为 |
| --- | :---: | --- |
| `model` | ◐ | 必须非空,还要通过密钥模型白名单并匹配可用渠道;随后原样透传 |
| `input` | ➡️ | 支持标准的字符串或 item 数组,原始内容透传;网关不逐项保证 OCI 能处理所有 item 类型 |
| `instructions`、`max_output_tokens` | ➡️ | 类型可解析后原样透传,不做范围或模型能力校验 |
| `temperature`、`top_p`、`parallel_tool_calls` | ➡️ | 原样透传,不做取值范围校验 |
| `text` / `text.format` / `text.verbosity` | ➡️ | 整个原始对象透传;结构化输出是否可用由 OCI 模型决定 |
| `reasoning` | ➡️ | 整个原始对象透传;`effort` 不校验档位,`summary` 等字段不会被网关删除 |
| `tool_choice` | ➡️ | 任意 JSON 原样透传,本地不校验枚举或结构 |
| `context_management`、`include`、`max_tool_calls`、`metadata`、`moderation`、`prompt` | ➡️ | 网关未建模但会保留在原始请求中,由 OCI 决定是否接受 |
| `prompt_cache_key`、`prompt_cache_retention`、`safety_identifier`、`service_tier` | ➡️ | 原样透传,不代表 OCI 一定实现 OpenAI 的对应语义 |
| `stream_options`、`top_logprobs`、`truncation`、`user` | ➡️ | 原样透传,不做本地语义校验 |
| `store` | 🔄 | 无论客户端传什么,上游请求都强制改写为 `false` |
| `previous_response_id` | ❌ | 非空即返回 400;网关不保存历史响应 |
| `conversation` | ◐ | 省略或 `null` 可通过;任何非 `null` 值返回 400 |
| `background` | ◐ | `true` 返回 400`false`、`null` 或省略时继续透传 |
| `stream` | ◐ | 普通请求和 `function` 工具支持 SSE;含 `web_search` / `x_search` 时拒绝流式 |
| `tools[].type=function` | ➡️ | 非流式和流式均可,工具对象原样透传 |
| `tools[].type=web_search` / `x_search` | ◐ | 使用 xAI 服务端工具语义且仅支持非流式;`x_search` 不是 OpenAI 标准工具 |
| 其他标准工具类型 | ❌ | `web_search_preview`、`file_search`、`computer`、`code_interpreter`、`image_generation`、`mcp`、`shell`、`custom` 等返回 400 |
| 其他未知顶层字段 | ➡️ | 只要整个请求是合法 JSON,字段名、结构和数值都会保留 |
响应边界:
- 非流式成功响应不做转换,OCI JSON 原样返回;usage 解析只用于面板调用日志
- SSE 事件逐行原样转发,不补 `data: [DONE]`,推理增量也不会被网关过滤
- 流建立前最多切换三个渠道;流建立后中断不重试,客户端可能只收到部分事件
- 未知模型返回 404、无渠道返回 503;上游错误会套入 OpenAI 风格错误体,不保证与标准 OpenAI 错误字段完全相同
实现依据:[`airesponses.go`](internal/service/airesponses.go) · [`responses.go`](internal/aiwire/responses.go) · [`aigateway.go`](internal/api/aigateway.go)
</details>
<details>
<summary><code>POST /ai/v1/chat/completions</code> 对比 OpenAI Chat Completions</summary>
Chat Completions 会先转换为 Responses 请求,再把 OCI Responses 响应桥接回 Chat Completions 形态。
| 标准字段 | 状态 | 网关行为 |
| --- | :---: | --- |
| `model` | ◐ | 必填,受密钥白名单和可用渠道限制;模型名保留到上游请求 |
| `messages` | 🔄 | 必填并转换为 Responses `input` / `instructions` |
| `system` / `developer` 消息 | ◐ | 文本按出现顺序合并为 `instructions`;块数组中的非文本内容被忽略 |
| `user` / `assistant` 文本内容 | 🔄 | 字符串及 `text` 块分别转为 `input_text` / `output_text` |
| `image_url` 内容块 | ◐ | URL 或 data URI 转为 `input_image``image_url.detail` 被忽略 |
| `input_audio`、`file`、`refusal` 等内容块 | ❌ | 当前消息转换器不支持,返回 400 |
| assistant `tool_calls` / `role=tool` | 🔄 | 转为 `function_call` / `function_call_output`,保留调用 ID、函数名和参数 |
| 消息 `name`、`refusal`、`audio`、旧式 `function_call` | ⚠️ | 当前消息结构未建模,静默忽略 |
| `max_completion_tokens` | 🔄 | 转为 `max_output_tokens`,优先于 `max_tokens` |
| `max_tokens` | 🔄 | 未提供 `max_completion_tokens` 时转为 `max_output_tokens` |
| `temperature`、`top_p`、`parallel_tool_calls` | ◐ | 原值写入 Responses 请求,但不校验范围或模型能力 |
| `stream` | 🔄 | OCI Responses SSE 桥接为 `chat.completion.chunk`,末尾补 `data: [DONE]`;上游断流且尚无输出时自动降级非流式重做,结果按 chunk 序列一次推送 |
| `stream_options.include_usage` | 🔄 | 控制网关在终块后追加 `choices: []` 的 usage 块 |
| `stream_options.include_obfuscation` | ⚠️ | 未建模,静默忽略 |
| `tools[].type=function` | ◐ | `name`、`description`、`parameters` 支持;`function.strict` 被忽略 |
| `tools[].type=custom` 及其他工具 | ❌ | 只接受 `function`,其他类型返回 400 |
| `tool_choice` | ◐ | 支持 `auto` / `none` / `required` 和具名 function;非法或未知值被静默忽略 |
| `response_format` | ◐ | `json_object`、`json_schema` 转为 Responses `text.format`;未知类型交给 OCI 处理 |
| `reasoning_effort` | ◐ | 转小写后映射为 `reasoning.effort`,不校验模型或档位 |
| `store` | ⚠️ / 🔄 | 客户端字段被忽略,转换后的上游请求始终使用 `store:false` |
| `stop`、`seed`、`n`、`frequency_penalty`、`presence_penalty` | ⚠️ | 静默忽略;`n` 因此恒为单个 choice |
| `logprobs`、`top_logprobs`、`logit_bias`、`user` | ⚠️ | 静默忽略 |
| `audio`、`modalities`、`prediction`、`metadata`、`moderation` | ⚠️ | 静默忽略 |
| `prompt_cache_key`、`safety_identifier`、`service_tier`、`verbosity`、`web_search_options` | ⚠️ | 静默忽略 |
| 其他未知字段 | ⚠️ | JSON 解码器不会拒绝未知字段,但转换后的上游请求不包含它们 |
响应边界:
- 非流式固定生成一个 `choices[0]`;文本会合并,函数调用转为 `tool_calls`
- `finish_reason` 只生成 `stop`、`tool_calls`、`length`;其他上游终止原因不保留
- reasoning 输出、logprobs、refusal、annotations、audio、service tier 和 system fingerprint 不返回
- usage 保留 `prompt_tokens`、`completion_tokens`、`total_tokens` 和 `prompt_tokens_details.cached_tokens`
- 已建立的流中断或上游 `response.failed` 不会转换成标准 SSE 错误事件
实现依据:[`chatresponses.go`](internal/service/chatresponses.go) · [`openai.go`](internal/aiwire/openai.go) · [`aigateway.go`](internal/api/aigateway.go)
</details>
<details>
<summary><code>POST /ai/v1/embeddings</code> 对比 OpenAI Embeddings</summary>
| 标准字段 | 状态 | 网关行为 |
| --- | :---: | --- |
| `model` | ◐ | 必填,受密钥白名单限制,并且必须存在具有 `EMBEDDING` 能力的渠道 |
| `input` 为字符串 | ✅ | 包装为单个输入后调用 OCI |
| `input` 为字符串数组 | ✅ | 按原顺序调用 OCI |
| `input` 为 token ID 数组或二维 token ID 数组 | ❌ | 只能解码字符串或字符串数组,绑定阶段返回 400 |
| 空数组 / `null` | ❌ | 本地返回 400;空字符串不在本地拒绝,由 OCI 决定 |
| `dimensions` | ◐ | 映射为 OCI 输出维度,不做范围或模型能力校验 |
| `encoding_format=float` | ✅ | 返回 float 数组;省略时行为相同 |
| `encoding_format=base64` | ❌ | 本地返回 400,不提供 base64 响应 |
| `user` | ⚠️ | 能解析但不会传给 OCI |
| 其他未知字段 | ⚠️ | 静默忽略 |
响应使用标准的 `object:"list"`、`data[].object:"embedding"`、`index`、`model` 和可选 `usage` 外壳;向量为 `float32` 数组,不支持流式。
实现依据:[`embeddings.go`](internal/aiwire/embeddings.go) · [`aigateway_chat.go`](internal/service/aigateway_chat.go) · [`aigateway.go`](internal/api/aigateway.go)
</details>
<details>
<summary><code>POST /ai/v1/messages</code> 对比 Anthropic Messages</summary>
网关接受 `Authorization: Bearer` 或 `x-api-key`,但不会校验或使用标准 Anthropic `anthropic-version`、`anthropic-beta` 请求头。
| 标准字段 | 状态 | 网关行为 |
| --- | :---: | --- |
| `model` | ◐ | 用于模型与渠道选择,但空字符串不会在 handler 中按参数错误拒绝,通常最终返回模型不存在 |
| `max_tokens` | 🔄 | 可缺省(缺省或 ≤0 时按默认值 8192),转换为 `max_output_tokens` |
| `messages` | ◐ | 必须非空;角色、交替顺序和空内容不做完整校验 |
| `system` | ◐ | 支持字符串或 text 块数组;多个文本块直接拼接,`cache_control` 等附加字段被忽略 |
| `temperature`、`top_p` | ◐ | 写入 Responses 请求,不做取值范围或模型能力校验 |
| `top_k` | ⚠️ | 能解析但不会传给上游 |
| `stop_sequences` | ⚠️ | 能解析但不会传给上游;响应 `stop_sequence` 恒为 `null` |
| `stream` | 🔄 | Responses SSE 桥接为 Anthropic 事件序列;上游断流且尚无输出时自动降级非流式重做,结果按事件序列一次推送 |
| `tools` | ◐ | 每个工具都转换成 Responses `function`;自定义客户端工具可用,Anthropic 服务端工具类型不保留原语义 |
| `tool_choice` | ◐ | 支持 `auto`、`any`、`none`、具名 `tool``disable_parallel_tool_use` 等附加字段被忽略 |
| `metadata` | ⚠️ | 能解析但不会传给上游 |
| `thinking` | ⚠️ | 顶层 thinking 配置不会控制上游思考预算 |
| `output_config.effort` | 🔄 | 转小写后映射为 Responses `reasoning.effort` |
| `output_config` 其他子字段 | ⚠️ | 未建模,静默忽略 |
| `cache_control`、`container`、`inference_geo`、`service_tier` | ⚠️ | 标准 SDK 中存在,但当前请求结构未建模,静默忽略 |
| 其他未知顶层字段 | ⚠️ | JSON 解码器接受,但转换后的上游请求不包含它们 |
`messages[].content`
| 标准内容块 | 状态 | 网关行为 |
| --- | :---: | --- |
| 字符串 / `text` | 🔄 | user 转 `input_text`assistant 历史转 `output_text` |
| `image` | ◐ | 仅支持 `base64` 和 `url` source;缺字段或其他 source 类型返回 400 |
| `tool_use` | 🔄 | 转为 `function_call`,保留 ID、名称和输入 |
| `tool_result` | ◐ | 转为 `function_call_output`;块数组只拼接 text`is_error` 和非文本结果丢失 |
| `thinking` / `redacted_thinking` | ⚠️ | 历史思考块被删除,不进入上游上下文 |
| `document`、服务端工具结果及其他未知块 | ❌ | 返回 400 |
| `null` / 空块数组 | ◐ | 该消息可能从上游 input 中消失,不返回参数错误 |
响应边界:
- 非流式只把 Responses `output_text` 转成 `text`、`function_call` 转成 `tool_use`
- `stop_reason` 只生成 `end_turn`、`tool_use`、`max_tokens``stop_sequence` 恒为 `null`
- reasoning 不会生成 Anthropic `thinking` / `redacted_thinking` 块,也没有 signature
- usage 只保留 `input_tokens`、`output_tokens` 和 `cache_read_input_tokens`,不提供 `cache_creation_input_tokens`
- 流式输出标准事件骨架,但不生成 `thinking_delta` 和 `signature_delta`;上游错误事件与无终态断流会转成 Anthropic `error` 事件
实现依据:[`anthresponses.go`](internal/service/anthresponses.go) · [`anthropic.go`](internal/aiwire/anthropic.go) · [`aigateway.go`](internal/api/aigateway.go)
</details>
`GET /ai/v1/models` 使用 OpenAI Models 列表外壳(`object`、`data[].id/object/created/owned_by`),但只返回当前渠道目录中通过分组、全局黑名单和密钥白名单筛选后的模型;网关不提供标准的单模型检索端点。
协议兼容边界、已知上游限制与逐字段兼容矩阵见 **[AI 网关文档](docs/ai-gateway.md)**。
## API 与配置
@@ -446,7 +260,7 @@ go vet ./...
go test ./...
# Handler 注释变更后重新生成唯一的对外 API 文档。
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency
go tool swag init -g cmd/server/main.go -o docs --parseInternal --parseDependency --overridesFile docs/.swaggo
```
- Go 后端规范与目录约定:[`AGENTS.md`](AGENTS.md) · [`.trellis/spec/backend/`](.trellis/spec/backend/)