-
Notifications
You must be signed in to change notification settings - Fork 29
docs: add api-proxy.md (OpenAI-compatible client surface) #24
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,288 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| # Proxy API | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| The Proxy API is the public, OpenAI-compatible HTTP surface that | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| caller traffic targets. It is served on the proxy listener (default | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `:3000`). For the operator CRUD surface see | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [`api-admin.md`](./api-admin.md). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > **Compatibility goal**: any client SDK that targets OpenAI | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > (`openai` Python, `openai-node`, `openai-go`, `instructor`, the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > Anthropic SDK against `/v1/messages`, etc.) should work unchanged | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| > by repointing `base_url` at aisix. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## 1. Authentication | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Every endpoint requires a caller API key, presented as either | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `Authorization: Bearer <key>` (preferred) or `Authorization: <key>` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| (bare-key fallback for legacy SDKs). The key must exist in the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `apikeys` table of the current snapshot. See | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| [architecture.md §3](./architecture.md#3-configuration-data-plane). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```http | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization: Bearer sk-aisix-… | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ``` | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Authorization is a *separate* check: the resolved `ApiKey` must list | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| the requested Model in its `allowed_models` array (or contain the | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| `"*"` wildcard). | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## 2. Error envelope | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Errors follow the OpenAI shape so SDK error handlers light up: | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ```json | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "error": { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "message": "model 'mygpt' not found", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "model_not_found", | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "param": null, | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "code": null | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+37
to
+39
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| "type": "model_not_found", | |
| "param": null, | |
| "code": null | |
| "type": "model_not_found" |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The type tokens in this status table don't match what the proxy actually emits. For example 401s map to invalid_api_key (not authentication_error), and 403s map to permission_denied (not model_access_forbidden). Consider deriving this table directly from aisix-proxy::ProxyError::kind() to keep docs and behavior in sync.
| | 401 | `authentication_error` | Missing or unknown bearer key | | |
| | 403 | `model_access_forbidden` | Key valid but Model not in `allowed_models` | | |
| | 401 | `invalid_api_key` | Missing or unknown bearer key | | |
| | 403 | `permission_denied` | Key valid but Model not in `allowed_models` | |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
More mismatches in this table: 422 is used for content_filter (guardrails), not invalid_request_error (empty messages is a 400). 429s are rate_limit_exceeded (the proxy doesn’t currently emit separate concurrency/token-specific type strings) or budget_exceeded. 503 uses provider_unavailable, and 504 comes from BridgeError::Timeout with type timeout.
| | 400 | `invalid_request_error` | Malformed body, missing `model`, etc. | | |
| | 401 | `authentication_error` | Missing or unknown bearer key | | |
| | 403 | `model_access_forbidden` | Key valid but Model not in `allowed_models` | | |
| | 404 | `model_not_found` | `req.model` does not resolve in the snapshot | | |
| | 413 | `request_too_large` | Body exceeds `proxy.request_body_limit_bytes` (default 10 MB) | | |
| | 422 | `invalid_request_error` | Schema-valid JSON but semantically wrong (e.g. empty `messages`) | | |
| | 429 | `rate_limit_exceeded` / `concurrency_limit_exceeded` / `budget_exceeded` | RPM/TPM/concurrency/budget cap | | |
| | 502 | `provider_error` | Upstream returned 5xx or invalid wire format | | |
| | 503 | `service_unavailable` | No bridge registered for the resolved Model's provider | | |
| | 504 | `request_timeout` | Upstream exceeded `Model.timeout` ms | | |
| | 400 | `invalid_request_error` | Malformed body, missing `model`, empty `messages`, etc. | | |
| | 401 | `authentication_error` | Missing or unknown bearer key | | |
| | 403 | `model_access_forbidden` | Key valid but Model not in `allowed_models` | | |
| | 404 | `model_not_found` | `req.model` does not resolve in the snapshot | | |
| | 413 | `request_too_large` | Body exceeds `proxy.request_body_limit_bytes` (default 10 MB) | | |
| | 422 | `content_filter` | Request blocked by guardrails/content filtering | | |
| | 429 | `rate_limit_exceeded` / `budget_exceeded` | RPM/TPM/concurrency/budget cap | | |
| | 502 | `provider_error` | Upstream returned 5xx or invalid wire format | | |
| | 503 | `provider_unavailable` | No bridge registered for the resolved Model's provider | | |
| | 504 | `timeout` | `BridgeError::Timeout`; upstream exceeded `Model.timeout` ms | |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
A couple of rows here describe behaviors that the proxy doesn’t currently implement: it doesn’t emit a dedicated 413 request_too_large based on proxy.request_body_limit_bytes (there’s no body-limit layer and oversize reads generally become 400 invalid_request_error). Also the proxy never sets BridgeContext::deadline from Model.timeout, so 504 timeout errors won’t correspond to Model.timeout as described here.
| | 400 | `invalid_request_error` | Malformed body, missing `model`, etc. | | |
| | 401 | `authentication_error` | Missing or unknown bearer key | | |
| | 403 | `model_access_forbidden` | Key valid but Model not in `allowed_models` | | |
| | 404 | `model_not_found` | `req.model` does not resolve in the snapshot | | |
| | 413 | `request_too_large` | Body exceeds `proxy.request_body_limit_bytes` (default 10 MB) | | |
| | 422 | `invalid_request_error` | Schema-valid JSON but semantically wrong (e.g. empty `messages`) | | |
| | 429 | `rate_limit_exceeded` / `concurrency_limit_exceeded` / `budget_exceeded` | RPM/TPM/concurrency/budget cap | | |
| | 502 | `provider_error` | Upstream returned 5xx or invalid wire format | | |
| | 503 | `service_unavailable` | No bridge registered for the resolved Model's provider | | |
| | 504 | `request_timeout` | Upstream exceeded `Model.timeout` ms | | |
| | 400 | `invalid_request_error` | Malformed body, missing `model`, etc.; oversized request bodies currently also surface here rather than as a dedicated 413 | | |
| | 401 | `authentication_error` | Missing or unknown bearer key | | |
| | 403 | `model_access_forbidden` | Key valid but Model not in `allowed_models` | | |
| | 404 | `model_not_found` | `req.model` does not resolve in the snapshot | | |
| | 422 | `invalid_request_error` | Schema-valid JSON but semantically wrong (e.g. empty `messages`) | | |
| | 429 | `rate_limit_exceeded` / `concurrency_limit_exceeded` / `budget_exceeded` | RPM/TPM/concurrency/budget cap | | |
| | 502 | `provider_error` | Upstream returned 5xx or invalid wire format | | |
| | 503 | `service_unavailable` | No bridge registered for the resolved Model's provider | | |
| | 504 | `request_timeout` | Request exceeded an active upstream/proxy deadline; this is not currently driven by `Model.timeout` | |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The proxy only injects Retry-After for ProxyError::RateLimit (see ProxyError::retry_after_secs()); budget_exceeded responses currently do not include any retry header, and there is no Retry-After-Seconds-Header emitted. Please update this section to reflect actual headers or add the missing header behavior in code.
| For rate-limit and budget errors the response also carries | |
| `Retry-After: <seconds>` (rate limit) or `Retry-After-Seconds-Header` | |
| (budget) headers when known. | |
| For rate-limit errors, the response may also carry | |
| `Retry-After: <seconds>` when the retry delay is known. | |
| `budget_exceeded` responses currently do not include a retry header. |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This header list doesn’t match current behavior: x-aisix-call-id, x-aisix-cache, and the x-ratelimit-* headers are injected by the chat handler only, not "every endpoint". Most other endpoints instead expose x-aisix-request-id. Also x-ratelimit-reset-{requests,tokens} are rendered as seconds-until-reset strings like "59s", not a Unix timestamp.
| ## 3. Response headers (every endpoint) | |
| | Header | Meaning | | |
| |---|---| | |
| | `x-aisix-call-id` | Server-issued request UUID. Echo this when filing support tickets. | | |
| | `x-aisix-cache` | `hit` if the response came from cache, `miss` otherwise. Absent for streaming responses. | | |
| | `x-ratelimit-limit-{requests,tokens,concurrent}` | Configured caps. | | |
| | `x-ratelimit-remaining-{requests,tokens,concurrent}` | Live counters at end of request. | | |
| | `x-ratelimit-reset-{requests,tokens}` | Unix timestamp when the window resets. | | |
| ## 3. Response headers | |
| Most endpoints include: | |
| | Header | Meaning | | |
| |---|---| | |
| | `x-aisix-request-id` | Server-issued request UUID. Echo this when filing support tickets. | | |
| Chat-handler responses also include: | |
| | Header | Meaning | | |
| |---|---| | |
| | `x-aisix-call-id` | Server-issued call UUID for the chat request. Echo this when filing support tickets. | | |
| | `x-aisix-cache` | `hit` if the response came from cache, `miss` otherwise. Absent for streaming responses. | | |
| | `x-ratelimit-limit-{requests,tokens,concurrent}` | Configured caps. | | |
| | `x-ratelimit-remaining-{requests,tokens,concurrent}` | Live counters at end of request. | | |
| | `x-ratelimit-reset-{requests,tokens}` | Seconds-until-reset string such as `"59s"`. | |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The router also mounts GET /health on the proxy listener (see crates/aisix-proxy/src/lib.rs), but it’s not documented in the endpoints list. Since the PR description calls this an "endpoint reference for every mounted route", /health should be included (and noted as unauthenticated).
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The docs say aisix auto-injects stream_options: {include_usage:true} when omitted, but the proxy/gateway types don’t currently implement this (there’s no stream_options field on ChatFormat, and nothing in the proxy mutates req.extra to add it). Please remove this claim or implement the injection so streaming usage matches the documentation.
| `usage` chunk before `[DONE]`. aisix injects this automatically when | |
| the request omits the field, so client SDKs get accurate token totals | |
| even in streaming mode. | |
| `usage` chunk before `[DONE]`. |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This "passes through unchanged" claim is not accurate for the current request schema: ChatMessage has #[serde(deny_unknown_fields)] and content: String, so OpenAI-style content blocks (array/object content) and message-level fields like tool_calls will fail deserialization at the proxy boundary. The docs should call out these limitations (or the request types need to be widened to support those shapes).
| **Tool calls**, **JSON mode**, **vision content blocks**, and | |
| **function-style tool definitions** all pass through unchanged. | |
| **Compatibility note** — the current chat request schema does **not** | |
| accept every OpenAI request shape unchanged. In particular, | |
| `messages[].content` must be a string, so OpenAI-style multimodal / | |
| vision content blocks (array/object content) are rejected at the proxy | |
| boundary, and unknown message-level fields such as `tool_calls` are | |
| also rejected. Top-level options such as **JSON mode** and | |
| **function-style tool definitions** may still be forwarded when they | |
| match the accepted request schema. |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The per-request cache controls described here (Cache-Control: no-store|no-cache|s-maxage) aren’t implemented in the chat handler: caching is currently unconditional for non-streaming requests when state.cache is enabled, and the handler never reads the incoming Cache-Control header. Please either implement these semantics or adjust the docs to describe the current behavior.
| **Caching** — non-streaming requests with the same fingerprint | |
| (model + messages + temperature + top_p + max_tokens) hit the cache. | |
| Override per request with `Cache-Control` header values: | |
| | Header value | Effect | | |
| |---|---| | |
| | `no-store` | Skip cache lookup AND skip storing the response | | |
| | `no-cache` | Skip lookup but still store on success | | |
| | `s-maxage=N` | Override TTL for this entry | | |
| **Caching** — when proxy caching is enabled, non-streaming requests | |
| with the same fingerprint (model + messages + temperature + top_p + | |
| max_tokens) hit the cache. | |
| Per-request cache overrides via the `Cache-Control` request header are | |
| not currently supported by the chat handler. In particular, | |
| `no-store`, `no-cache`, and `s-maxage=N` request directives are not | |
| implemented. |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The streaming failure semantics described here don’t match the implementation: build_sse_stream always appends a final data: [DONE] after the upstream stream ends, even if it previously yielded an error (it emits event: error chunks but still sends [DONE]). Please update this section or adjust the stream builder to omit [DONE] on error if that behavior is required.
| - The terminal `data: [DONE]` is always sent on a clean upstream | |
| finish, even if the upstream omitted it. | |
| - If the upstream stream terminates abnormally, aisix sends a final | |
| error chunk and closes the response without `[DONE]`. Client SDKs | |
| that interpret missing `[DONE]` as an error will surface the right | |
| error class. | |
| - The terminal `data: [DONE]` is sent when the stream builder reaches | |
| end-of-stream, including clean upstream completion and cases where | |
| an upstream error was already emitted as an SSE error event/chunk. | |
| - If the upstream stream terminates abnormally, aisix emits an SSE | |
| error event/chunk before the response closes. Clients should treat | |
| that error event as authoritative rather than relying on absence of | |
| `[DONE]` to detect failure. |
Copilot
AI
Apr 20, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The doc links to ./architecture.md and ./api-admin.md, but those files don't exist anywhere in this repo (the docs/ directory only contains api-proxy.md). This will render as broken links in GitHub; either add the referenced docs or update these links to point at the correct existing locations.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Authentication behavior here doesn't match the implementation: the proxy extractor only accepts
Authorization: Bearer <key>orx-api-key: <key>; it rejects a bareAuthorization: <key>header. Also noteGET /healthis mounted without auth, so "Every endpoint requires" is not strictly true as written.