Skip to content
29 changes: 5 additions & 24 deletions apisix/plugins/ai-lakera-guard.lua
Original file line number Diff line number Diff line change
Expand Up @@ -83,32 +83,13 @@ local function deny_message(ctx, conf, message, breakdown)
end


-- Normalize a protocol's canonical {role, content} messages into the shape
-- Lakera /v2/guard accepts: role preserved, content coerced to a plain string.
-- Some adapters (e.g. openai-chat) return body.messages verbatim, so a message's
-- content can be a multimodal array or nil (tool-call turns); flatten the text
-- parts and drop messages that carry no text.
-- get_messages returns canonical {role, content} with content already flattened
-- to a string; drop turns without a role or with nothing for Lakera to scan.
local function normalize_messages(messages)
local out = {}
for _, message in ipairs(messages or {}) do
if type(message) == "table" and type(message.role) == "string" then
local content = message.content
local text
if type(content) == "string" then
text = content
elseif type(content) == "table" then
local parts = {}
for _, part in ipairs(content) do
if type(part) == "table" and part.type == "text"
and type(part.text) == "string" then
core.table.insert(parts, part.text)
end
end
text = concat(parts, " ")
end
if text and text ~= "" then
core.table.insert(out, { role = message.role, content = text })
end
for _, message in ipairs(messages) do
if type(message.role) == "string" and message.content ~= "" then
core.table.insert(out, message)
end
end
return out
Expand Down
23 changes: 7 additions & 16 deletions apisix/plugins/ai-protocols/anthropic-messages.lua
Original file line number Diff line number Diff line change
Expand Up @@ -286,22 +286,13 @@ function _M.get_messages(body)
end
if type(body.messages) == "table" then
for _, message in ipairs(body.messages) do
local content = message.content
if type(content) == "string" then
core.table.insert(messages, {role = message.role, content = content})
elseif type(content) == "table" then
local texts = {}
for _, block in ipairs(content) do
if type(block) == "table" and block.type == "text" then
core.table.insert(texts, block.text)
end
end
if #texts > 0 then
core.table.insert(messages, {
role = message.role,
content = table.concat(texts, " "),
})
end
local texts = {}
append_message_text(texts, message)
if #texts > 0 then
core.table.insert(messages, {
role = message.role,
content = table.concat(texts, " "),
})
end
end
end
Expand Down
80 changes: 28 additions & 52 deletions apisix/plugins/ai-protocols/bedrock-converse.lua
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,19 @@ function _M.extract_usage(res_body)
end


-- Append the text of each Bedrock content block ({text = "..."}) into `texts`.
local function append_block_texts(texts, blocks)
if type(blocks) ~= "table" then
return
end
for _, block in ipairs(blocks) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(texts, block.text)
end
end
end


--- Extract response text from a Bedrock Converse response.
-- Bedrock format: res_body.output.message.content[].text
function _M.extract_response_text(res_body)
Expand All @@ -160,11 +173,7 @@ function _M.extract_response_text(res_body)
return nil
end
local texts = {}
for _, block in ipairs(message.content) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(texts, block.text)
end
end
append_block_texts(texts, message.content)
if #texts > 0 then
return table.concat(texts, " ")
end
Expand All @@ -175,26 +184,12 @@ end
--- Extract all text content from a request body for moderation.
function _M.extract_request_content(body)
local contents = {}
if type(body.system) == "table" then
for _, block in ipairs(body.system) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(contents, block.text)
end
end
end
append_block_texts(contents, body.system)
if type(body.messages) == "table" then
for _, message in ipairs(body.messages) do
if type(message) ~= "table" then
goto CONTINUE_MESSAGE
end
if type(message.content) == "table" then
for _, block in ipairs(message.content) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(contents, block.text)
end
end
if type(message) == "table" then
append_block_texts(contents, message.content)
end
::CONTINUE_MESSAGE::
end
end
return contents
Expand Down Expand Up @@ -226,13 +221,8 @@ function _M.extract_user_content(body, mode)
end
for i = start_idx, #messages do
local message = messages[i]
if type(message) == "table" and message.role == "user"
and type(message.content) == "table" then
for _, block in ipairs(message.content) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(contents, block.text)
end
end
if type(message) == "table" and message.role == "user" then
append_block_texts(contents, message.content)
end
end
return contents
Expand All @@ -243,40 +233,26 @@ end
-- Bedrock content blocks [{text: "..."}] are flattened to plain text.
function _M.get_messages(body)
local messages = {}
if type(body.system) == "table" then
local texts = {}
for _, block in ipairs(body.system) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(texts, block.text)
end
end
if #texts > 0 then
core.table.insert(messages, {
role = "system",
content = table.concat(texts, " "),
})
end
local system_texts = {}
append_block_texts(system_texts, body.system)
if #system_texts > 0 then
core.table.insert(messages, {
role = "system",
content = table.concat(system_texts, " "),
})
end
if type(body.messages) == "table" then
for _, message in ipairs(body.messages) do
if type(message) ~= "table" then
goto CONTINUE
end
if type(message.content) == "table" then
if type(message) == "table" then
local texts = {}
for _, block in ipairs(message.content) do
if type(block) == "table" and type(block.text) == "string" then
core.table.insert(texts, block.text)
end
end
append_block_texts(texts, message.content)
if #texts > 0 then
core.table.insert(messages, {
role = message.role,
content = table.concat(texts, " "),
})
end
end
::CONTINUE::
end
end
return messages
Expand Down
21 changes: 20 additions & 1 deletion apisix/plugins/ai-protocols/openai-chat.lua
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ end

-- Append a single message's text (string content or text parts) into `contents`.
local function append_message_text(contents, message)
if type(message) ~= "table" then
return
end
if type(message.content) == "string" then
core.table.insert(contents, message.content)
elseif type(message.content) == "table" then
Expand Down Expand Up @@ -279,8 +282,24 @@ end


--- Get messages in canonical {role, content} format.
-- OpenAI Chat content may be a plain string or an array of typed parts
-- (e.g. {type = "text", text = "..."}); the text parts are flattened so
-- consumers always receive string content, consistent with the other adapters.
function _M.get_messages(body)
return body.messages or {}
local messages = {}
if type(body.messages) == "table" then
for _, message in ipairs(body.messages) do
local texts = {}
append_message_text(texts, message)
if #texts > 0 then
core.table.insert(messages, {
role = message.role,
content = table.concat(texts, " "),
})
end
end
end
return messages
end


Expand Down
75 changes: 41 additions & 34 deletions apisix/plugins/ai-protocols/openai-responses.lua
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,32 @@ function _M.extract_end_user_id(body)
end


-- Append an input item's text into `contents`. An item may be a plain string, a
-- message object ({content = string | {parts}}), or a bare content part
-- ({text = "..."}); text parts are flattened so consumers get plain strings.
local function append_item_text(contents, item)
if type(item) == "string" then
core.table.insert(contents, item)
return
end
if type(item) ~= "table" then
return
end
local content = item.content
if type(content) == "string" then
core.table.insert(contents, content)
elseif type(content) == "table" then
for _, part in ipairs(content) do
if type(part) == "table" and type(part.text) == "string" then
core.table.insert(contents, part.text)
end
end
elseif content == nil and type(item.text) == "string" then
core.table.insert(contents, item.text)
end
end


--- Extract all text content from a request body for moderation.
function _M.extract_request_content(body)
local contents = {}
Expand All @@ -201,22 +227,10 @@ function _M.extract_request_content(body)
core.table.insert(contents, input)
elseif type(input) == "table" then
for _, item in ipairs(input) do
if type(item) == "string" then
core.table.insert(contents, item)
elseif type(item) == "table" and item.content then
if type(item.content) == "string" then
core.table.insert(contents, item.content)
elseif type(item.content) == "table" then
for _, part in ipairs(item.content) do
if type(part) == "table" and part.text then
core.table.insert(contents, part.text)
end
end
end
end
append_item_text(contents, item)
end
end
if body.instructions then
if type(body.instructions) == "string" then
core.table.insert(contents, body.instructions)
end
return contents
Expand Down Expand Up @@ -256,18 +270,8 @@ function _M.extract_user_content(body, mode)
end
for i = start_idx, #input do
local item = input[i]
if type(item) == "string" then
core.table.insert(contents, item)
elseif type(item) == "table" and item.role == "user" and item.content then
if type(item.content) == "string" then
core.table.insert(contents, item.content)
elseif type(item.content) == "table" then
for _, part in ipairs(item.content) do
if type(part) == "table" and part.text then
core.table.insert(contents, part.text)
end
end
end
if type(item) == "string" or (type(item) == "table" and item.role == "user") then
append_item_text(contents, item)
end
end
return contents
Expand All @@ -286,14 +290,17 @@ function _M.get_messages(body)
core.table.insert(messages, {role = "user", content = input})
elseif type(input) == "table" then
for _, item in ipairs(input) do
if type(item) == "string" then
core.table.insert(messages, {role = "user", content = item})
elseif type(item) == "table" then
local role = item.role or "user"
local content = item.content or item.text
if type(content) == "string" then
core.table.insert(messages, {role = role, content = content})
end
local role = "user"
if type(item) == "table" and type(item.role) == "string" then
role = item.role
end
local texts = {}
append_item_text(texts, item)
if #texts > 0 then
core.table.insert(messages, {
role = role,
content = table.concat(texts, " "),
})
end
end
end
Expand Down
Loading
Loading