Summary
parse_messages_json (cactus-engine/src/utils.h) locates "content" by searching after the "role" key:
size_t role_pos = json.find("\"role\"", pos);
...
size_t content_pos = json.find("\"content\"", role_end);
JSON object key order is not significant, and several serializers emit keys in non-fixed order. When a client sends {"content":"...","role":"user"} (content first), content_pos falls outside the message object and the content is silently dropped — the engine renders a template-only prompt (9 tokens for gemma-4) and the model replies along the lines of "Please provide the text you would like me to process." No error is surfaced.
Where it bites
Swift's JSONSerialization randomizes dictionary key order per process, so an iOS app calling cactus_complete with a dict-built messages array generates from an empty user turn on roughly half of launches — while working correctly on the other half. Python's json.dumps preserves insertion order, which is why Python-driven usage rarely hits this. (Observed on iOS 27 / iPhone 17 Pro at commit 1ace6d78; the tell in our logs was prefill_tokens: 9 instead of 20.)
Repro (no Swift needed)
from cactus.bindings import cactus as C
m = C.cactus_init("<any gemma-4 bundle>")
buf_ok = C.cactus_complete(m, '[{"role":"user","content":"What is 2+2?"}]', {"max_tokens": 32})
buf_bad = C.cactus_complete(m, '[{"content":"What is 2+2?","role":"user"}]', {"max_tokens": 32})
# buf_ok answers "4"; buf_bad answers "Please provide..." with prompt_tokens ≈ template-only
(cactus_complete above stands for the messages-JSON string path — passing a pre-serialized string, which the Python binding accepts.)
Suggested fix
Search for "content" within the message object's bounds (obj_start..obj_end) independently of the role key's position — the object boundaries are already computed a few lines above. The options parser (parse_inference_options_json) is order-insensitive already; only the messages parser has this constraint.
Summary
parse_messages_json(cactus-engine/src/utils.h) locates"content"by searching after the"role"key:JSON object key order is not significant, and several serializers emit keys in non-fixed order. When a client sends
{"content":"...","role":"user"}(content first),content_posfalls outside the message object and the content is silently dropped — the engine renders a template-only prompt (9 tokens for gemma-4) and the model replies along the lines of "Please provide the text you would like me to process." No error is surfaced.Where it bites
Swift's
JSONSerializationrandomizes dictionary key order per process, so an iOS app callingcactus_completewith a dict-built messages array generates from an empty user turn on roughly half of launches — while working correctly on the other half. Python'sjson.dumpspreserves insertion order, which is why Python-driven usage rarely hits this. (Observed on iOS 27 / iPhone 17 Pro at commit1ace6d78; the tell in our logs wasprefill_tokens: 9instead of 20.)Repro (no Swift needed)
(
cactus_completeabove stands for the messages-JSON string path — passing a pre-serialized string, which the Python binding accepts.)Suggested fix
Search for
"content"within the message object's bounds (obj_start..obj_end) independently of the role key's position — the object boundaries are already computed a few lines above. The options parser (parse_inference_options_json) is order-insensitive already; only the messages parser has this constraint.