Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apisix/plugins/ai-providers/base.lua
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,9 @@ function _M.build_request(self, conf, body, opts)
-- Passed through as given: a string goes out verbatim (the client's own
-- bytes), a table is encoded by the transport.
body = body,
-- Providers that route through a redirect declare it, so the transport
-- follows the hop instead of handing the 3xx back downstream.
follow_redirects = self.follow_redirects,
}

-- AWS SigV4 signing (must be last — signs the finalized body)
Expand Down
2 changes: 1 addition & 1 deletion apisix/plugins/ai-providers/schema.lua
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ local _M = {}
_M.providers = {
"openai", "deepseek", "aimlapi", "anthropic",
"openai-compatible", "azure-openai", "openrouter",
"gemini", "vertex-ai", "bedrock",
"gemini", "vertex-ai", "bedrock", "thegrid",
}

return _M
59 changes: 59 additions & 0 deletions apisix/plugins/ai-providers/thegrid.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
--
-- Licensed to the Apache Software Foundation (ASF) under one or more
-- contributor license agreements. See the NOTICE file distributed with
-- this work for additional information regarding copyright ownership.
-- The ASF licenses this file to You under the Apache License, Version 2.0
-- (the "License"); you may not use this file except in compliance with
-- the License. You may obtain a copy of the License at
--
-- http://www.apache.org/licenses/LICENSE-2.0
--
-- Unless required by applicable law or agreed to in writing, software
-- distributed under the License is distributed on an "AS IS" BASIS,
-- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-- See the License for the specific language governing permissions and
-- limitations under the License.
--

-- The Grid accepts `max_completion_tokens` like OpenAI, so the override is
-- mapped to that field rather than the legacy `max_tokens`.
local function rewrite_chat_request_body(body, override, force)
if override.max_tokens then
if force or (body.max_completion_tokens == nil and body.max_tokens == nil) then
body.max_completion_tokens = override.max_tokens
body.max_tokens = nil
end
end
end


local function rewrite_responses_request_body(body, override, force)
if override.max_tokens then
if force or body.max_output_tokens == nil then
body.max_output_tokens = override.max_tokens
end
end
end

return require("apisix.plugins.ai-providers.base").new(
{
host = "api.thegrid.ai",
port = 443,
-- The Consumption API validates the request and then answers with a
-- documented 307 to its routing layer on the same host, which is where
-- inference is actually fulfilled. The transport follows that hop so
-- the Plugin observes the real response, and token usage stays visible
-- to ai-rate-limiting and to ai-proxy-multi's fallback.
follow_redirects = true,
capabilities = {
["openai-chat"] = {
path = "/v1/chat/completions",
rewrite_request_body = rewrite_chat_request_body,
},
["openai-responses"] = {
path = "/v1/responses",
rewrite_request_body = rewrite_responses_request_body,
},
},
}
)
94 changes: 94 additions & 0 deletions apisix/plugins/ai-transport/http.lua
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@

local core = require("apisix.core")
local http_client = require("apisix.utils.http")
local url = require("socket.url")
local ngx_now = ngx.now
local pairs = pairs
local ipairs = ipairs
local pcall = pcall
local type = type
local str_lower = string.lower
local tonumber = tonumber
local tostring = tostring

local attr_schema = {
Expand Down Expand Up @@ -121,6 +123,82 @@ local function encode_body(body)
end


-- Redirect statuses that preserve the method and the body, so the request can
-- be replayed unchanged. 301/302/303 are deliberately absent: they permit the
-- method to be rewritten to GET, which would silently drop the prompt.
local REDIRECT_STATUSES = {
[307] = true,
[308] = true,
}


--- Follow one same-origin 307/308 redirect, reusing the open connection.
-- Providers that route through a redirect (The Grid's Consumption API answers
-- with a documented 307 to its routing layer) need it followed inside the
-- gateway: returning the 3xx downstream would leave the Plugin blind to the
-- real response, so token usage, retries and instance fallback would all see a
-- request that never produced any tokens.
--
-- Only same-origin redirects are followed. A cross-origin hop would replay the
-- provider credentials in `Authorization` against a host chosen by the
-- response, so it is refused rather than followed.
-- @param httpc table Connected HTTP client
-- @param params table Request parameters that produced `res`
-- @param res table The redirect response
-- @return table|nil New response object
-- @return string|nil Error message
local function follow_redirect(httpc, params, res)
local location = res.headers["Location"] or res.headers["location"]
if not location then
return nil, "redirect response carried no Location header"
end

local parsed = url.parse(location)
if not parsed or not parsed.path then
return nil, "could not parse Location header"
end

-- A Location without a host is relative, and therefore same-origin already.
if parsed.host then
local scheme = parsed.scheme or params.scheme
local port = tonumber(parsed.port)
or (scheme == "https" and 443 or 80)
if parsed.host ~= params.host
or scheme ~= params.scheme
or port ~= tonumber(params.port) then
return nil, "refusing to follow cross-origin redirect to " .. location
end
end

-- The connection is reused for the replay, so the redirect's own body has
-- to be drained off the wire first.
local _, read_err = res:read_body()
if read_err then
return nil, "failed to drain redirect body: " .. read_err
end

local next_params = {}
for k, v in pairs(params) do
next_params[k] = v
end
next_params.path = parsed.path
-- A redirect target that carries its own query string replaces the original
-- one, which belonged to the hop that has already been answered. Without
-- one, the original is kept, so providers that authenticate through
-- `auth.query` still reach the target authenticated.
if parsed.query then
next_params.query = parsed.query
end

local next_res, err = httpc:request(next_params)
if not next_res then
return nil, "redirect request: " .. (err or "unknown")
end

return next_res
end


--- Send an HTTP request to an AI service.
-- Handles the full lifecycle: create client, connect, encode body,
-- send request, and return the response object.
Expand Down Expand Up @@ -196,6 +274,22 @@ function _M.request(params, timeout)
}
end

if params.follow_redirects and REDIRECT_STATUSES[res.status] then
local redirected, redirect_err = follow_redirect(httpc, params, res)
if not redirected then
httpc:close()
return nil, redirect_err, {
upstream_addr = upstream_addr,
upstream_host = upstream_host,
upstream_scheme = upstream_scheme,
upstream_uri = params.path,
connect_time = connect_time,
t0 = t0,
}
end
res = redirected
end

local header_time = (ngx_now() - t0) * 1000

-- Attach httpc and upstream metadata to res
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/plugins/ai-proxy-multi.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ When an instance's `provider` is set to `bedrock`, the Plugin expects requests i
| balancer.key | string | False | | | Used when `type` is `chash`. When `hash_on` is set to `header` or `cookie`, `key` is required. When `hash_on` is set to `consumer`, `key` is not required as the consumer name will be used as the key automatically. |
| instances | array[object] | True | | | LLM instance configurations. |
| instances.name | string | True | | | Name of the LLM service instance. It must be unique within `instances`, since it identifies the instance in the balancer, in its health checker, and in other Plugins that reference it, such as `ai-rate-limiting`. |
| instances.provider | string | True | | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, bedrock, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `api.openai.com`. When set to `deepseek`, the Plugin will proxy the request to `api.deepseek.com`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `api.aimlapi.com` by default. When set to `anthropic`, the Plugin will proxy the request to `api.anthropic.com` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `openrouter.ai` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `generativelanguage.googleapis.com` by default. When set to `vertex-ai`, the Plugin will proxy the request to `aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `bedrock`, the Plugin proxies the request to Amazon Bedrock's Converse API at `bedrock-runtime.{region}.amazonaws.com` and signs the request with AWS SigV4. Requires `provider_conf.region` and `auth.aws`. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |
| instances.provider | string | True | | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, bedrock, thegrid, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `api.openai.com`. When set to `deepseek`, the Plugin will proxy the request to `api.deepseek.com`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `api.aimlapi.com` by default. When set to `anthropic`, the Plugin will proxy the request to `api.anthropic.com` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `openrouter.ai` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `generativelanguage.googleapis.com` by default. When set to `vertex-ai`, the Plugin will proxy the request to `aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `bedrock`, the Plugin proxies the request to Amazon Bedrock's Converse API at `bedrock-runtime.{region}.amazonaws.com` and signs the request with AWS SigV4. Requires `provider_conf.region` and `auth.aws`. When set to `thegrid`, the Plugin uses the OpenAI-compatible driver and proxies the request to `api.thegrid.ai` by default; The Grid's model ids are market instruments such as `text-standard` or `agent-max` rather than fixed models. The Consumption API answers with a `307` redirect to its routing layer on the same host; the Plugin follows it, so the response — and the token usage that `ai-rate-limiting` and instance fallback depend on — stays visible to the gateway. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |
| instances.provider_conf | object | False | | | Configuration for the specific provider. Required when `provider` is set to `vertex-ai` and `override` is not configured. Required when `provider` is set to `bedrock`. |
| instances.provider_conf.project_id | string | True | | | Google Cloud Project ID. |
| instances.provider_conf.region | string | True (depending on provider) | | minLength = 1 (for Bedrock) | When `provider` is `vertex-ai`, this is the Google Cloud Region. When `provider` is `bedrock`, this is the AWS region used to construct the Bedrock endpoint and to sign the request with SigV4 (required, must be non-empty). |
Expand Down
3 changes: 2 additions & 1 deletion docs/en/latest/plugins/ai-proxy.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ When `provider` is set to `bedrock`, the Plugin expects requests in the [Bedrock

| Name | Type | Required | Default | Valid values | Description |
|--------------------|--------|----------|---------|------------------------------------------|-------------|
| provider | string | True | | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, bedrock, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `https://api.openai.com/chat/completions`. When set to `deepseek`, the Plugin will proxy the request to `https://api.deepseek.com/chat/completions`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://api.aimlapi.com/v1/chat/completions` by default. When set to `anthropic`, the Plugin will proxy the request to `https://api.anthropic.com/v1/chat/completions` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://openrouter.ai/api/v1/chat/completions` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` by default. When set to `vertex-ai`, the Plugin will proxy the request to `https://aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `bedrock`, the Plugin will proxy the request to the AWS Bedrock Converse API (`https://bedrock-runtime.<region>.amazonaws.com`) and signs the request with AWS SigV4. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |
| provider | string | True | | [openai, deepseek, azure-openai, aimlapi, anthropic, openrouter, gemini, vertex-ai, bedrock, thegrid, openai-compatible] | LLM service provider. When set to `openai`, the Plugin will proxy the request to `https://api.openai.com/chat/completions`. When set to `deepseek`, the Plugin will proxy the request to `https://api.deepseek.com/chat/completions`. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://api.aimlapi.com/v1/chat/completions` by default. When set to `anthropic`, the Plugin will proxy the request to `https://api.anthropic.com/v1/chat/completions` by default. When set to `openrouter`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://openrouter.ai/api/v1/chat/completions` by default. When set to `gemini`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://generativelanguage.googleapis.com/v1beta/openai/chat/completions` by default. When set to `vertex-ai`, the Plugin will proxy the request to `https://aiplatform.googleapis.com` by default and requires `provider_conf` or `override`. When set to `bedrock`, the Plugin will proxy the request to the AWS Bedrock Converse API (`https://bedrock-runtime.<region>.amazonaws.com`) and signs the request with AWS SigV4. When set to `thegrid`, the Plugin uses the OpenAI-compatible driver and proxies the request to `api.thegrid.ai` by default; The Grid's model ids are market instruments such as `text-standard` or `agent-max` rather than fixed models. The Consumption API answers with a `307` redirect to its routing layer on the same host; the Plugin follows it, so the response — and the token usage that `ai-rate-limiting` and instance fallback depend on — stays visible to the gateway. When set to `openai-compatible`, the Plugin will proxy the request to the custom endpoint configured in `override`. |
| provider_conf | object | False | | | Configuration for the specific provider. Required when `provider` is set to `vertex-ai` and `override` is not configured. Required when `provider` is set to `bedrock`. |
| provider_conf.project_id | string | True | | | Google Cloud Project ID. |
| provider_conf.region | string | True (depending on provider) | | minLength = 1 (for Bedrock) | When `provider` is `vertex-ai`, this is the Google Cloud Region. When `provider` is `bedrock`, this is the AWS region used to construct the Bedrock endpoint and to sign the request with SigV4 (required, must be non-empty). |
Expand Down Expand Up @@ -117,6 +117,7 @@ The table below shows, for each `provider` and target API endpoint, the upstream
| `deepseek` | `max_tokens` | — | — |
| `aimlapi` | `max_tokens` | — | — |
| `openrouter` | `max_tokens` | — | — |
| `thegrid` | `max_completion_tokens` | `max_output_tokens` | — |
| `gemini` | `max_completion_tokens` | — | — |
| `vertex-ai` | `max_completion_tokens` | — | — |
| `anthropic` | `max_tokens` | — | `max_tokens` |
Expand Down
2 changes: 1 addition & 1 deletion docs/en/latest/plugins/ai-request-rewrite.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ The `ai-request-rewrite` Plugin processes client requests by forwarding them to
| --- | --- | --- | --- | --- | --- |
| `max_req_body_size` | integer | False | 67108864 | >= 1 | Maximum request body size in bytes buffered into memory. Requests with a larger body are rejected. |
| `prompt` | string | True | | | The prompt to send to the LLM service for rewriting the client request. |
| `provider` | string | True | | [openai, deepseek, azure-openai, aimlapi, gemini, vertex-ai, anthropic, openrouter, openai-compatible] | LLM service provider. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://api.aimlapi.com/v1/chat/completions`. When set to `openai-compatible`, the Plugin proxies requests to the custom endpoint configured in `override`. When set to `azure-openai`, the Plugin also proxies requests to the custom endpoint configured in `override` and additionally omits the `model` parameter from the request body sent to Azure OpenAI. |
| `provider` | string | True | | [openai, deepseek, azure-openai, aimlapi, gemini, vertex-ai, anthropic, openrouter, thegrid, openai-compatible] | LLM service provider. When set to `aimlapi`, the Plugin uses the OpenAI-compatible driver and proxies the request to `https://api.aimlapi.com/v1/chat/completions`. When set to `openai-compatible`, the Plugin proxies requests to the custom endpoint configured in `override`. When set to `azure-openai`, the Plugin also proxies requests to the custom endpoint configured in `override` and additionally omits the `model` parameter from the request body sent to Azure OpenAI. |
| `auth` | object | True | | | Authentication configurations. |
| `auth.header` | object | False | | | Authentication headers. Key must match pattern `^[a-zA-Z0-9._-]+$`. At least one of `header` and `query` should be configured. |
| `auth.query` | object | False | | | Authentication query parameters. Key must match pattern `^[a-zA-Z0-9._-]+$`. At least one of `header` and `query` should be configured. |
Expand Down
Loading
Loading