From 7e4e9fbba890bed667bd341545f4dd3adeb0f996 Mon Sep 17 00:00:00 2001 From: SnowFox143 Date: Sun, 19 Apr 2026 11:04:52 +0300 Subject: [PATCH 1/7] Fix: Fixed models returning '404 model not found' when using :THINKING suffix --- lua/e2ee_discovery.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/lua/e2ee_discovery.lua b/lua/e2ee_discovery.lua index 8ee8d71..c8569dd 100644 --- a/lua/e2ee_discovery.lua +++ b/lua/e2ee_discovery.lua @@ -99,11 +99,14 @@ function _M.resolve_chute_id(model, api_key) return model end + -- Before lookup, strip suffix from model name + local base_model = model:match("^([^:]+)") + local now = ngx.now() -- Check cache if model_map and now < model_map_expires then - local entry = model_map[model] + local entry = model_map[base_model] if entry then return check_confidential(model, entry) end @@ -114,7 +117,7 @@ function _M.resolve_chute_id(model, api_key) if not map then -- If we have a stale cache, try it if model_map then - local entry = model_map[model] + local entry = model_map[base_model] if entry then return check_confidential(model, entry) end @@ -125,7 +128,7 @@ function _M.resolve_chute_id(model, api_key) model_map = map model_map_expires = now + MODEL_MAP_TTL - local entry = map[model] + local entry = map[base_model] return check_confidential(model, entry) end From c5c031d148d4899bd13356e8d3206bb106cd78b9 Mon Sep 17 00:00:00 2001 From: SnowFox143 Date: Mon, 20 Apr 2026 10:10:11 +0300 Subject: [PATCH 2/7] Added :THINKING suffix handling for chat completions endpoint --- lua/e2ee_handler.lua | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/lua/e2ee_handler.lua b/lua/e2ee_handler.lua index 71938e5..524ae8a 100644 --- a/lua/e2ee_handler.lua +++ b/lua/e2ee_handler.lua @@ -345,6 +345,76 @@ function _M.handle() return _M.send_error(400, "missing 'model' field") end + -- Thinking mode handling + local enable_thinking = false + local think_header = ngx.req.get_headers()["X-Enable-Thinking"] + if think_header and think_header:lower() == "true" then + enable_thinking = true + end + if model:sub(-#":THINKING") == ":THINKING" then + payload.model = payload.model:sub(1, #payload.model - #":THINKING") + model = payload.model + enable_thinking = true + end + if enable_thinking then + if not payload.chat_template_kwargs then + payload.chat_template_kwargs = {} + end + payload.chat_template_kwargs.thinking = true + payload.chat_template_kwargs.enable_thinking = true + end + + -- Normalize the chat template kwargs thinking keys since there are two... + if payload.chat_template_kwargs then + if payload.chat_template_kwargs.thinking ~= nil + and payload.chat_template_kwargs.enable_thinking == nil then + payload.chat_template_kwargs.enable_thinking = payload.chat_template_kwargs.thinking + end + if payload.chat_template_kwargs.enable_thinking ~= nil + and payload.chat_template_kwargs.thinking == nil then + payload.chat_template_kwargs.thinking = payload.chat_template_kwargs.enable_thinking + end + if payload.chat_template_kwargs.thinking == nil then + local thinking_prefixes = { + "deepseek-ai/DeepSeek-V3.2-Speciale", + "zai-org/GLM-4.7", + "moonshotai/Kimi-K2.5", + } + local is_thinking_model = false + for _, prefix in ipairs(thinking_prefixes) do + if model:sub(1, #prefix) == prefix then + is_thinking_model = true + break + end + end + if is_thinking_model then + payload.chat_template_kwargs.thinking = true + payload.chat_template_kwargs.enable_thinking = true + end + end + + -- Fix default MiMo-V2-Flash thinking. + if payload.chat_template_kwargs.thinking == nil + and model:sub(1, #"XiaomiMiMo/MiMo-V2-Flash") == "XiaomiMiMo/MiMo-V2-Flash" then + payload.chat_template_kwargs.thinking = false + payload.chat_template_kwargs.enable_thinking = false + end + + elseif model == "deepseek-ai/DeepSeek-V3.2-Speciale" + or model == "deepseek-ai/DeepSeek-V3.2-Speciale-TEE" + or model == "zai-org/GLM-4.7" + or model == "zai-org/GLM-4.7-TEE" + or model == "moonshotai/Kimi-K2.5" + or model == "moonshotai/Kimi-K2.5-TEE" then + payload.chat_template_kwargs = { thinking = true, enable_thinking = true } + + elseif model == "XiaomiMiMo/MiMo-V2-Flash-TEE" then + payload.chat_template_kwargs = { thinking = false, enable_thinking = false } + end + + -- Re-encode body with any chat_template_kwargs modifications + body = cjson.encode(payload) + local is_streaming = (payload.stream == true) local original_path = ngx.var.uri From 5f59ff87cac52d82fb215e9eda0cefdddd869c8f Mon Sep 17 00:00:00 2001 From: SnowFox143 Date: Mon, 20 Apr 2026 10:52:39 +0300 Subject: [PATCH 3/7] Added function to handle thinking mode, :THINKING suffix now supported in the OpenAI Chat Completions API, the Claude Messages API, and the OpenAI Responses API --- lua/claude_handler.lua | 4 ++ lua/e2ee_handler.lua | 144 +++++++++++++++++++++----------------- lua/responses_handler.lua | 4 ++ 3 files changed, 87 insertions(+), 65 deletions(-) diff --git a/lua/claude_handler.lua b/lua/claude_handler.lua index 8531a1c..4f144ad 100644 --- a/lua/claude_handler.lua +++ b/lua/claude_handler.lua @@ -49,6 +49,10 @@ function _M.handle() -- Translate Claude request to OpenAI format local oai_request = claude_fmt.request_to_openai(claude_body) + + -- Thinking mode handling (operates on OpenAI request format) + oai_request, model = e2ee.handle_thinking(oai_request) + local oai_body = cjson.encode(oai_request) if not is_streaming then diff --git a/lua/e2ee_handler.lua b/lua/e2ee_handler.lua index 524ae8a..ee7b439 100644 --- a/lua/e2ee_handler.lua +++ b/lua/e2ee_handler.lua @@ -40,6 +40,84 @@ function _M.get_api_key() return key end +--- Handle thinking mode for OpenAI format requests +-- Takes an OpenAI request table, applies thinking logic, and returns the modified table +-- Also strips :THINKING suffix from model name and returns the model name +function _M.handle_thinking(oai_request) + if not oai_request or not oai_request.model then + return oai_request, nil + end + + local model = oai_request.model + local enable_thinking = false + local think_header = ngx.req.get_headers()["X-Enable-Thinking"] + if think_header and think_header:lower() == "true" then + enable_thinking = true + end + if model:sub(-#":THINKING") == ":THINKING" then + oai_request.model = oai_request.model:sub(1, #oai_request.model - #":THINKING") + model = oai_request.model + enable_thinking = true + end + if enable_thinking then + if not oai_request.chat_template_kwargs then + oai_request.chat_template_kwargs = {} + end + oai_request.chat_template_kwargs.thinking = true + oai_request.chat_template_kwargs.enable_thinking = true + end + + -- Normalize the chat template kwargs thinking keys since there are two... + if oai_request.chat_template_kwargs then + if oai_request.chat_template_kwargs.thinking ~= nil + and oai_request.chat_template_kwargs.enable_thinking == nil then + oai_request.chat_template_kwargs.enable_thinking = oai_request.chat_template_kwargs.thinking + end + if oai_request.chat_template_kwargs.enable_thinking ~= nil + and oai_request.chat_template_kwargs.thinking == nil then + oai_request.chat_template_kwargs.thinking = oai_request.chat_template_kwargs.enable_thinking + end + if oai_request.chat_template_kwargs.thinking == nil then + local thinking_prefixes = { + "deepseek-ai/DeepSeek-V3.2-Speciale", + "zai-org/GLM-4.7", + "moonshotai/Kimi-K2.5", + } + local is_thinking_model = false + for _, prefix in ipairs(thinking_prefixes) do + if model:sub(1, #prefix) == prefix then + is_thinking_model = true + break + end + end + if is_thinking_model then + oai_request.chat_template_kwargs.thinking = true + oai_request.chat_template_kwargs.enable_thinking = true + end + end + + -- Fix default MiMo-V2-Flash thinking. + if oai_request.chat_template_kwargs.thinking == nil + and model:sub(1, #"XiaomiMiMo/MiMo-V2-Flash") == "XiaomiMiMo/MiMo-V2-Flash" then + oai_request.chat_template_kwargs.thinking = false + oai_request.chat_template_kwargs.enable_thinking = false + end + + elseif model == "deepseek-ai/DeepSeek-V3.2-Speciale" + or model == "deepseek-ai/DeepSeek-V3.2-Speciale-TEE" + or model == "zai-org/GLM-4.7" + or model == "zai-org/GLM-4.7-TEE" + or model == "moonshotai/Kimi-K2.5" + or model == "moonshotai/Kimi-K2.5-TEE" then + oai_request.chat_template_kwargs = { thinking = true, enable_thinking = true } + + elseif model == "XiaomiMiMo/MiMo-V2-Flash-TEE" then + oai_request.chat_template_kwargs = { thinking = false, enable_thinking = false } + end + + return oai_request, model +end + --- Send error response function _M.send_error(status, message) ngx.status = status @@ -346,71 +424,7 @@ function _M.handle() end -- Thinking mode handling - local enable_thinking = false - local think_header = ngx.req.get_headers()["X-Enable-Thinking"] - if think_header and think_header:lower() == "true" then - enable_thinking = true - end - if model:sub(-#":THINKING") == ":THINKING" then - payload.model = payload.model:sub(1, #payload.model - #":THINKING") - model = payload.model - enable_thinking = true - end - if enable_thinking then - if not payload.chat_template_kwargs then - payload.chat_template_kwargs = {} - end - payload.chat_template_kwargs.thinking = true - payload.chat_template_kwargs.enable_thinking = true - end - - -- Normalize the chat template kwargs thinking keys since there are two... - if payload.chat_template_kwargs then - if payload.chat_template_kwargs.thinking ~= nil - and payload.chat_template_kwargs.enable_thinking == nil then - payload.chat_template_kwargs.enable_thinking = payload.chat_template_kwargs.thinking - end - if payload.chat_template_kwargs.enable_thinking ~= nil - and payload.chat_template_kwargs.thinking == nil then - payload.chat_template_kwargs.thinking = payload.chat_template_kwargs.enable_thinking - end - if payload.chat_template_kwargs.thinking == nil then - local thinking_prefixes = { - "deepseek-ai/DeepSeek-V3.2-Speciale", - "zai-org/GLM-4.7", - "moonshotai/Kimi-K2.5", - } - local is_thinking_model = false - for _, prefix in ipairs(thinking_prefixes) do - if model:sub(1, #prefix) == prefix then - is_thinking_model = true - break - end - end - if is_thinking_model then - payload.chat_template_kwargs.thinking = true - payload.chat_template_kwargs.enable_thinking = true - end - end - - -- Fix default MiMo-V2-Flash thinking. - if payload.chat_template_kwargs.thinking == nil - and model:sub(1, #"XiaomiMiMo/MiMo-V2-Flash") == "XiaomiMiMo/MiMo-V2-Flash" then - payload.chat_template_kwargs.thinking = false - payload.chat_template_kwargs.enable_thinking = false - end - - elseif model == "deepseek-ai/DeepSeek-V3.2-Speciale" - or model == "deepseek-ai/DeepSeek-V3.2-Speciale-TEE" - or model == "zai-org/GLM-4.7" - or model == "zai-org/GLM-4.7-TEE" - or model == "moonshotai/Kimi-K2.5" - or model == "moonshotai/Kimi-K2.5-TEE" then - payload.chat_template_kwargs = { thinking = true, enable_thinking = true } - - elseif model == "XiaomiMiMo/MiMo-V2-Flash-TEE" then - payload.chat_template_kwargs = { thinking = false, enable_thinking = false } - end + payload, model = _M.handle_thinking(payload) -- Re-encode body with any chat_template_kwargs modifications body = cjson.encode(payload) diff --git a/lua/responses_handler.lua b/lua/responses_handler.lua index 10bf166..02d3a71 100644 --- a/lua/responses_handler.lua +++ b/lua/responses_handler.lua @@ -49,6 +49,10 @@ function _M.handle() -- Translate Responses request to OpenAI format local oai_request = resp_fmt.request_to_openai(resp_body) + + -- Thinking mode handling (operates on OpenAI request format) + oai_request, model = e2ee.handle_thinking(oai_request) + local oai_body = cjson.encode(oai_request) if not is_streaming then From e36b476a5c27159e71fb428b5c0c06721816c1d7 Mon Sep 17 00:00:00 2001 From: sirouk <8901571+sirouk@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:15:41 -0400 Subject: [PATCH 4/7] discovery: narrow lookup strip to :THINKING only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stripping any ":" suffix in resolve_chute_id masks unknown suffixes (e.g. LoRA adapter names) by silently resolving them to the base chute, after which vLLM emits a confusing "LoRA adapter X not found" error. Only strip :THINKING — the one suffix the proxy knows how to handle. Other suffixes are now passed through unchanged so discovery fails with a clear "model not found" error. Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/e2ee_discovery.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/lua/e2ee_discovery.lua b/lua/e2ee_discovery.lua index c8569dd..832fb03 100644 --- a/lua/e2ee_discovery.lua +++ b/lua/e2ee_discovery.lua @@ -99,8 +99,12 @@ function _M.resolve_chute_id(model, api_key) return model end - -- Before lookup, strip suffix from model name - local base_model = model:match("^([^:]+)") + -- Strip the :THINKING suffix before map lookup. v1/models returns + -- base model IDs only; :THINKING is a proxy-side flag and must not + -- leak into the lookup key. Other suffixes (e.g. LoRA names) are + -- left intact so they surface as proper errors upstream rather than + -- silently resolving to the base chute. + local base_model = model:match("^(.-):THINKING$") or model local now = ngx.now() From 06e90e14a0a6db9b72e24e2996d36723a5195cc8 Mon Sep 17 00:00:00 2001 From: sirouk <8901571+sirouk@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:16:04 -0400 Subject: [PATCH 5/7] handler: use model:match(':THINKING$') for suffix check Equivalent to the previous :sub-based check but reads more naturally. Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/e2ee_handler.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/lua/e2ee_handler.lua b/lua/e2ee_handler.lua index ee7b439..55188d3 100644 --- a/lua/e2ee_handler.lua +++ b/lua/e2ee_handler.lua @@ -54,9 +54,9 @@ function _M.handle_thinking(oai_request) if think_header and think_header:lower() == "true" then enable_thinking = true end - if model:sub(-#":THINKING") == ":THINKING" then - oai_request.model = oai_request.model:sub(1, #oai_request.model - #":THINKING") - model = oai_request.model + if model:match(":THINKING$") then + model = model:sub(1, -#":THINKING" - 1) + oai_request.model = model enable_thinking = true end if enable_thinking then From e7aa426747503abed90c33deb0cea93aad03313f Mon Sep 17 00:00:00 2001 From: sirouk <8901571+sirouk@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:16:58 -0400 Subject: [PATCH 6/7] handler: extract per-model thinking defaults to named constants MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prefix list, exact-match list, and MiMo-V2-Flash special case mirror chutes-api's invocation router. Hoist them to top-level constants so the coupling is visible at a glance, and add a comment noting that /v1/models exposes a "reasoning" capability but not the per-model default value — which is why we still duplicate the list here. No behavior change. Co-Authored-By: Claude Opus 4.7 (1M context) --- lua/e2ee_handler.lua | 85 ++++++++++++++++++++++++-------------------- 1 file changed, 47 insertions(+), 38 deletions(-) diff --git a/lua/e2ee_handler.lua b/lua/e2ee_handler.lua index 55188d3..b605c9f 100644 --- a/lua/e2ee_handler.lua +++ b/lua/e2ee_handler.lua @@ -17,6 +17,31 @@ local API_BASE = "https://api.chutes.ai" local _M = {} +-- Per-model thinking defaults. Mirrors chutes-api's invocation router +-- (api/invocation/router.py). The /v1/models endpoint exposes a +-- "reasoning" capability flag in supported_features but not the +-- per-model default value, so these stay duplicated until the API +-- grows a dedicated attribute. +local THINKING_DEFAULT_ON_PREFIXES = { + "deepseek-ai/DeepSeek-V3.2-Speciale", + "zai-org/GLM-4.7", + "moonshotai/Kimi-K2.5", +} +local THINKING_DEFAULT_ON_EXACT = { + ["deepseek-ai/DeepSeek-V3.2-Speciale"] = true, + ["deepseek-ai/DeepSeek-V3.2-Speciale-TEE"] = true, + ["zai-org/GLM-4.7"] = true, + ["zai-org/GLM-4.7-TEE"] = true, + ["moonshotai/Kimi-K2.5"] = true, + ["moonshotai/Kimi-K2.5-TEE"] = true, +} +local MIMO_PREFIX = "XiaomiMiMo/MiMo-V2-Flash" +local MIMO_EXACT_TEE = "XiaomiMiMo/MiMo-V2-Flash-TEE" + +local function starts_with(s, prefix) + return s:sub(1, #prefix) == prefix +end + --- Extract API key from Authorization header or x-api-key function _M.get_api_key() local headers = ngx.req.get_headers() @@ -67,52 +92,36 @@ function _M.handle_thinking(oai_request) oai_request.chat_template_kwargs.enable_thinking = true end - -- Normalize the chat template kwargs thinking keys since there are two... - if oai_request.chat_template_kwargs then - if oai_request.chat_template_kwargs.thinking ~= nil - and oai_request.chat_template_kwargs.enable_thinking == nil then - oai_request.chat_template_kwargs.enable_thinking = oai_request.chat_template_kwargs.thinking + local kwargs = oai_request.chat_template_kwargs + if kwargs then + -- Normalize the two spellings so downstream sees both. + if kwargs.thinking ~= nil and kwargs.enable_thinking == nil then + kwargs.enable_thinking = kwargs.thinking end - if oai_request.chat_template_kwargs.enable_thinking ~= nil - and oai_request.chat_template_kwargs.thinking == nil then - oai_request.chat_template_kwargs.thinking = oai_request.chat_template_kwargs.enable_thinking + if kwargs.enable_thinking ~= nil and kwargs.thinking == nil then + kwargs.thinking = kwargs.enable_thinking end - if oai_request.chat_template_kwargs.thinking == nil then - local thinking_prefixes = { - "deepseek-ai/DeepSeek-V3.2-Speciale", - "zai-org/GLM-4.7", - "moonshotai/Kimi-K2.5", - } - local is_thinking_model = false - for _, prefix in ipairs(thinking_prefixes) do - if model:sub(1, #prefix) == prefix then - is_thinking_model = true + + if kwargs.thinking == nil then + for _, prefix in ipairs(THINKING_DEFAULT_ON_PREFIXES) do + if starts_with(model, prefix) then + kwargs.thinking = true + kwargs.enable_thinking = true break end end - if is_thinking_model then - oai_request.chat_template_kwargs.thinking = true - oai_request.chat_template_kwargs.enable_thinking = true - end end - -- Fix default MiMo-V2-Flash thinking. - if oai_request.chat_template_kwargs.thinking == nil - and model:sub(1, #"XiaomiMiMo/MiMo-V2-Flash") == "XiaomiMiMo/MiMo-V2-Flash" then - oai_request.chat_template_kwargs.thinking = false - oai_request.chat_template_kwargs.enable_thinking = false + if kwargs.thinking == nil and starts_with(model, MIMO_PREFIX) then + kwargs.thinking = false + kwargs.enable_thinking = false + end + else + if THINKING_DEFAULT_ON_EXACT[model] then + oai_request.chat_template_kwargs = { thinking = true, enable_thinking = true } + elseif model == MIMO_EXACT_TEE then + oai_request.chat_template_kwargs = { thinking = false, enable_thinking = false } end - - elseif model == "deepseek-ai/DeepSeek-V3.2-Speciale" - or model == "deepseek-ai/DeepSeek-V3.2-Speciale-TEE" - or model == "zai-org/GLM-4.7" - or model == "zai-org/GLM-4.7-TEE" - or model == "moonshotai/Kimi-K2.5" - or model == "moonshotai/Kimi-K2.5-TEE" then - oai_request.chat_template_kwargs = { thinking = true, enable_thinking = true } - - elseif model == "XiaomiMiMo/MiMo-V2-Flash-TEE" then - oai_request.chat_template_kwargs = { thinking = false, enable_thinking = false } end return oai_request, model From cc7d33dcb06913eace6ab64e4aad33dd37502247 Mon Sep 17 00:00:00 2001 From: sirouk <8901571+sirouk@users.noreply.github.com> Date: Tue, 21 Apr 2026 13:19:34 -0400 Subject: [PATCH 7/7] tests: add Lua unit tests for handle_thinking Standalone LuaJIT tests stub ngx/cjson/resty.http and exercise handle_thinking across 19 scenarios: - :THINKING suffix strip + flag set - X-Enable-Thinking header (true / false / TRUE) - Key normalization (thinking/enable_thinking) - Per-model prefix defaults (GLM-4.7, Kimi-K2.5, DeepSeek-V3.2-Speciale) - Per-model exact defaults (TEE variants) - MiMo-V2-Flash default-off - User overrides (explicit false, :THINKING on MiMo) - Edge cases (mid-string :THINKING, unrelated kwargs preserved) Run with: luajit tests/test_thinking.lua Co-Authored-By: Claude Opus 4.7 (1M context) --- tests/test_thinking.lua | 271 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 271 insertions(+) create mode 100644 tests/test_thinking.lua diff --git a/tests/test_thinking.lua b/tests/test_thinking.lua new file mode 100644 index 0000000..04d6dc8 --- /dev/null +++ b/tests/test_thinking.lua @@ -0,0 +1,271 @@ +-- +-- Unit tests for e2ee_handler.handle_thinking +-- +-- Runs the :THINKING suffix / X-Enable-Thinking header logic in isolation +-- by stubbing the openresty globals (ngx, cjson.safe, resty.http) and the +-- sibling lua modules that e2ee_handler requires but doesn't exercise here. +-- +-- Usage: +-- luajit tests/test_thinking.lua +-- +-- Exits non-zero on any assertion failure. +-- + +-- Make lua/ importable regardless of cwd. +local script_path = arg and arg[0] or "" +local script_dir = script_path:match("(.*/)") or "./" +package.path = script_dir .. "../lua/?.lua;" .. package.path + +-- Stub ngx. The only surface handle_thinking touches is ngx.req.get_headers(). +local stub_headers = {} +_G.ngx = { + req = { + get_headers = function() return stub_headers end, + }, + log = function() end, + INFO = 0, WARN = 0, ERR = 0, + now = function() return 0 end, +} + +-- Stub modules that e2ee_handler requires but handle_thinking doesn't use. +package.loaded["cjson.safe"] = { encode = function(x) return x end, decode = function(x) return x end } +package.loaded["resty.http"] = { new = function() return {} end } +package.loaded["e2ee_crypto"] = {} +package.loaded["e2ee_discovery"] = {} + +local e2ee = require("e2ee_handler") + +local failures = 0 +local total = 0 + +local function set_header(name, value) + stub_headers = {} + if value ~= nil then stub_headers[name] = value end +end + +local function assert_eq(label, actual, expected) + total = total + 1 + local ok + if type(expected) == "table" and type(actual) == "table" then + ok = true + for k, v in pairs(expected) do + if actual[k] ~= v then ok = false; break end + end + if ok then + for k, v in pairs(actual) do + if expected[k] ~= v then ok = false; break end + end + end + else + ok = (actual == expected) + end + if not ok then + failures = failures + 1 + io.stderr:write(string.format( + "FAIL %s\n expected: %s\n actual: %s\n", + label, tostring(expected), tostring(actual) + )) + else + io.stdout:write("ok " .. label .. "\n") + end +end + +-- 1. Missing model → returns request, nil; no mutation. +set_header() +do + local req = { stream = true } + local out, model = e2ee.handle_thinking(req) + assert_eq("nil model returns nil", model, nil) + assert_eq("nil model leaves request untouched", out.chat_template_kwargs, nil) +end + +-- 2. Plain model, no suffix, no header, no kwargs → unchanged. +set_header() +do + local req = { model = "deepseek-ai/DeepSeek-V3.1-TEE" } + local out, model = e2ee.handle_thinking(req) + assert_eq("plain model name preserved", model, "deepseek-ai/DeepSeek-V3.1-TEE") + assert_eq("plain model payload.model preserved", out.model, "deepseek-ai/DeepSeek-V3.1-TEE") + assert_eq("plain model leaves kwargs absent", out.chat_template_kwargs, nil) +end + +-- 3. :THINKING suffix → strip, set both keys. +set_header() +do + local req = { model = "zai-org/GLM-5.1-TEE:THINKING" } + local out, model = e2ee.handle_thinking(req) + assert_eq(":THINKING strips suffix (return)", model, "zai-org/GLM-5.1-TEE") + assert_eq(":THINKING strips suffix (payload)", out.model, "zai-org/GLM-5.1-TEE") + assert_eq(":THINKING sets thinking=true", out.chat_template_kwargs.thinking, true) + assert_eq(":THINKING sets enable_thinking=true", out.chat_template_kwargs.enable_thinking, true) +end + +-- 4. X-Enable-Thinking: true → sets both keys, no suffix. +set_header("X-Enable-Thinking", "true") +do + local req = { model = "zai-org/GLM-5.1-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("header=true sets thinking", out.chat_template_kwargs.thinking, true) + assert_eq("header=true sets enable_thinking", out.chat_template_kwargs.enable_thinking, true) + assert_eq("header=true keeps model intact", out.model, "zai-org/GLM-5.1-TEE") +end + +-- 5. X-Enable-Thinking: TRUE (uppercase) → case-insensitive. +set_header("X-Enable-Thinking", "TRUE") +do + local req = { model = "zai-org/GLM-5.1-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("header TRUE case-insensitive", out.chat_template_kwargs.thinking, true) +end + +-- 6. X-Enable-Thinking: false → does not set kwargs. +set_header("X-Enable-Thinking", "false") +do + local req = { model = "deepseek-ai/DeepSeek-V3.1-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("header=false leaves kwargs absent", out.chat_template_kwargs, nil) +end + +-- 7. Only `thinking` key → `enable_thinking` propagated. +set_header() +do + local req = { + model = "deepseek-ai/DeepSeek-V3.1-TEE", + chat_template_kwargs = { thinking = true }, + } + local out = e2ee.handle_thinking(req) + assert_eq("thinking=true propagates to enable_thinking", out.chat_template_kwargs.enable_thinking, true) +end + +-- 8. Only `enable_thinking` key → `thinking` propagated. +set_header() +do + local req = { + model = "deepseek-ai/DeepSeek-V3.1-TEE", + chat_template_kwargs = { enable_thinking = false }, + } + local out = e2ee.handle_thinking(req) + assert_eq("enable_thinking=false propagates to thinking", out.chat_template_kwargs.thinking, false) +end + +-- 9. Per-model prefix default-on (GLM-4.7) when kwargs present but no thinking key. +set_header() +do + local req = { + model = "zai-org/GLM-4.7", + chat_template_kwargs = { some_other = "x" }, + } + local out = e2ee.handle_thinking(req) + assert_eq("GLM-4.7 prefix defaults thinking=true", out.chat_template_kwargs.thinking, true) + assert_eq("GLM-4.7 prefix defaults enable_thinking=true", out.chat_template_kwargs.enable_thinking, true) + assert_eq("GLM-4.7 prefix preserves other keys", out.chat_template_kwargs.some_other, "x") +end + +-- 10. Per-model exact default-on (GLM-4.7-TEE) when kwargs absent. +set_header() +do + local req = { model = "zai-org/GLM-4.7-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("GLM-4.7-TEE exact defaults thinking=true", out.chat_template_kwargs.thinking, true) + assert_eq("GLM-4.7-TEE exact defaults enable_thinking=true", out.chat_template_kwargs.enable_thinking, true) +end + +-- 11. Per-model exact default-on for DeepSeek-V3.2-Speciale-TEE and Kimi-K2.5-TEE. +set_header() +do + local req = { model = "deepseek-ai/DeepSeek-V3.2-Speciale-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("DeepSeek-V3.2-Speciale-TEE exact thinking=true", out.chat_template_kwargs.thinking, true) +end +do + local req = { model = "moonshotai/Kimi-K2.5-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("Kimi-K2.5-TEE exact thinking=true", out.chat_template_kwargs.thinking, true) +end + +-- 12. MiMo-V2-Flash prefix default-off when kwargs present. +set_header() +do + local req = { + model = "XiaomiMiMo/MiMo-V2-Flash-Pro", + chat_template_kwargs = { some_other = "x" }, + } + local out = e2ee.handle_thinking(req) + assert_eq("MiMo-V2-Flash prefix defaults thinking=false", out.chat_template_kwargs.thinking, false) + assert_eq("MiMo-V2-Flash prefix defaults enable_thinking=false", out.chat_template_kwargs.enable_thinking, false) +end + +-- 13. MiMo-V2-Flash-TEE exact default-off when kwargs absent. +set_header() +do + local req = { model = "XiaomiMiMo/MiMo-V2-Flash-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("MiMo-V2-Flash-TEE exact thinking=false", out.chat_template_kwargs.thinking, false) + assert_eq("MiMo-V2-Flash-TEE exact enable_thinking=false", out.chat_template_kwargs.enable_thinking, false) +end + +-- 14. :THINKING on MiMo → user override wins over default-off. +set_header() +do + local req = { model = "XiaomiMiMo/MiMo-V2-Flash-TEE:THINKING" } + local out, model = e2ee.handle_thinking(req) + assert_eq(":THINKING on MiMo strips suffix", model, "XiaomiMiMo/MiMo-V2-Flash-TEE") + assert_eq(":THINKING on MiMo sets thinking=true", out.chat_template_kwargs.thinking, true) +end + +-- 15. Non-reasoning model + no suffix + no header → no kwargs injected. +set_header() +do + local req = { model = "deepseek-ai/DeepSeek-V3.1-TEE" } + local out = e2ee.handle_thinking(req) + assert_eq("non-reasoning model leaves kwargs absent", out.chat_template_kwargs, nil) +end + +-- 16. :THINKING preserves unrelated kwargs. +set_header() +do + local req = { + model = "zai-org/GLM-5.1-TEE:THINKING", + chat_template_kwargs = { tools_in_user_message = false }, + } + local out = e2ee.handle_thinking(req) + assert_eq(":THINKING preserves unrelated kwargs", + out.chat_template_kwargs.tools_in_user_message, false) + assert_eq(":THINKING still sets thinking=true", + out.chat_template_kwargs.thinking, true) +end + +-- 17. :THINKING appears mid-string (not suffix) → not stripped. +set_header() +do + local req = { model = "some:THINKING:other" } + local out, model = e2ee.handle_thinking(req) + assert_eq("non-suffix :THINKING not stripped", model, "some:THINKING:other") + assert_eq("non-suffix :THINKING leaves kwargs absent", out.chat_template_kwargs, nil) +end + +-- 18. Explicit thinking=false overrides per-model default-on. +set_header() +do + local req = { + model = "zai-org/GLM-4.7", + chat_template_kwargs = { thinking = false }, + } + local out = e2ee.handle_thinking(req) + assert_eq("explicit thinking=false overrides GLM-4.7 default", out.chat_template_kwargs.thinking, false) + assert_eq("explicit thinking=false propagates enable_thinking=false", out.chat_template_kwargs.enable_thinking, false) +end + +-- 19. DeepSeek-V3.2-Speciale prefix (bare, no TEE) with kwargs present. +set_header() +do + local req = { + model = "deepseek-ai/DeepSeek-V3.2-Speciale", + chat_template_kwargs = {}, + } + local out = e2ee.handle_thinking(req) + assert_eq("DeepSeek-V3.2-Speciale prefix default thinking=true", out.chat_template_kwargs.thinking, true) +end + +io.stdout:write(string.format("\n%d/%d passed\n", total - failures, total)) +os.exit(failures == 0 and 0 or 1)