Skip to content

parse_messages_json is key-order-sensitive: {"content":…,"role":…} silently generates from an empty user turn #772

Description

@john-rocky

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions