Skip to content

Android JNI: applyChatTemplate failure is returned as an empty formattedText instead of an error #1432

Description

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

  1. Load an LLM through the QAIRT plugin from the Android bindings.
  2. Call applyChatTemplate in a way that fails inside the plugin — any input the plugin rejects will do; we hit it via a rejected role.
  3. Observe that the returned Result is successful and formattedText is "".
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions