Skip to content

llm_text_generate() returns '' with no error when the active model/context is an embedding one #33

Description

@andinux

Affects: sqlite-ai 1.0.7

Summary

Calling llm_text_generate() while the connection's model and context were set
up for embeddings returns an empty string — no error, not even NULL.
The presence checks pass, because a model and a context do exist; nothing
checks that they are the right kind for the operation:

// src/sqlite-ai.c:1760 — llm_text_generate
if (llm_check_context(context) == false) return;   // only checks ai->ctx != NULL  (src/sqlite-ai.c:940)
...
if (!ai->model) {                                   // only checks presence
    sqlite_context_result_error(context, SQLITE_ERROR, "No model loaded");
    return;
}

The ai_context does not record whether the current context was created by
llm_context_create_textgen() or llm_context_create_embedding(), so
generation runs against an embedding-configured context and produces zero
tokens, which surfaces as ''.

Reproduction

Single connection:

SELECT llm_model_load('qwen3-1.7b-q4_k_m.gguf', 'gpu_layers=99');
SELECT llm_context_create_textgen();
SELECT length(llm_text_generate('Say hi'));        -- 13 (works)

SELECT llm_model_load('all-MiniLM-L6-v2-q8_0.gguf');   -- frees prior model AND context (ai_cleanup, src/sqlite-ai.c:2920)
SELECT llm_context_create_embedding('embedding_type=FLOAT32');
SELECT length(llm_embed_generate('Say hi'));       -- 1536 (works)

SELECT length(llm_text_generate('Say hi'));        -- 0. Empty string. No error.

Measured output:

textgen ok:                 13     -- chars returned
embed ok:                 1536     -- bytes, 384 x FLOAT32
textgen after embed load:    0     -- no error, no NULL, just ''

Why it matters

The one-model-per-connection rule is fine as a constraint — but violating it
must be loud. Today the failure is maximally quiet, and in the most natural
setting for this extension the empty string masquerades as data: inside a
trigger, '' flows through json_extract() as NULL and lands in the table as
blank AI columns. Nothing in the application ever learns that generation
stopped working. In a demo that reads as "the AI didn't work"; in production it
is silent data loss.

A contributing surprise: llm_model_load() silently frees the previous model
and its context (ai_cleanup(ai, true, false) at src/sqlite-ai.c:2920). That
behavior is reasonable, but it is what makes this state so easy to reach, and it
is currently undocumented.

Proposed fix

  1. Record the context kind at creation time, e.g. an enum on ai_context set by
    llm_context_create_textgen / llm_context_create_embedding /
    llm_context_create_chat.
  2. In llm_text_generate (and symmetrically in llm_embed_generate), fail with
    SQLITE_MISUSE on a kind mismatch:
    "Current context was created for embeddings. Call llm_context_create_textgen() first."
  3. Independently: a generation that produces zero tokens should never surface as
    '' with success — return NULL or an error so callers can distinguish
    "empty output" from "nothing ran".
  4. Document on llm_model_load() that it releases the previously loaded model
    and context on the connection.

(1)+(2) turn a silent-corruption bug into a one-line error at the exact call
site; (3) is defense in depth for any other path that yields no tokens.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions