Describe the bug
When geniex_llm_apply_chat_template fails, the Android JNI bridge builds a result object holding an empty string instead of propagating the failure. LlmWrapper.applyChatTemplate therefore returns a successful Result, and a caller that passes formattedText on to generateStreamFlow silently generates from an empty prompt.
The failure is indistinguishable from a template that legitimately produced empty text, so nothing in the Kotlin API can detect it.
In bindings/android/app/src/main/cpp/llm_bridge_jni.cpp#L193-L212:
int32_t ret = geniex_llm_apply_chat_template(reinterpret_cast<geniex_LLM*>(handle), &input, &output);
...
if (ret < 0 || !output.formatted_text) {
jobject result = env->NewObject(cls, ctor, env->NewStringUTF(""));
...
}
ret is discarded. LlmApplyChatTemplateOutput carries only formattedText, so the error code cannot be surfaced through it either.
To Reproduce
- Load an LLM through the QAIRT plugin from the Android bindings.
- Call
applyChatTemplate in a way that fails inside the plugin — any input the plugin rejects will do; we hit it via a rejected role.
- Observe that the returned
Result is successful and formattedText is "".
- Pass that string to
generateStreamFlow.
Logs from such a run:
[src/llm.cpp:105:geniex_llm_apply_chat_template] LlmApplyChatTemplateInput(messages: 0x…, message_count: 2, …)
[plugins/qairt/src/llm.cpp:179:apply_chat_template] messages[0] has unknown role: …
[src/llm.cpp:111:geniex_llm_apply_chat_template] ErrorCode[-100001](Invalid input parameters or handle)
[src/llm.cpp:120:geniex_llm_generate] LlmGenerateInput(prompt_utf8: , input_ids: nullptr, input_ids_count: 0, …)
prompt: 1 tokens, first-turn BOS=true, ids start with [151643]
The model is prompted with the BOS token alone and emits degenerate output (STRACTSTRACT…). -100001 was logged, but no error reached the caller.
Expected behavior
A failing apply_chat_template should produce a failed Result, so getOrElse / onFailure can react to it. At minimum, the outcome should be distinguishable from a successful empty template.
Smartphone
- Device: Zebra TC501 (Qualcomm QCM6690, Hexagon v73)
- OS: Android 15, arm64-v8a
- GenieX: 0.6.1 (behaviour also present in 0.5.0)
- Runtime: QAIRT plugin, QAIRT 2.45.0.260326154327
- Model: Qwen3-1.7B w4a16 AI Hub bundle, cl4096
Additional context
We hit this while debugging a different bug — roles being corrupted when several ChatMessage were passed, which 0.6.1 has since fixed. That bug was hard to find precisely because of this one: the visible symptom was fluent nonsense from a model that appeared to be working, with no error anywhere in the Kotlin layer. It cost about a day of investigation.
The role bug is gone, but the silent-failure path remains and will mask the next template error the same way.
Our workaround is to treat an empty formattedText as a failure:
val prompt = wrapper.applyChatTemplate(messages, null, enableThinking)
.getOrElse { throw GenieXException("Failed to apply the chat template", it) }
.formattedText
if (prompt.isEmpty()) throw GenieXException("The chat template produced an empty prompt")
Happy to send a PR if you would like the failure branch to return an error instead.
Describe the bug
When
geniex_llm_apply_chat_templatefails, the Android JNI bridge builds a result object holding an empty string instead of propagating the failure.LlmWrapper.applyChatTemplatetherefore returns a successfulResult, and a caller that passesformattedTexton togenerateStreamFlowsilently generates from an empty prompt.The failure is indistinguishable from a template that legitimately produced empty text, so nothing in the Kotlin API can detect it.
In
bindings/android/app/src/main/cpp/llm_bridge_jni.cpp#L193-L212:retis discarded.LlmApplyChatTemplateOutputcarries onlyformattedText, so the error code cannot be surfaced through it either.To Reproduce
applyChatTemplatein a way that fails inside the plugin — any input the plugin rejects will do; we hit it via a rejected role.Resultis successful andformattedTextis"".generateStreamFlow.Logs from such a run:
The model is prompted with the BOS token alone and emits degenerate output (
STRACTSTRACT…).-100001was logged, but no error reached the caller.Expected behavior
A failing
apply_chat_templateshould produce a failedResult, sogetOrElse/onFailurecan react to it. At minimum, the outcome should be distinguishable from a successful empty template.Smartphone
Additional context
We hit this while debugging a different bug — roles being corrupted when several
ChatMessagewere passed, which 0.6.1 has since fixed. That bug was hard to find precisely because of this one: the visible symptom was fluent nonsense from a model that appeared to be working, with no error anywhere in the Kotlin layer. It cost about a day of investigation.The role bug is gone, but the silent-failure path remains and will mask the next template error the same way.
Our workaround is to treat an empty
formattedTextas a failure:Happy to send a PR if you would like the failure branch to return an error instead.