网关工作原理
同一个域名原生提供 Anthropic、OpenAI 和 Gemini 三种 API。端点、base URL、模型 ID 写法、鉴权方式,以及 Half Off Dev 会改动请求的哪些地方。
用 Half Off Dev 的前提是:你现有的代码一行都不用重写。Claude、GPT、Gemini 各自用厂商原生的协议格式,在同一个域名下提供,价格是官方标价的一半。SDK 发出的 JSON 和直连厂商时完全一样,要换的只有 base URL 和密钥。
中间没有任何格式转换层。Anthropic 格式的请求交给支持 Anthropic 格式的渠道,OpenAI 格式的请求交给支持 OpenAI 格式的渠道,依此类推。除了两处很小的改动,请求体原样到达渠道,所以 thinking、cache_control、tools、generationConfig 这类厂商参数的行为,和各家官方文档里写的一致。
端点一览#
把下表里的 base URL 交给 SDK,路径由 SDK 自己拼。
| 协议格式 | 路径 | SDK 的 base URL | 指南 |
|---|---|---|---|
| Anthropic Messages | POST /v1/messages、POST /v1/messages/count_tokens | https://halfoff.dev | Anthropic Messages |
| OpenAI Chat Completions | POST /v1/chat/completions | https://halfoff.dev/v1 | OpenAI |
| OpenAI Responses(仅 GPT) | POST /v1/responses | https://halfoff.dev/v1 | Responses API |
| Gemini | POST /v1beta/models/{model}:generateContent、:streamGenerateContent、:countTokens | https://halfoff.dev | Gemini |
| 模型目录 | GET /v1/models、GET /v1beta/models | 价格 |
表外的端点均不提供,详见不能调用的接口。
密钥放在哪里#
每个端点都会在四个位置查找密钥,因此各家 SDK 用默认鉴权方式即可:
| 位置 | 常见发送方 |
|---|---|
Authorization: Bearer ho-... | OpenAI SDK、通过 ANTHROPIC_AUTH_TOKEN 配置的 Claude Code、普通 HTTP 客户端 |
x-api-key: ho-... | Anthropic SDK |
x-goog-api-key: ho-... | Google Gen AI SDK、Gemini CLI |
?key=ho-... | Gemini 的 REST 示例 |
两个模型目录端点是公开的,无需密钥。密钥的创建、限额和轮换见身份验证。
各模型家族的原生格式#
每个模型家族都是围绕一种 API 设计的,用它的原生格式调用,才能用全厂商提供的功能。
| 模型家族 | 原生格式 | 目前也接受 |
|---|---|---|
Claude(claude-*) | Anthropic Messages | OpenAI Chat Completions、Gemini |
GPT(gpt-*) | OpenAI Chat Completions、OpenAI Responses | Anthropic Messages、Gemini |
Gemini(gemini-*) | Gemini | OpenAI Chat Completions、Anthropic Messages |
用非原生格式调用时,渠道会把请求转换成模型能接受的形式,厂商独有的参数可能在转换中被忽略或被拒绝。Anthropic 的 cache_control 和 thinking、OpenAI 的 reasoning_effort、Gemini 的 thinkingConfig,只有在原生格式下才保证生效。
一个模型接受哪些格式,取决于为它服务的渠道,因此会有变化。模型广场列出了每个模型当前支持的格式。如果没有任何渠道接受你用的格式,会返回 404,消息类似 Model 'gpt-6.1-sol' is not served in gemini format in any group.
模型 ID#
标准 ID 以 GET /v1/models 返回的为准,例如 claude-opus-5.5、gpt-6.1-sol、gemini-3-flash。各种工具有自己的写法,网关会先做归一化,所以照样能找到正确的模型:
| 发送的 ID | 实际对应 |
|---|---|
Claude-Opus-5-5 | claude-opus-5.5 |
anthropic/claude-opus-5.5 | claude-opus-5.5 |
claude-haiku-4-5-20251001 | claude-haiku-4.5 |
gemini-3.1-pro-preview | gemini-3.1-pro |
models/gemini-3-flash | gemini-3-flash |
归一化规则:转成小写;去掉开头的 vendor/ 或 models/;. 和 - 视为同一个字符;去掉末尾的日期(-20251001)、-preview 或 -latest。计费、日志和密钥的模型白名单一律记录标准 ID。
找不到对应模型的 ID 返回 404:Model '<id>' is not available. See GET /v1/models.
一个请求的完整路径#
- 校验密钥。 密钥未启用或已过期,返回
401。 - 解析模型。 按上面的规则归一化 ID,再对照密钥的模型白名单;不在白名单内返回
403。 - 检查余额。 余额小于等于零,或密钥已用完消费上限,返回
402。 - 密钥限额。 执行密钥的每分钟请求数和并发数限制(
429),详见速率限制与故障转移。 - 选择渠道。 进行中的对话回到持有其提示缓存的渠道;新对话进入所在渠道分组里状态最好的渠道。详见提示缓存。
- 调用模型。 渠道在产生任何输出之前失败,就换一个渠道,最多尝试三次。流式响应在第一个 token 到达前会被暂扣,所以切换过程你看不到。详见流式输出。
- 计量。 按响应中的 token 数、以官方标价的一半计费,几秒内从余额扣除。详见价格。
网关会改动什么#
请求体只有两处改动,此外一概不动:
model字段。 网关会替换成服务渠道所需的写法。因此响应里的model可能是厂商的写法,比如claude-opus-5-5。- OpenAI 流式响应里的用量。 所有流式 Chat Completions 请求都会被加上
stream_options.include_usage: true,以便计费。所以流的末尾会多出一个 chunk,它的choices为空数组,并带有usage。读取 chunk 时请处理choices为空的情况。
你发送的请求头都不会转发,只有三个例外:anthropic-version、anthropic-beta、openai-beta。未带 anthropic-version 时,网关补上 2023-06-01。Gemini 路径上的查询参数(如 alt=sse)会转发,key 则被移除。
响应体逐字节原样返回。content-type 和 cache-control 会保留,网关再加上自己的响应头;厂商的请求 ID 和限流相关响应头不会透传。
如果模型认为请求本身无效(400、404、413、422),你会按所调用 API 的错误格式收到同样的状态码和原因。其他失败会先在另一个渠道上重试。详见速率限制与故障转移和错误。
每个响应都带的响应头#
| 响应头 | 内容 |
|---|---|
x-halfoff-request-id | 本次调用的 ID,如 req_6f1c...,对应日志里的那条记录。联系客服时请附上。 |
x-halfoff-group | 实际响应的渠道分组,如 gpt-value。 |
x-halfoff-channel | 该分组的渠道标签,经过 URL 编码,如 Pooled%20capacity。 |
x-halfoff-queued-ms | 仅在调用等待过容量时出现:等待的毫秒数。 |
前三个已列入 access-control-expose-headers,前端 JavaScript 可以直接读取。
不能调用的接口#
上表就是模型相关的全部端点。以下这些明确不提供:
- OpenAI:已存储的 response(
/v1/responses/{id})、旧版 Completions、embeddings、音频、文件和批处理。Responses API 本身只支持 GPT 模型。 - Anthropic:Message Batches、Files,以及按 ID 获取单个模型。
- Gemini:
cachedContents(显式上下文缓存)、文件上传、embedContent和 Live API。
具体客户端怎么配置,见接入工具。