nic-6443 commented on code in PR #14139:
URL: https://github.com/apache/apisix/pull/14139#discussion_r4464296151
##########
apisix/plugins/ai-protocols/anthropic-messages.lua:
##########
@@ -345,31 +345,78 @@ function _M.get_messages(body)
end
+local function system_blocks(system)
+ local blocks = {}
+ if type(system) == "string" then
+ core.table.insert(blocks, {type = "text", text = system})
+ elseif type(system) == "table" then
+ for _, block in ipairs(system) do
+ core.table.insert(blocks, block)
+ end
+ end
+ return blocks
+end
+
+
+local function configured_messages(msgs)
+ local systems = {}
+ local messages = {}
+ for _, msg in ipairs(msgs or {}) do
+ if msg.role == "system" then
+ core.table.insert(systems, {type = "text", text = msg.content})
+ else
+ core.table.insert(messages, {role = msg.role, content =
msg.content})
+ end
+ end
+ return systems, messages
+end
+
+
--- Prepend messages to the request body.
function _M.prepend_messages(body, msgs)
if not msgs or #msgs == 0 then return end
- if not body.messages then
- body.messages = {}
+
+ local systems, messages = configured_messages(msgs)
+ if #systems > 0 then
+ local merged = systems
+ for _, block in ipairs(system_blocks(body.system)) do
+ core.table.insert(merged, block)
+ end
+ body.system = merged
Review Comment:
This turns `body.system` into a block array, but `get_messages` (line 329)
only reads `system` when it's a string, so the system prompt disappears from
the normalized messages as soon as the decorator prepends one.
The decorator runs in rewrite, and ai-prompt-guard (`match_all_roles`) and
ai-lakera-guard read `get_messages` in access. So a client sending a string
`system` with denied content now gets through, and the decorated content isn't
checked either. Before this PR both were checked. A client sending an array
`system` could already skip this, so having `get_messages` reuse
`extract_system_content` would fix both.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
+ local ok, render = pcall(template.compile, raw_template)
+ if not ok then
+ return nil, "template compile: " .. render
end
- return body_tab
+ local output
+ ok, output = pcall(render, input)
+ if not ok then
+ return nil, "template rendering: " .. output
+ end
+ return output
end
-local function find_template(conf, template_name)
- for _, template in ipairs(conf.templates) do
- if template.name == template_name then
- return template.template
+local function render_template(configured, input)
Review Comment:
Same regression membphis raised above, just adding to the list: `system`,
`input` and `messages[].name` are affected too, not only `user` and `metadata`.
There's no error, the literal `{{ x }}` just goes to the LLM, so nobody will
notice after an upgrade until they look at the model output.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
+ local ok, render = pcall(template.compile, raw_template)
+ if not ok then
+ return nil, "template compile: " .. render
end
- return body_tab
+ local output
+ ok, output = pcall(render, input)
Review Comment:
`render(input)` uses the request body as the context directly, which drops
the `_ctx`, `_body`, `_escape_json`, `_escape_xml` and `_multipart` helpers
that body-transformer injected through the metatable.
An existing template like `ip {{ _ctx.var.remote_addr }}` or `{*
_escape_json(x) *}` now returns 503 on every request with `attempt to index
global '_ctx' (a nil value)`. I reproduced that one with a small `resty`
script. Neither the docs nor the PR description mention this as a breaking
change.
##########
apisix/plugins/ai-prompt-decorator.lua:
##########
@@ -71,36 +55,30 @@ function _M.check_schema(conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
- end
-
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
- end
-
- return body_tab
-end
-
-
function _M.rewrite(conf, ctx)
- local body_tab, err = get_request_body_table(conf.max_req_body_size)
+ local body_tab, status, err = body.read(conf.max_req_body_size)
Review Comment:
`fail_mode` doesn't cover this path. `body.read` returns before protocol
detection, so `skip` and `warn` never apply to read or parse failures.
With the default `fail_mode=skip`, a Consumer-bound decorator returns 400
for an empty-body GET or a non-JSON form post, while `binding.lua` and the docs
say that kind of non-AI traffic should pass through. ai-prompt-guard already
sends non-JSON bodies to `binding.on_unsupported` (line 119), so the two
plugins disagree here too.
##########
apisix/plugins/ai-prompt/schema.lua:
##########
@@ -0,0 +1,44 @@
+--
+-- 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.
+--
+
+local _M = {}
+
+
+_M.message = {
+ type = "object",
+ properties = {
+ role = {
+ type = "string",
+ enum = {"system", "user", "assistant"},
+ },
+ content = {
+ type = "string",
+ minLength = 1,
+ },
+ },
+ required = {"role", "content"},
+}
+
+
+_M.messages = {
+ type = "array",
+ minItems = 1,
Review Comment:
Sharing `minItems = 1` with the decorator's `prepend` and `append` tightens
the schema for configs already in etcd. `{"prepend": [], "append": [{...}]}` is
easy to get from generated config, and the `append` works fine today.
After an upgrade that route fails `check_route`, config_etcd logs `failed to
check item data` and skips it, so its traffic gets a 404. That's a lot worse
than the "ineffective config" the PR description calls it. I'd either keep
tolerating empty arrays or give this a migration path.
##########
apisix/plugins/ai-protocols/anthropic-messages.lua:
##########
@@ -345,31 +345,78 @@ function _M.get_messages(body)
end
+local function system_blocks(system)
+ local blocks = {}
+ if type(system) == "string" then
Review Comment:
An empty-string `system` also becomes `{type = "text", text = ""}` here.
`"system": ""` is fine as a string, but after a `prepend` with role system the
request becomes `[{text: "x"}, {type: "text", text: ""}]`.
From memory Anthropic rejects that with a 400 (`text content blocks must be
non-empty`), I haven't confirmed it against the live API. Skipping empty
strings here avoids the question either way.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
+ local ok, render = pcall(template.compile, raw_template)
+ if not ok then
+ return nil, "template compile: " .. render
end
- return body_tab
+ local output
+ ok, output = pcall(render, input)
Review Comment:
Not new in this PR, but worth fixing while this is being rewritten:
resty.template looks names up in the context first, so request fields can
shadow the globals the compiled template relies on, and falsy values fall
through to `template` and `_G`.
`{"template_name": "sql", "table": "users"}` with `Describe {{ table }}`
shadows `table.concat` and returns 503 with `attempt to call field 'concat' (a
nil value)`. `{"flag": false}` with `{{ flag }}` renders as an empty string.
`{{ type }}` with no `type` in the body resolves to `_G.type` and errors. All
reproduced with a `resty` script. Since this no longer reuses body-transformer,
plain variable path substitution may fit better than compiling and running Lua.
##########
apisix/plugins/ai-prompt/body.lua:
##########
@@ -0,0 +1,63 @@
+--
+-- 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.
+--
+
+local core = require("apisix.core")
+local ngx = ngx
+local pairs = pairs
+local type = type
+
+local _M = {}
+
+
+function _M.read(max_size)
+ local body, err = core.request.get_json_request_body_table(max_size)
Review Comment:
membphis already covered the size limit bypass on the cache-hit path.
There's a second problem from the same cache: the type check.
Say two routes share a URI, one with `vars` on `post_arg.model` and a
fallback carrying the prompt plugin. A JSON body sent as
`application/x-www-form-urlencoded` (what `curl -d` does by default) gets
parsed and cached as form while the first route's vars are evaluated. It then
matches the fallback, and `body.read` returns 400 `request body type mismatch`,
where the old `get_body` plus decode parsed the JSON fine.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
+ local ok, render = pcall(template.compile, raw_template)
Review Comment:
`template.compile` without `plain = true` makes resty.template try each
string as a file path first, and if it can read one it uses the file's contents
as the template.
That couldn't happen in the old flow since it compiled a whole JSON string
starting with `{`, but `model` and `content` are compiled individually now. A
literal `../../../../../../../../../etc/hostname` rendered that file's contents
when I tried it, so a configured string that happens to match an existing path
sends the file to the LLM. It's also an extra blocking `io.open` per new string
per worker. `pcall(template.compile, raw_template, nil, true)` fixes both.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
+ local ok, render = pcall(template.compile, raw_template)
+ if not ok then
+ return nil, "template compile: " .. render
end
- return body_tab
+ local output
+ ok, output = pcall(render, input)
+ if not ok then
+ return nil, "template rendering: " .. output
+ end
+ return output
end
-local function find_template(conf, template_name)
- for _, template in ipairs(conf.templates) do
- if template.name == template_name then
- return template.template
+local function render_template(configured, input)
+ local rendered = core.table.deepcopy(configured)
+ local err
+
+ if rendered.model then
+ rendered.model, err = render_string(rendered.model, input)
+ if not rendered.model then
+ return nil, err
end
end
- return nil
+
+ for _, message in ipairs(rendered.messages or {}) do
+ message.content, err = render_string(message.content, input)
+ if not message.content then
+ return nil, err
+ end
+ end
+
+ return rendered
end
+
function _M.rewrite(conf, ctx)
- local body_tab, err = get_request_body_table(conf.max_req_body_size)
+ local body_tab, status, err = body.read(conf.max_req_body_size)
if not body_tab then
- return 400, err
+ return status, err
end
local template_name = body_tab.template_name
Review Comment:
Also pre-existing, but the PR adds a strict body helper and this still gets
through: `body.read` doesn't check that the top level is an object and
`template_name` isn't type checked. A POST body of `123` or `true` throws here
and comes back as a 500 with an error log, and `{"template_name": {}}` throws
at the string concat on line 140.
The decorator has the same problem on `/v1/messages`. `{"messages": "x"}`
throws at the `ipairs` on line 392 of `anthropic-messages.lua` because
`matches` only checks the body is a table, while the Bedrock path guards with
`type(body.messages) == "table"`. These should all be 4xx.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -88,43 +72,63 @@ local templates_lrucache = core.lrucache.new({
ttl = 300, count = 256
})
-local templates_json_lrucache = core.lrucache.new({
- ttl = 300, count = 256
-})
-
function _M.check_schema(conf)
return core.schema.check(schema, conf)
end
-local function get_request_body_table(max_size)
- local body, err = core.request.get_body(max_size)
- if not body then
- return nil, { message = "could not get body: " .. (err or "request
body is empty") }
+local function find_template(conf, template_name)
+ for _, template in ipairs(conf.templates) do
+ if template.name == template_name then
+ return template.template
+ end
end
+ return nil
+end
- local body_tab, err = core.json.decode(body)
- if not body_tab then
- return nil, { message = "could not parse JSON request body: " .. (err
or "invalid JSON") }
+
+local function render_string(value, input)
+ local raw_template = string_gsub(value, "{{(.-)}}", "{*%1*}")
Review Comment:
Rewriting `{{ }}` to `{* *}` with `gsub` breaks resty.template's escape and
raw semantics. `literal \{{ x }} end` now outputs `literal {* x *} end` and
`{-raw-}{{ x }}{-raw-}` outputs `{* x *}`, where both used to give a literal
`{{ x }}`.
A Lua table constructor like `{{a=1}}` inside `{% %}` gets rewritten too and
fails to compile. And because each string is compiled on its own now, `\r\n` in
config gets normalized to `\n` by the Lua long string, which the old flow kept
through JSON escaping. All of these reproduced with a `resty` script.
##########
apisix/plugins/ai-protocols/anthropic-messages.lua:
##########
@@ -345,31 +345,78 @@ function _M.get_messages(body)
end
+local function system_blocks(system)
+ local blocks = {}
+ if type(system) == "string" then
+ core.table.insert(blocks, {type = "text", text = system})
+ elseif type(system) == "table" then
+ for _, block in ipairs(system) do
+ core.table.insert(blocks, block)
+ end
+ end
+ return blocks
+end
+
+
+local function configured_messages(msgs)
+ local systems = {}
+ local messages = {}
+ for _, msg in ipairs(msgs or {}) do
+ if msg.role == "system" then
+ core.table.insert(systems, {type = "text", text = msg.content})
Review Comment:
Decorated content is now its own system block, and `convert_system` in
`converters/anthropic-messages-to-openai-chat.lua:362` joins blocks with
`table.concat(parts, "")`. That file isn't in this diff, but this change is
what makes it matter.
With prepend `Respond in French`, append `Be brief` and a client system `You
are a tutor` going to an OpenAI upstream, the system message becomes `Respond
in FrenchYou are a tutorBe brief`. They used to be separate messages. Joining
with a newline there would fix it.
##########
t/plugin/ai-prompt-decorator.t:
##########
@@ -78,7 +78,265 @@ passed
-=== TEST 2: test prepend
+=== TEST 2: reject empty and non-object prompt arrays
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local empty_code = t('/apisix/admin/routes/18',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/invalid-empty",
+ "plugins": {"ai-prompt-decorator": {"prepend": []}}
+ }]]
+ )
+ local item_code = t('/apisix/admin/routes/19',
+ ngx.HTTP_PUT,
+ [[{
+ "uri": "/invalid-item",
+ "plugins": {"ai-prompt-decorator": {"append": ["bad"]}}
+ }]]
+ )
+ ngx.say("empty=", empty_code, " item=", item_code)
+ }
+ }
+--- response_body
+empty=400 item=400
+
+
+
+=== TEST 3: configure protocol-aware decoration
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/20',
+ ngx.HTTP_PUT,
+ [[{
+ "uris": ["/v1/messages", "/model/test/converse",
"/v1/responses"],
+ "upstream": {
+ "type": "roundrobin",
+ "nodes": {"127.0.0.1:1980": 1}
+ },
+ "plugins": {
+ "ai-prompt-decorator": {
+ "prepend": [
+ {"role": "system", "content": "first system"},
+ {"role": "assistant", "content": "first
assistant"}
+ ],
+ "append": [
+ {"role": "assistant", "content": "last
assistant"},
+ {"role": "system", "content": "last system"}
+ ]
+ },
+ "proxy-rewrite": {
+ "uri": "/echo"
+ }
+ }
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ end
+ ngx.say(body)
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 4: decorate Anthropic system and conversation fields
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ ngx.sleep(1)
+ local code, body = t('/v1/messages',
+ ngx.HTTP_POST,
+ [[{
+ "model": "claude-test",
+ "system": "existing system",
+ "messages": [{"role": "user", "content": "question"}]
+ }]],
+ [[{
+ "model": "claude-test",
+ "system": [
+ {"type": "text", "text": "first system"},
+ {"type": "text", "text": "existing system"},
+ {"type": "text", "text": "last system"}
+ ],
+ "messages": [
+ {"role": "assistant", "content": "first assistant"},
+ {"role": "user", "content": "question"},
+ {"role": "assistant", "content": "last assistant"}
+ ]
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 5: decorate Bedrock system and conversation fields
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/model/test/converse',
+ ngx.HTTP_POST,
+ [[{
+ "system": [{"text": "existing system"}],
+ "messages": [{"role": "user", "content": [{"text":
"question"}]}]
+ }]],
+ [[{
+ "system": [
+ {"text": "first system"},
+ {"text": "existing system"},
+ {"text": "last system"}
+ ],
+ "messages": [
+ {"role": "assistant", "content": [{"text": "first
assistant"}]},
+ {"role": "user", "content": [{"text": "question"}]},
+ {"role": "assistant", "content": [{"text": "last
assistant"}]}
+ ]
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 6: preserve roles when decorating Responses input
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/v1/responses',
+ ngx.HTTP_POST,
+ [[{
+ "model": "gpt-test",
+ "instructions": "existing system",
+ "input": "question"
+ }]],
+ [[{
+ "model": "gpt-test",
+ "instructions": "first system\nexisting system\nlast
system",
+ "input": [
+ {"type": "message", "role": "assistant", "content":
"first assistant"},
+ {"type": "message", "role": "user", "content":
"question"},
+ {"type": "message", "role": "assistant", "content":
"last assistant"}
+ ]
+ }]]
+ )
+ if code >= 300 then
+ ngx.status = code
+ ngx.say(body)
+ return
+ end
+ ngx.say("passed")
+ }
+ }
+--- response_body
+passed
+
+
+
+=== TEST 7: fail_mode controls unsupported request handling
+--- config
+ location /t {
+ content_by_lua_block {
+ local t = require("lib.test_admin").test
+ local code, body = t('/apisix/admin/routes/23',
Review Comment:
A few test issues. TEST 7 and TEST 8 here and TEST 5 in
`ai-prompt-template.t` send a request right after PUTting a new route with no
wait, and `t()` doesn't sleep, so slow config sync gives a 404 instead of the
expected 400 or 413. TEST 4 here uses `ngx.sleep(1)` and TEST 4 in the template
test uses `ngx.sleep(0.2)`.
Route 20 (line 113) is never cleaned up and matches `/v1/responses` with the
same priority as route 1 in TEST 16 and TEST 22. From reading the code TEST 17
only passes because route 1 loads first in etcd key order, I haven't run it.
Also the default `fail_mode=skip` and `warn` have no tests at all, only
`error` does.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -136,17 +140,13 @@ function _M.rewrite(conf, ctx)
return 400, { message = "template: " .. template_name .. " not
configured." }
end
- local template_json = templates_json_lrucache(template, template,
core.json.encode, template)
- core.log.info("sending template to body_transformer, name: ",
template_name)
- return body_transformer.rewrite(
- {
- request = {
- template = template_json,
- input_format = "json"
- }
- },
- ctx
- )
+ local rendered, render_err = render_template(template, body_tab)
+ if not rendered then
+ return 503, {message = render_err}
Review Comment:
The old flow logged render failures with `core.log.error` in
body-transformer (`body-transformer.lua:218`), and that's gone here, so the
error only goes back to the client.
A template with `{{ nested.name }}` and a request without `nested` returns
503 with no gateway log, so ops see a 5xx with nothing to trace, when it's
really a client error that should be a 4xx. I get not logging the rendered
body, but the error text doesn't contain it, so this log line can stay. The
response also exposes a chunk fragment like `[string "context=... or {}..."]:5`.
##########
apisix/plugins/ai-prompt-template.lua:
##########
@@ -136,17 +140,13 @@ function _M.rewrite(conf, ctx)
return 400, { message = "template: " .. template_name .. " not
configured." }
end
- local template_json = templates_json_lrucache(template, template,
core.json.encode, template)
- core.log.info("sending template to body_transformer, name: ",
template_name)
- return body_transformer.rewrite(
- {
- request = {
- template = template_json,
- input_format = "json"
- }
- },
- ctx
- )
+ local rendered, render_err = render_template(template, body_tab)
+ if not rendered then
+ return 503, {message = render_err}
+ end
+
+ body.replace(body_tab, rendered)
Review Comment:
The PR is about aligning protocols, but ai-prompt-template still replaces
the whole body with a Chat-shaped template whatever the protocol is.
With a `{role: system}` message on `/v1/messages`, the system message stays
in `messages` and Anthropic rejects it, which is the same bug this PR fixes in
the decorator. On `/model/x/converse`, `content` is a string rather than
`[{text}]`, which Bedrock rejects. On `/v1/responses` the body has no `input`,
so detection falls back to openai-chat. I think this should write through the
module `protocols.detect` returns.
--
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.
To unsubscribe, e-mail: [email protected]
For queries about this service, please contact Infrastructure at:
[email protected]