#14983·kong

ai-prompt-decorator re-encodes empty JSON arrays as objects (tools: [] → tools: {})

Author: Great-StoneCreated Aug 27, 2026Updated Aug 27, 2026

Is there an existing issue for this?

  • I have searched the existing issues

This is not a duplicate of #14431. That issue dropped sibling fields (model, temperature) when rewriting messages. This one keeps fields such as tools, but Lua cjson turns empty arrays into empty objects.

Kong version ($ kong version)

Kong Gateway 3.14.0.x and 3.15.0.5 (Konnect data plane and self-managed). Still reproduces after the 3.15.0.0 native-body decorator fix (AG-976 / FTI-7459).

Current Behavior

Claude Code (e.g. 2.1.247) talks to Anthropic through AI Proxy with llm_format: anthropic. Even with tools disabled it still sends tools: [] instead of omitting the field.

With ai-prompt-decorator on the same service, the upstream Anthropic Messages API returns:

400 tools: Input should be a valid array

The body that leaves Kong has "tools": {} instead of "tools": [].

The same rewrite also flips nested empty arrays inside tool JSON Schema, e.g. input_schema.required: []{}.

Without the decorator (AI Proxy native passthrough only), tools: [] is forwarded unchanged and the request succeeds.

decorate-prompt.lua decodes/encodes the native body with cjson.safe (no array_mt) and then kong.service.request.set_body(), whose JSON encoder also treats empty Lua tables as objects.

Expected Behavior

ai-prompt-decorator should preserve JSON array vs object identity when it re-serializes the request, including empty arrays:

  • tools: [] stays [] (or the field is omitted)
  • nested schema arrays such as required: [] stay []

Decorating a shared system prompt must not change Anthropic (or other native) tool payloads.

A practical fix is to decode with decode_array_with_array_mt(true) (or kong.tools.cjson.decode_with_array_mt) and encode with an encoder that honors array_mt, the same pattern request-transformer already uses. Optionally expose a config toggle, but preserving empty arrays by default for native Anthropic requests would be enough.

Steps To Reproduce

  1. Route Anthropic-native traffic through ai-proxy (llm_format: anthropic, route_type: llm/v1/chat) plus ai-prompt-decorator (llm_format: anthropic, prepend a system prompt).
  2. POST a Messages body that includes an empty tools array:
bash
curl -sS -X POST "$PROXY/v1/messages" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-4",
    "max_tokens": 8,
    "messages": [{"role": "user", "content": "ping"}],
    "tools": []
  }'
  1. Upstream sees "tools": {} and Anthropic (or a mock that validates the Anthropic contract) returns 400 tools: Input should be a valid array.
  2. Repeat without ai-prompt-decorator: "tools": [] is preserved and the request succeeds.
  3. Repeat with one tool whose input_schema.required is []: decorator rewrites required to {}.

Anything else?

Workaround we are using today: drop ai-prompt-decorator and inject the shared org system prompt with pre-function, decoding via cjson.safe.new() + decode_array_with_array_mt(true) and writing back with kong.service.request.set_raw_body. That keeps tools: [] / required: [] and still prepends Anthropic system (string or text-block array).

Related but different:

  • #14431 — decorator dropped model / temperature (fixed)
  • 3.15.0.0 AG-976 — decorator forwarded an OpenAI-shaped body for native formats (fixed); empty-array encoding was not part of that change