From 98c3187b4b829230aba3dadfe8af804cc13f4d8f Mon Sep 17 00:00:00 2001 From: Camy <209573702+camy-x@users.noreply.github.com> Date: Mon, 14 Sep 2026 16:33:40 +0800 Subject: [PATCH] docs(plugin): clarify executor error boundaries --- examples/plugin/README.md | 8 +++++--- examples/plugin/README_CN.md | 8 +++++--- sdk/pluginabi/types.go | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/examples/plugin/README.md b/examples/plugin/README.md index 2d3065c3b85..df8dcce9071 100644 --- a/examples/plugin/README.md +++ b/examples/plugin/README.md @@ -110,14 +110,16 @@ plugins: When a plugin executor encounters an upstream failure (such as `401 Unauthorized` for invalid credentials, `403 Forbidden` for model permission/quota limits, or `429 Too Many Requests` for rate limits), it should report the HTTP status code in the error envelope: - In the JSON RPC error envelope, set the `http_status` field inside the `error` object (`pluginabi.Error.HTTPStatus`). -- If `http_status` is omitted or `0`, CPA defaults to returning HTTP `500 Internal Server Error` (`server_error` / `internal_server_error`), which clients typically treat as a temporary gateway outage and retry with backoff. -- When `http_status` is set, CPA maps the status into client-visible error responses: +- If `http_status` is omitted or `0`, an executor failure defaults to HTTP `500 Internal Server Error`. +- For a plain-text message on OpenAI-compatible handlers, CPA maps the status into a client-visible error response: - `401` -> HTTP 401 with `type: "authentication_error"`, `code: "invalid_api_key"` - `403` -> HTTP 403 with `type: "permission_error"`, `code: "insufficient_quota"` - `429` -> HTTP 429 with `type: "rate_limit_error"`, `code: "rate_limit_exceeded"` - `404` -> HTTP 404 with `type: "invalid_request_error"`, `code: "model_not_found"` - `>=500` -> HTTP 5xx with `type: "server_error"`, `code: "internal_server_error"` -- **Important**: Both non-streaming (`executor.execute`) and streaming (`executor.execute_stream`) call sites must include `http_status` so failures are classified consistently. +- Error-body handling is protocol-specific. Structured and streaming handlers may forward, normalize, or sanitize the message, so plugins should not rely on byte-for-byte preservation of the body. +- For streaming, wait until the upstream accepts or rejects the initial request before returning success from `executor.execute_stream`. After success, asynchronous `host.stream.emit` and `host.stream.close` calls can carry only a string error, not an HTTP status; later failures therefore use HTTP 500 even when no output has reached the client. Once headers or chunks reach the client, the HTTP status cannot change in any case. +- `error.retryable` is currently informational. The host does not use it for executor retry decisions. - Because native dynamic library plugins communicate across the C ABI via serialized JSON buffers, the status code must be encoded in the serialized JSON envelope (e.g. using `pluginabi.NewErrorEnvelope` or a custom envelope struct with an `http_status` field). Returning an unmarshaled Go error does not traverse the C ABI boundary. Go plugins can import `github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi` and use `pluginabi.NewErrorEnvelope(code, message, httpStatus)`: diff --git a/examples/plugin/README_CN.md b/examples/plugin/README_CN.md index a3f6d1963c6..d964acdb68b 100644 --- a/examples/plugin/README_CN.md +++ b/examples/plugin/README_CN.md @@ -109,14 +109,16 @@ plugins: 当插件执行器(Executor)遇到上游调用失败(如因凭据无效返回 `401 Unauthorized`、因模型权限或配额限制返回 `403 Forbidden`,或因限流返回 `429 Too Many Requests`)时,应在错误信封中设置 HTTP 状态码: - 在 JSON RPC 错误信封中,设置 `error` 对象内的 `http_status` 字段(对应 `pluginabi.Error.HTTPStatus`)。 -- 若省略 `http_status` 或设置为 `0`,CPA 会默认将错误降级为 HTTP `500 Internal Server Error`(`server_error` / `internal_server_error`),导致客户端将其误判为网关故障并进行退避重试。 -- 显式设置 `http_status` 后,CPA 会将状态码正确映射为结构化的客户端错误响应: +- 若省略 `http_status` 或设置为 `0`,执行器失败会默认使用 HTTP `500 Internal Server Error`。 +- 对于 OpenAI 兼容接口中的纯文本消息,CPA 会按状态码生成客户端错误响应: - `401` -> HTTP 401,`type: "authentication_error"`,`code: "invalid_api_key"` - `403` -> HTTP 403,`type: "permission_error"`,`code: "insufficient_quota"` - `429` -> HTTP 429,`type: "rate_limit_error"`,`code: "rate_limit_exceeded"` - `404` -> HTTP 404,`type: "invalid_request_error"`,`code: "model_not_found"` - `>=500` -> HTTP 5xx,`type: "server_error"`,`code: "internal_server_error"` -- **注意**:非流式执行(`executor.execute`)和流式执行(`executor.execute_stream`)两处报错路径均需要设置 `http_status`,以保证异常分类行为一致。 +- 错误正文的处理方式取决于具体协议。结构化和流式处理器可能会转发、规整或脱敏消息,因此插件不应依赖错误正文逐字节不变。 +- 对于流式请求,插件应等到上游接受或拒绝初始请求后再从 `executor.execute_stream` 返回成功。返回成功后,异步的 `host.stream.emit` 和 `host.stream.close` 只能携带字符串错误,不能携带 HTTP 状态码;因此,即使尚未向客户端发送数据,后续错误也会使用 HTTP 500。响应头或数据块一旦到达客户端,HTTP 状态码在任何情况下都无法再更改。 +- `error.retryable` 当前只是描述信息,宿主不会用它决定是否重试执行器。 - 原生动态库插件通过 C ABI 交换序列化的 JSON 缓冲区通信,因此状态码必须编码到序列化的 JSON 信封中(例如使用 `pluginabi.NewErrorEnvelope` 或自定义带 `http_status` 字段的信封结构体)。Go 语言原生的 error 对象无法跨越 C ABI 边界传递。 Go 语言编写的插件可导入 `github.com/router-for-me/CLIProxyAPI/v7/sdk/pluginabi` 并直接使用 `pluginabi.NewErrorEnvelope(code, message, httpStatus)`: diff --git a/sdk/pluginabi/types.go b/sdk/pluginabi/types.go index 11f28fc91ca..dd2fba210fc 100644 --- a/sdk/pluginabi/types.go +++ b/sdk/pluginabi/types.go @@ -121,7 +121,8 @@ type Error struct { Message string `json:"message"` Retryable bool `json:"retryable,omitempty"` // HTTPStatus is the HTTP status code (e.g. 401, 403, 429) to surface to the client. - // When omitted or 0, CPA defaults to HTTP 500 (internal_server_error). + // When omitted or 0, executor failures default to HTTP 500. Error-body handling + // remains protocol-specific. HTTPStatus int `json:"http_status,omitempty"` }