@@ -1841,3 +1841,108 @@ OK
18411841OK
18421842--- no_error_log
18431843[error]
1844+
1845+
1846+
1847+ === TEST 54: malformed tool_call arguments fall back to empty input instead of aborting
1848+ An OpenAI-compatible upstream may emit tool_call arguments that are not valid
1849+ JSON (or not a JSON object). The converter must not abort the whole response --
1850+ which would also drop already-collected text/thinking content -- but fall back
1851+ to an empty input object and log a warning.
1852+ --- config
1853+ location /t {
1854+ content_by_lua_block {
1855+ local converter = require("apisix.plugins.ai-protocols.converters.anthropic-messages-to-openai-chat")
1856+ local ctx = { var = { llm_model = "gpt-4o" } }
1857+
1858+ local res, err = converter.convert_response({
1859+ id = "msg_1",
1860+ choices = {{
1861+ message = {
1862+ content = "partial answer",
1863+ tool_calls = {{
1864+ id = "call_1",
1865+ type = "function",
1866+ ["function"] = { name = "do_it", arguments = "{not valid json" },
1867+ }},
1868+ },
1869+ finish_reason = "tool_calls",
1870+ }},
1871+ usage = { prompt_tokens = 10, completion_tokens = 5 },
1872+ }, ctx)
1873+
1874+ assert(res ~= nil, "conversion must not abort: " .. tostring(err))
1875+
1876+ local has_text, has_tool = false, false
1877+ for _, c in ipairs(res.content) do
1878+ if c.type == "text" and c.text == "partial answer" then
1879+ has_text = true
1880+ end
1881+ if c.type == "tool_use" then
1882+ has_tool = true
1883+ assert(type(c.input) == "table", "input is an object")
1884+ assert(next(c.input) == nil, "input is an empty object")
1885+ assert(c.name == "do_it", "tool name preserved")
1886+ end
1887+ end
1888+ assert(has_text, "already-collected text content preserved")
1889+ assert(has_tool, "tool_use block still emitted")
1890+
1891+ ngx.say("OK")
1892+ }
1893+ }
1894+ --- response_body
1895+ OK
1896+ --- error_log
1897+ failed to decode tool_call arguments
1898+
1899+
1900+
1901+ === TEST 55: tool_call arguments that decode to non-object fall back to empty input
1902+ Valid JSON that is not an object (number, string, boolean) should also trigger
1903+ the empty-object fallback and log "not a JSON object".
1904+ --- config
1905+ location /t {
1906+ content_by_lua_block {
1907+ local converter = require("apisix.plugins.ai-protocols.converters.anthropic-messages-to-openai-chat")
1908+ local ctx = { var = { llm_model = "gpt-4o" } }
1909+
1910+ local res, err = converter.convert_response({
1911+ id = "msg_1",
1912+ choices = {{
1913+ message = {
1914+ content = "answer",
1915+ tool_calls = {{
1916+ id = "call_1",
1917+ type = "function",
1918+ ["function"] = { name = "do_it", arguments = "123" },
1919+ }},
1920+ },
1921+ finish_reason = "tool_calls",
1922+ }},
1923+ usage = { prompt_tokens = 10, completion_tokens = 5 },
1924+ }, ctx)
1925+
1926+ assert(res ~= nil, "conversion must not abort: " .. tostring(err))
1927+
1928+ local has_text, has_tool = false, false
1929+ for _, c in ipairs(res.content) do
1930+ if c.type == "text" and c.text == "answer" then
1931+ has_text = true
1932+ end
1933+ if c.type == "tool_use" then
1934+ has_tool = true
1935+ assert(type(c.input) == "table", "input is an object")
1936+ assert(next(c.input) == nil, "input is an empty object")
1937+ end
1938+ end
1939+ assert(has_text, "already-collected text content preserved")
1940+ assert(has_tool, "tool_use block emitted")
1941+
1942+ ngx.say("OK")
1943+ }
1944+ }
1945+ --- response_body
1946+ OK
1947+ --- error_log
1948+ not a JSON object
0 commit comments