Skip to content

Commit 5f11809

Browse files
tysoekongfffonion
authored andcommitted
fix(ai): AG-488 Correctly return GCP Model Armor 'floor' failures [aigw-only] (#14137)
"Floor" is set and then prompts must abide by specific rulesets (e.g. hate, violence) else it will be blocked. Kong was not correctly handling a "bad" or "blocked" response from GCP. This PR makes that work. With this patch, the user no longer gets 500 'an error occured' and instead gets 400:
1 parent a4888e9 commit 5f11809

File tree

5 files changed

+122
-0
lines changed

5 files changed

+122
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
message: >
2+
**AI Plugins**: Fixed an issue where the Gemini provider would not correctly return Model Armor 'Floor' blocking responses to the caller.
3+
type: bugfix
4+
scope: Plugin

kong/llm/drivers/gemini.lua

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -658,6 +658,19 @@ local function extract_response_finish_reason(response_candidate)
658658
return "stop"
659659
end
660660

661+
local function feedback_to_kong_error(promptFeedback)
662+
if promptFeedback
663+
and type(promptFeedback) == "table"
664+
then
665+
return {
666+
error = true,
667+
message = promptFeedback.blockReasonMessage or cjson.null,
668+
reason = promptFeedback.blockReason or cjson.null,
669+
}
670+
end
671+
672+
return nil
673+
end
661674

662675
local function extract_response_tool_calls(response_candidate)
663676
local tool_calls
@@ -782,6 +795,30 @@ local function from_gemini_chat_openai(response, model_info, route_type)
782795
}
783796
end
784797

798+
elseif response.promptFeedback then
799+
kong_response = feedback_to_kong_error(response.promptFeedback)
800+
801+
if get_global_ctx("stream_mode") then
802+
set_global_ctx("blocked_by_guard", kong_response)
803+
else
804+
kong.response.set_status(400) -- safety call this in case we have already returned from e.g. another AI plugin
805+
806+
-- This is duplicated DELIBERATELY - to avoid regression,
807+
-- moving it outside of the block above may cause bugs that
808+
-- we can't predict.
809+
if response.usageMetadata and
810+
(response.usageMetadata.promptTokenCount
811+
or response.usageMetadata.candidatesTokenCount
812+
or response.usageMetadata.totalTokenCount)
813+
then
814+
kong_response.usage = {
815+
prompt_tokens = response.usageMetadata.promptTokenCount,
816+
completion_tokens = response.usageMetadata.candidatesTokenCount,
817+
total_tokens = response.usageMetadata.totalTokenCount,
818+
}
819+
end
820+
end
821+
785822
else -- probably a server fault or other unexpected response
786823
local err = "no generation candidates received from Gemini, or max_tokens too short"
787824
ngx.log(ngx.ERR, err)

spec/03-plugins/38-ai-proxy/11-gemini_integration_spec.lua

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,46 @@ for _, strategy in helpers.all_strategies() do
282282
},
283283
}
284284

285+
-- 400 chat fails Model Armor "Floor".
286+
-- NOT related to the "ai-gcp-model-armor" plugin.
287+
local chat_fail_model_armor = assert(bp.routes:insert({
288+
service = empty_service,
289+
protocols = { "http" },
290+
strip_path = true,
291+
paths = { "/gemini/llm/v1/chat/fail-model-armor" },
292+
}))
293+
bp.plugins:insert({
294+
name = "ai-proxy-advanced",
295+
id = "27544c15-3c8c-5c3f-c98a-69990644aaaa",
296+
route = { id = chat_fail_model_armor.id },
297+
config = {
298+
targets = {
299+
{
300+
route_type = "llm/v1/chat",
301+
auth = {
302+
header_name = "Authorization",
303+
header_value = "Bearer gemini-key",
304+
},
305+
logging = {
306+
log_payloads = false,
307+
log_statistics = false,
308+
},
309+
model = {
310+
name = "gemini-2.5-flash",
311+
provider = "gemini",
312+
options = {
313+
max_tokens = 256,
314+
temperature = 1.0,
315+
upstream_url = "http://" .. helpers.mock_upstream_host .. ":" .. MOCK_PORTS._GEMINI .. "/v1/chat/completions/fail-model-armor",
316+
input_cost = 15.0,
317+
output_cost = 15.0,
318+
},
319+
},
320+
},
321+
},
322+
},
323+
})
324+
285325
----
286326
-- ANTHROPIC MODELS
287327
----
@@ -435,6 +475,26 @@ for _, strategy in helpers.all_strategies() do
435475
local json = cjson.decode(body)
436476
assert.equals("gemini-2.0-flash-079", json.model)
437477
end)
478+
479+
it("bad request fails gcp model armor floor settings", function()
480+
local r = client:get("/gemini/llm/v1/chat/fail-model-armor", {
481+
headers = {
482+
["content-type"] = "application/json",
483+
["accept"] = "application/json",
484+
},
485+
-- the body doesn't matter - the mock server always returns the error we want
486+
body = pl_file.read("spec/fixtures/ai-proxy/openai/llm-v1-chat/requests/good.json"),
487+
})
488+
-- validate that the request succeeded, response status 400
489+
local body = assert.res_status(400, r)
490+
local json = cjson.decode(body)
491+
492+
assert.same(json, {
493+
error = true,
494+
message = "Blocked by Model Armor Floor Setting: The prompt violated Responsible AI Safety settings (Harassment), Prompt Injection and Jailbreak filters.",
495+
reason = "MODEL_ARMOR"
496+
})
497+
end)
438498
end)
439499

440500
describe("gemini (gemini) llm/v1/chat with query param auth", function()
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"usageMetadata": {
3+
"trafficType": "ON_DEMAND"
4+
},
5+
"createTime": "2025-09-13T22:15:16.223845Z",
6+
"promptFeedback": {
7+
"blockReasonMessage": "Blocked by Model Armor Floor Setting: The prompt violated Responsible AI Safety settings (Harassment), Prompt Injection and Jailbreak filters.",
8+
"blockReason": "MODEL_ARMOR"
9+
},
10+
"responseId": "9OzFaOXUDdyZgLUPmd2_sA0",
11+
"modelVersion": "gemini-2.5-flash"
12+
}

spec/fixtures/ai-proxy/mock_servers/gemini.lua.txt

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,13 @@ server {
7575
end
7676
}
7777
}
78+
79+
location = "/v1/chat/completions/fail-model-armor" {
80+
content_by_lua_block {
81+
local pl_file = require "pl.file"
82+
83+
ngx.status = 200
84+
ngx.print(pl_file.read("spec/fixtures/ai-proxy/gemini/llm-v1-chat/responses/fails-model-armor-floor.json"))
85+
}
86+
}
7887
}

0 commit comments

Comments
 (0)