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
- 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.
- 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."
- 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".
- 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.
Affects: sqlite-ai 1.0.7
Summary
Calling
llm_text_generate()while the connection's model and context were setup 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:
The
ai_contextdoes not record whether the current context was created byllm_context_create_textgen()orllm_context_create_embedding(), sogeneration runs against an embedding-configured context and produces zero
tokens, which surfaces as
''.Reproduction
Single connection:
Measured output:
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 throughjson_extract()asNULLand lands in the table asblank 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 modeland its context (
ai_cleanup(ai, true, false)at src/sqlite-ai.c:2920). Thatbehavior is reasonable, but it is what makes this state so easy to reach, and it
is currently undocumented.
Proposed fix
ai_contextset byllm_context_create_textgen/llm_context_create_embedding/llm_context_create_chat.llm_text_generate(and symmetrically inllm_embed_generate), fail withSQLITE_MISUSEon a kind mismatch:"Current context was created for embeddings. Call llm_context_create_textgen() first."''with success — returnNULLor an error so callers can distinguish"empty output" from "nothing ran".
llm_model_load()that it releases the previously loaded modeland 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.