Repository navigation
Description
chat_template_kwargs is the standard way to pass per-request flags into a model's chat template (the mlx-lm server takes any JSON object and hands it to apply_chat_template as is). On SwiftLM it works for one key and one type (main @ ce9b717, M1 Max, mlx-community/Qwen2.5-0.5B-Instruct-4bit):
1. Any value that is not a boolean fails the whole request
curl -s localhost:5413/v1/chat/completions -H 'Content-Type: application/json' \
-d '{"messages":[{"role":"user","content":"hi"}],"max_tokens":16,"chat_template_kwargs":{"reasoning_effort":"low"}}'
# → HTTP 400
# {"error":{"message":"Invalid type for 'chat_template_kwargs.reasoning_effort': Expected to decode Bool but found a string instead.","type":"invalid_request_error","param":"chat_template_kwargs.reasoning_effort"}}A number fails the same way. The field is declared as [String: Bool]?:
https://github.com/SharpAI/SwiftLM/blob/ce9b717/Sources/SwiftLM/Server.swift#L4924
2. Any key other than enable_thinking is dropped silently
A boolean under another key decodes fine and then goes nowhere. The template context is built from enable_thinking alone, so {"chat_template_kwargs": {"thinking": true}} is accepted with 200 and the template never sees it:
https://github.com/SharpAI/SwiftLM/blob/ce9b717/Sources/SwiftLM/Server.swift#L2582-L2621
Why it matters
enable_thinking is Qwen's and Gemma's name for the switch. Other families SwiftLM runs name theirs differently, and their templates read them straight from the kwargs:
- DeepSeek-V3.1 reads
thinking(bool):{% if not thinking is defined %}{% set thinking = false %}and then opens<think>or closes it. On SwiftLM a V3.1 user cannot switch thinking per request at all. - gpt-oss reads
reasoning_effort("low"/"medium"/"high", a string) andmodel_identity. Both are a 400 here. - MiniMax-M2 reads
model_identity(string) the same way.
All three families are in mlx-swift-lm's model factory, and mlx-community ships 4-bit conversions of each (DeepSeek-V3.1-4bit, gpt-oss-20b-MXFP4-Q4, MiniMax-M2-4bit). mlx-lm, vLLM and llama-server all take chat_template_kwargs as a plain JSON object, so a request that works against any of them fails or degrades here with nothing in the log.
Fix
Decode chat_template_kwargs as arbitrary JSON (the AnyCodable type tool parameters already use), run it through the existing sanitizeForJinja, and merge it into the template context. enable_thinking keeps its precedence (top-level field, then kwargs, then --thinking) and the Gemma 4 tool-call override stays as it is. Requests that only send enable_thinking see no change.
I'm finishing this with parsing tests and a test that the extra keys reach the template context; PR coming shortly.
Activity
solderzzc commentedon Nov 6, 2026
Thanks for the report. The detailed table of which template families read which kwarg names made the scope clear, and it became #260, which is merged now (7376b6b). Closing this out. Follow-ups: #263 covers the precedence test, #264 covers the low-severity edge cases.