Docs
Apache APISIXManage traffic and APIsAI GatewayConfigure Prompt Decorators

Configure Prompt Decorators

Add fixed instructions to Chat Completions and Responses API bodies, inspect the exact upstream payloads, and clean up.

Applies to:Apache APISIX 3.18.0

Deployments:DockerRoles:Platform engineerTime:20 minutesImpact:Changes live traffic

Goal

Use ai-prompt-decorator on one isolated Route to prepend a system instruction and append a user instruction. Send Chat Completions and Responses API bodies to a controlled local echo server and assert the exact payload APISIX forwards upstream.

Prompt decorators change client input and can affect safety, privacy, cost, caching, and model behavior. Treat their text as versioned policy, review changes, disclose transformations to application owners, and test every supported request format before production rollout.

Before you begin

  • Complete Get APISIX with the Docker quickstart.
  • Install Docker, cURL, and jq.
  • Use a shell that supports Bash or Zsh heredocs.

Set the endpoints and protect the fixed Route, container, and local file used by this guide:

export APISIX_ADMIN_URL="http://127.0.0.1:9180"
export GATEWAY_URL="http://127.0.0.1:9080"
export GUIDE_DIR="$(pwd)"
read -rsp "Admin API key (press Enter for an unchanged quickstart): " APISIX_ADMIN_KEY && export APISIX_ADMIN_KEY && echo
test "$(curl -sS --connect-timeout 3 --max-time 10 -o /dev/null -w '%{http_code}' "${APISIX_ADMIN_URL}/apisix/admin/routes/prompt-decorator-guide-route" -H "X-API-KEY: ${APISIX_ADMIN_KEY}")" = 404 || { echo 'Stop: route prompt-decorator-guide-route already exists'; exit 1; }
test -z "$(docker ps -aq --filter 'name=^/prompt-decorator-guide-upstream$')" || { echo 'Stop: container prompt-decorator-guide-upstream already exists'; exit 1; }
test ! -e prompt-decorator-guide-server.py || { echo 'Stop: prompt-decorator-guide-server.py already exists'; exit 1; }

Steps

Step 1: Start a request-body echo server

Create a minimal server that returns the JSON body it receives, then start it without publishing an upstream port:

cat > prompt-decorator-guide-server.py <<'PYTHON'
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer

class Handler(BaseHTTPRequestHandler):
    def do_POST(self):
        length = int(self.headers.get("content-length", "0"))
        body = self.rfile.read(length)
        self.send_response(200)
        self.send_header("content-type", "application/json")
        self.send_header("content-length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def log_message(self, *_):
        pass

ThreadingHTTPServer(("0.0.0.0", 8080), Handler).serve_forever()
PYTHON
docker run -d --name prompt-decorator-guide-upstream \
  --network apisix-quickstart-net \
  -v "${GUIDE_DIR}/prompt-decorator-guide-server.py:/srv/server.py:ro" \
  python:3.13.7-alpine python /srv/server.py
for attempt in {1..15}; do
  docker exec apisix-quickstart bash -c '</dev/tcp/prompt-decorator-guide-upstream/8080' >/dev/null 2>&1 && break
  sleep 1
done
docker exec apisix-quickstart bash -c '</dev/tcp/prompt-decorator-guide-upstream/8080'

This echo server is for local inspection only. It has no authentication, TLS, request limit, or production hardening, and its port is intentionally reachable only from the Docker network.

What changed: APISIX has a controlled upstream that reveals the post-plugin request body without calling a model provider.

Step 2: Create the decorated Route

Use both URI shapes on one authenticated fixed-ID Route. Responses API detection requires the /v1/responses suffix; Chat Completions detection uses the messages field:

jq -n '{
  name:"prompt-decorator-guide-route",
  uris:["/decorator/chat","/decorator/v1/responses"],
  methods:["POST"],
  plugins:{
    "ai-prompt-decorator":{
      prepend:[{role:"system",content:"Answer briefly and conceptually."}],
      append:[{role:"user",content:"End with a simple analogy."}],
      max_req_body_size:16384
    }
  },
  upstream:{type:"roundrobin",nodes:{"prompt-decorator-guide-upstream:8080":1}}
}' | \
curl -sS --connect-timeout 3 --max-time 15 \
  "${APISIX_ADMIN_URL}/apisix/admin/routes/prompt-decorator-guide-route" -X PUT \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}" -H 'Content-Type: application/json' \
  --data-binary @- | \
jq -e '.value.id == "prompt-decorator-guide-route" and .value.uris == ["/decorator/chat","/decorator/v1/responses"] and .value.plugins["ai-prompt-decorator"].max_req_body_size == 16384'

The plugin does not decorate Embeddings requests because that format has no prompt roles. Unsupported non-empty JSON objects pass through unchanged; bind this plugin only to intended AI endpoints.

What changed: both guide paths apply the same bounded, reviewable prompt policy.

Validate

Send a Chat Completions body and require the exact three-message order returned by the echo server:

curl --fail-with-body -sS --connect-timeout 3 --max-time 10 \
  "${GATEWAY_URL}/decorator/chat" -X POST \
  -H 'Content-Type: application/json' \
  -d '{"model":"guide-model","messages":[{"role":"user","content":"What is mTLS?"}]}' | \
jq -e '.model == "guide-model" and .messages == [
  {role:"system",content:"Answer briefly and conceptually."},
  {role:"user",content:"What is mTLS?"},
  {role:"user",content:"End with a simple analogy."}
]'

Send a Responses API body and require the prepended instructions and appended input:

curl --fail-with-body -sS --connect-timeout 3 --max-time 10 \
  "${GATEWAY_URL}/decorator/v1/responses" -X POST \
  -H 'Content-Type: application/json' \
  -d '{"model":"guide-model","input":"What is mTLS?"}' | \
jq -e '.model == "guide-model" and .instructions == "Answer briefly and conceptually." and .input == "What is mTLS?\nEnd with a simple analogy."'

Both assertions must pass. They validate the body APISIX sent upstream rather than inferring decoration from a model's prose.

Roll back / Clean up

Delete the Route before its upstream, then remove the local file and shell variables:

curl --fail-with-body -sS --connect-timeout 3 --max-time 15 -X DELETE \
  "${APISIX_ADMIN_URL}/apisix/admin/routes/prompt-decorator-guide-route" \
  -H "X-API-KEY: ${APISIX_ADMIN_KEY}"
docker rm -f prompt-decorator-guide-upstream
rm -f prompt-decorator-guide-server.py
unset APISIX_ADMIN_URL APISIX_ADMIN_KEY GATEWAY_URL GUIDE_DIR

Expected: the fixed Route, container, local file, and shell variables are absent.

Troubleshoot

SymptomCauseFix
The body is unchanged.The body is not a recognized request format, or another Route matched.Use the exact validation request and read the fixed Route through the authenticated Admin API.
A Responses body is treated as Embeddings.The Route URI does not end in /v1/responses.Preserve the required suffix on the client-facing path.
The request returns 400.The body is invalid JSON or exceeds max_req_body_size.Validate the JSON and size the limit from an explicit payload and concurrency budget.
The echo server is unreachable.The containers do not share apisix-quickstart-net or the Python process exited.Inspect docker logs prompt-decorator-guide-upstream and repeat the bounded TCP readiness test.