认证、请求头与错误
所有 Gateway 端点用 Bearer:
Authorization: Bearer gw_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx真实的 Gateway Key 是 gw_ 加 43 个字符(32 字节 base64url)。缺失或格式不对返回 401 invalid_api_key。
Anthropic 兼容端点也接受 x-api-key。两个头同时出现且值不一致时直接判失败,返回 401 —— SDK 配了 base URL 又留着环境变量里的旧 key,就是这个场景。
Gateway Key Scope
Section titled “Gateway Key Scope”Gateway Key 按用途显式授予 Scope。旧 Key 和没有 Scope 的历史记录只拥有 inference,升级不会
自动扩大权限。
| Scope | 允许的操作 |
|---|---|
inference | 调用模型兼容端点 |
discovery | 查询公开别名列表或单个别名;同时需要 inference |
work_unit:create | 创建和关闭 Work Unit |
run:create | 创建和关闭 Run |
run:attach | 把模型请求归入一个现有 Run |
governance:read | 读取本 Project 的 Work Unit、Run 和 Outcome |
outcome:write | 按 Work Unit 冻结的 Definition 上报或修订 Outcome |
建议把 Agent 编排 Key 与业务验收 Key 分开:前者通常拥有 inference、生命周期 Scope 和
run:attach;后者只需要 outcome:write,不会因为创建了 Run 而自动获得结果写入权。
Run 归因请求头
Section titled “Run 归因请求头”OpenAI、Anthropic 和延迟 Responses 调用都通过同一个请求头归入 Run:
X-Halro-Run-ID: run_xxxxxxxxxxxxxxxxxxxxxxxxxx带此头时,Key 同时需要 inference 和 run:attach。Halro 会在 Provider I/O 前验证 Project、
Run 状态、TTL 和 Run 生命周期预算。不带该头的旧推理调用继续按原路径工作。
下表的 code 是 OpenAI 形状响应使用的字段。Anthropic 调用会得到相同的 HTTP 结果,但响应体没有
error.code,必须读取 error.type;完整映射见Gateway 错误码。
| 状态 | OpenAI error.code | 含义 |
|---|---|---|
| 400 | invalid_run_id | Run ID 格式错误 |
| 403 | run_governance_disabled | Project 未启用运行治理 |
| 403 | gateway_key_scope_denied | Key 没有 run:attach |
| 403 | run_budget_exceeded | Run 的可用预算不足,请求未到达 Provider |
| 404 | run_not_found | Run 不存在或不属于当前 Project |
| 409 | run_not_active | Run 已关闭或过期 |
| 503 | run_governance_unavailable | 无法验证运行治理权威状态,失败关闭 |
Gateway 为每个请求生成一个 req_ 加 26 个字符的 ID(16 字节随机数,小写 base32),例如
req_k9x8v5fcwde34gqbn142t2r9qw。它在处理器入口第一步就写入响应,先于鉴权和限流,
所以 401、429 在内的错误响应同样带着它;流式请求的 ID 在首个内容字节之前就已发出。
两个协议门面放的位置不同,各自对齐官方惯例:
| 端点 | 位置 |
|---|---|
OpenAI 兼容(/v1/chat/completions 等) | 响应头 X-Request-ID,不在响应体里 |
Anthropic 兼容(/v1/messages) | 响应头 request-id;错误响应体还带顶层 request_id 字段 |
响应体里的 id(chatcmpl-...)是补全对象的 ID,和请求 ID 是两回事;OpenAI 格式的错误信封
(error.message/type/param/code)里没有请求 ID,只能从响应头拿。
SDK 已经替你解析好了这个头:
# OpenAI Python SDKresponse = client.chat.completions.create(...)print(response._request_id)
# 出错时异常对象上也有except openai.APIStatusError as e: print(e.request_id)// OpenAI Node SDKconst response = await client.chat.completions.create({ ... });console.log(response._request_id);Anthropic 官方 SDK 同样通过 _request_id(Python / Node)暴露 request-id 头。
需要完整响应头时,用 SDK 的 with_raw_response(Python)/ withResponse(Node)变体。
用它来报障。 把错误路径的请求 ID 记进应用自己的日志;向 Halro 管理员反馈问题时给出这个 ID,
就能直接定位网关侧对应的记录 —— 比贴 message 文本可靠得多,message 是脱敏后的展示文案,
不保证稳定。
模型发现:GET /v1/models
Section titled “模型发现:GET /v1/models”v0.9.0 起,GET /v1/models 和 GET /v1/models/{id} 返回当前路由表中 Project 可见的公开别名。
Key 需同时有 inference 和 discovery;旧 Key 不会自动扩权。单个别名不存在或无权访问均返回 404。
owned_by 固定 halro、created 固定 0,不调用上游、不写 Ledger;列表不证明能力或健康。
这两个端点仍为 experimental。示例与限制见模型发现。
七个会返回 429 的地方
Section titled “七个会返回 429 的地方”v0.9.0 另外增加发现与资源面的固定上限:每 Key 600 次/分钟、每 Project 5,000 次/分钟,返回
429 rate_limit_exceeded 与 Retry-After;Project RPM=0 不会关闭它。
下表列出推理路径原有的七类触发点;上面的本地请求上限是额外边界。rate_limit_exceeded 可出现在鉴权前的来源限流或鉴权后的不同限制中:
| 触发点 | code | 何时发生 |
|---|---|---|
| 每源地址限流 | rate_limit_exceeded | 鉴权之前。匿名调用方能索取的工作量本身是有界的 |
| Project 每分钟请求数 | rate_limit_exceeded | 鉴权之后,管理员在 Project 上配置 |
| Project 每分钟 token 数 | token_rate_limit_exceeded | 同上 |
| Project 并发 | concurrency_limit_exceeded | 同上 |
| Deployment 并发 | deployment_concurrency_limit_exceeded | 管理员在 Deployment 上配置 |
| 全部可选 Provider 都到并发上限 | provider_concurrency_limit_exceeded | 同上 |
| 上游服务商限流 | provider_rate_limit | 上游,Halro 透传 |
Token Guard 不在这张表里。它拒绝时返回 403 token_guard_blocked,可能是本次尝试超过成本
上限,也可能是异常使用触发的 temporary block。前者需要调整请求、模型或策略;后者等待 block
TTL 或由管理员处置。按 429 立即重试会形成无效循环。
Project/Key 隔离见案例一, Token Guard 调优和预算拒绝见案例三。
Retry-After 不是每次都有
Section titled “Retry-After 不是每次都有”Retry-After 只在能算出一个正数等待时长时才写入。每源限流与 Project 限流会带上它;Deployment 并发那条不设置;上游透传的 429 只在上游自己带了可解析的头时才有。客户端必须处理这个头缺失的情况,不能无条件读取。
当前源码基线中公开 Gateway 数据面与 Run Governance 的 code × HTTP 状态 × 处理方式,以及
Anthropic 门面的 error.type 映射,见Gateway 错误码与处理方式。其中 price_unavailable、
sensitive_data_detected 和 unsupported_feature 会因触发阶段不同对应多个 HTTP 状态,客户端不能只按
状态码决定是否重试。