Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ env:
GGUF_MODEL_DIR: tests/models/unsloth/gemma-3-270m-it-GGUF
GGUF_MODEL_NAME: gemma-3-270m-it-UD-IQ2_M.gguf
GGUF_MODEL_URL: https://huggingface.co/unsloth/gemma-3-270m-it-GGUF/resolve/main/gemma-3-270m-it-UD-IQ2_M.gguf
EMBED_MODEL_DIR: tests/models/Mungert/all-MiniLM-L6-v2-GGUF
EMBED_MODEL_NAME: all-MiniLM-L6-v2-q8_0.gguf
EMBED_MODEL_URL: https://huggingface.co/Mungert/all-MiniLM-L6-v2-GGUF/resolve/main/all-MiniLM-L6-v2-q8_0.gguf
WHISPER_MODEL_DIR: tests/models/ggerganov/whisper-tiny
WHISPER_MODEL_NAME: ggml-tiny.bin
WHISPER_MODEL_URL: https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
Expand All @@ -31,6 +34,8 @@ jobs:
outputs:
gguf-cache-key: gguf-${{ steps.meta.outputs.gguf-hash }}
gguf-model-path: ${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }}
embed-cache-key: embed-${{ steps.meta.outputs.embed-hash }}
embed-model-path: ${{ env.EMBED_MODEL_DIR }}/${{ env.EMBED_MODEL_NAME }}
whisper-cache-key: whisper-${{ steps.meta.outputs.whisper-hash }}
whisper-model-path: ${{ env.WHISPER_MODEL_DIR }}/${{ env.WHISPER_MODEL_NAME }}
audio-cache-key: audio-${{ steps.meta.outputs.audio-hash }}
Expand All @@ -43,20 +48,24 @@ jobs:
run: |
if command -v sha256sum >/dev/null 2>&1; then
gguf_hash=$(echo -n "${{ env.GGUF_MODEL_URL }}" | sha256sum | cut -d' ' -f1)
embed_hash=$(echo -n "${{ env.EMBED_MODEL_URL }}" | sha256sum | cut -d' ' -f1)
whisper_hash=$(echo -n "${{ env.WHISPER_MODEL_URL }}" | sha256sum | cut -d' ' -f1)
audio_hash=$(echo -n "${{ env.AUDIO_TEST_WAV_URL }}" | sha256sum | cut -d' ' -f1)
else
gguf_hash=$(echo -n "${{ env.GGUF_MODEL_URL }}" | shasum -a 256 | cut -d' ' -f1)
embed_hash=$(echo -n "${{ env.EMBED_MODEL_URL }}" | shasum -a 256 | cut -d' ' -f1)
whisper_hash=$(echo -n "${{ env.WHISPER_MODEL_URL }}" | shasum -a 256 | cut -d' ' -f1)
audio_hash=$(echo -n "${{ env.AUDIO_TEST_WAV_URL }}" | shasum -a 256 | cut -d' ' -f1)
fi
echo "gguf-hash=$gguf_hash" >> "$GITHUB_OUTPUT"
echo "embed-hash=$embed_hash" >> "$GITHUB_OUTPUT"
echo "whisper-hash=$whisper_hash" >> "$GITHUB_OUTPUT"
echo "audio-hash=$audio_hash" >> "$GITHUB_OUTPUT"

- name: Prepare directories
run: |
mkdir -p "${{ env.GGUF_MODEL_DIR }}"
mkdir -p "${{ env.EMBED_MODEL_DIR }}"
mkdir -p "${{ env.WHISPER_MODEL_DIR }}"
mkdir -p "${{ env.AUDIO_TEST_DIR }}"

Expand All @@ -74,6 +83,20 @@ jobs:
- name: Verify GGUF model
run: test -f "${{ env.GGUF_MODEL_DIR }}/${{ env.GGUF_MODEL_NAME }}"

- name: Restore embedding model cache
id: cache-embed
uses: actions/cache@v4
with:
path: ${{ env.EMBED_MODEL_DIR }}/${{ env.EMBED_MODEL_NAME }}
key: embed-${{ steps.meta.outputs.embed-hash }}

- name: Download embedding model
if: steps.cache-embed.outputs.cache-hit != 'true'
run: curl -L --fail --retry 3 "${{ env.EMBED_MODEL_URL }}" -o "${{ env.EMBED_MODEL_DIR }}/${{ env.EMBED_MODEL_NAME }}"

- name: Verify embedding model
run: test -f "${{ env.EMBED_MODEL_DIR }}/${{ env.EMBED_MODEL_NAME }}"

- name: Restore Whisper cache
id: cache-whisper
uses: actions/cache@v4
Expand Down Expand Up @@ -205,6 +228,7 @@ jobs:
- name: Prepare test asset directories
run: |
mkdir -p "${{ env.GGUF_MODEL_DIR }}"
mkdir -p "${{ env.EMBED_MODEL_DIR }}"
mkdir -p "${{ env.WHISPER_MODEL_DIR }}"
mkdir -p "${{ env.AUDIO_TEST_DIR }}"

Expand All @@ -214,6 +238,12 @@ jobs:
path: ${{ needs.download-models.outputs.gguf-model-path }}
key: ${{ needs.download-models.outputs.gguf-cache-key }}

- name: Restore embedding model cache
uses: actions/cache@v4
with:
path: ${{ needs.download-models.outputs.embed-model-path }}
key: ${{ needs.download-models.outputs.embed-cache-key }}

- name: Restore Whisper cache
uses: actions/cache@v4
with:
Expand Down
37 changes: 35 additions & 2 deletions API.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,14 +158,29 @@ Creates a new inference context with comma separated key=value configuration.

**Context must explicitly created before performing any AI operation!**

The context is classified from how it is actually configured, not from which constructor
you call. It is an *embedding* context when either `generate_embedding=1` was passed — so
`llm_context_create('generate_embedding=1,...')` behaves exactly like
`llm_context_create_embedding()` — or the model pools by default, which is how
BERT-family embedding models (`all-MiniLM`, `nomic-embed`) describe themselves in their
GGUF. Anything else is a *text generation* context. Forcing `pooling_type` yourself opts out of that second
test in both directions: on a generative model it does **not** make the context an
embedding one, and on an embedding model it suppresses the detection, so
`llm_text_generate()` is allowed again and returns an empty string. Only pooling you did
not ask for identifies the model as an embedding one — if you want the check, leave
`pooling_type` unset, or use `llm_context_create_embedding()`. Text generation and chat reject an
embedding context with `SQLITE_MISUSE`, since it produces no per-token logits to sample
from. Embedding generation is not restricted this way — it needs pooling rather than a
particular constructor, and checks for that directly.

## context_settings
The following keys are available in context_settings:

### General

| Key | Type | Meaning |
| ------------------------| -------- | ---------------------------------------------------------------- |
| `generate_embedding` | `1 or 0` | Force the model to generate embeddings. |
| `generate_embedding` | `1 or 0` | Force the model to generate embeddings. This is what marks the context as an *embedding* context, which makes `llm_text_generate()`, `llm_chat_respond()` and the `llm_chat()` vtab reject it. Also forces `pooling_type` to `mean`, and `n_ubatch` is clamped to `n_batch` for embedding contexts. |
| `normalize_embedding` | `1 or 0` | Force normalization during embedding generation (default to 1). |
| `json_output` | `1 or 0` | Force JSON output in embedding generation (default to 0). |
| `max_tokens` | `number` | Set a maximum number of tokens in input. If input is too large then an error is returned. |
Expand Down Expand Up @@ -218,7 +233,6 @@ The following keys are available in context_settings:

| Key | Type | Meaning |
| -------------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------ |
| `embeddings` | `1 or 0` | If `1`, extract embeddings (with logits). Used by the embedding preset. |
| `offload_kqv` | `1 or 0` | Offload KQV ops (incl. KV cache) to GPU. |
| `no_perf` | `1 or 0` | Disable performance timing. |
| `op_offload` | `1 or 0` | Offload host tensor ops to device. |
Expand Down Expand Up @@ -685,6 +699,12 @@ Leave `json_output` off when storing embeddings for
[sqlite-vector](https://github.com/sqliteai/sqlite-vector): the BLOB is already
layout-compatible, so insert it directly rather than wrapping it.

Requires a context that pools token embeddings. `llm_context_create_embedding()`
guarantees that; a context built another way also works whenever pooling resolves to
something other than `none` — embedding models (BERT-family, such as `all-MiniLM` or
`nomic-embed-text`) inherit mean pooling from the GGUF, so they need no extra settings. A
context with no pooling fails with *"Embedding generation requires pooling"*.

**Example:**

```sql
Expand All @@ -706,6 +726,13 @@ Generates a full-text completion based on input, with optional configuration pro

When a vision model is loaded via `llm_vision_load()`, you can pass one or more images as additional arguments. Images can be file paths (TEXT) or raw image data (BLOB). Supported image formats: JPG, PNG, BMP, GIF.

Requires a text generation context. Contexts from `llm_context_create_textgen()`,
`llm_context_create_chat()` and a plain `llm_context_create()` are all accepted — they are
configured identically. Calling this against an embedding context fails with
`SQLITE_MISUSE` rather than returning an empty string, which also covers embedding
*models*: on a BERT-family model every context is an embedding context, so generation is
rejected there no matter which constructor was used.

**Examples:**

```sql
Expand Down Expand Up @@ -750,6 +777,12 @@ Returns unique chat UUIDv7 value.
If no chat is explicitly created, one will be created automatically when needed —
but the UUID is needed for `llm_chat_save()` / `llm_chat_restore()`.

`llm_chat_respond()` and the `llm_chat()` virtual table require a text generation
context, because they decode and sample; running either against an embedding context
(`generate_embedding=1`) fails with `SQLITE_MISUSE`. `llm_chat_create()`,
`llm_chat_restore()` and `llm_chat_system_prompt()` only build up in-memory message state,
so they work regardless of the active context.

**Example:**

```sql
Expand Down
15 changes: 13 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,13 @@ GGUF_MODEL_NAME ?= gemma-3-270m-it-UD-IQ2_M.gguf
GGUF_MODEL_URL ?= https://huggingface.co/unsloth/gemma-3-270m-it-GGUF/resolve/main/gemma-3-270m-it-UD-IQ2_M.gguf
GGUF_MODEL_PATH := $(GGUF_MODEL_DIR)/$(GGUF_MODEL_NAME)

# an embedding model (BERT-family) is needed to cover the context-kind checks: it pools
# by default from its own GGUF, which a generative model never does
EMBED_MODEL_DIR ?= tests/models/Mungert/all-MiniLM-L6-v2-GGUF
EMBED_MODEL_NAME ?= all-MiniLM-L6-v2-q8_0.gguf
EMBED_MODEL_URL ?= https://huggingface.co/Mungert/all-MiniLM-L6-v2-GGUF/resolve/main/all-MiniLM-L6-v2-q8_0.gguf
EMBED_MODEL_PATH := $(EMBED_MODEL_DIR)/$(EMBED_MODEL_NAME)

WHISPER_MODEL_DIR ?= tests/models/ggerganov/whisper-tiny
WHISPER_MODEL_NAME ?= ggml-tiny.bin
WHISPER_MODEL_URL ?= https://huggingface.co/ggerganov/whisper.cpp/resolve/main/ggml-tiny.bin
Expand Down Expand Up @@ -245,6 +252,10 @@ $(GGUF_MODEL_PATH):
@mkdir -p $(GGUF_MODEL_DIR)
curl -L --fail --retry 3 -o $@ $(GGUF_MODEL_URL)

$(EMBED_MODEL_PATH):
@mkdir -p $(EMBED_MODEL_DIR)
curl -L --fail --retry 3 -o $@ $(EMBED_MODEL_URL)

$(WHISPER_MODEL_PATH):
@mkdir -p $(WHISPER_MODEL_DIR)
curl -L --fail --retry 3 -o $@ $(WHISPER_MODEL_URL)
Expand All @@ -255,14 +266,14 @@ $(AUDIO_TEST_WAV):

TEST_DEPS := $(TARGET)
ifeq ($(SKIP_UNITTEST),0)
TEST_DEPS += $(CTEST_BIN) $(GGUF_MODEL_PATH) $(WHISPER_MODEL_PATH) $(AUDIO_TEST_WAV)
TEST_DEPS += $(CTEST_BIN) $(GGUF_MODEL_PATH) $(EMBED_MODEL_PATH) $(WHISPER_MODEL_PATH) $(AUDIO_TEST_WAV)
endif

test: $(TEST_DEPS)
@echo "Running sqlite3 CLI smoke test (ensures .load works)..."
$(SQLITE3) ":memory:" -cmd ".bail on" ".load ./dist/ai" "SELECT ai_version();"
ifeq ($(SKIP_UNITTEST),0)
$(CTEST_BIN) --extension "$(TARGET)" --model "$(GGUF_MODEL_PATH)" --whisper-model "$(WHISPER_MODEL_PATH)" --audio "$(AUDIO_TEST_WAV)"
$(CTEST_BIN) --extension "$(TARGET)" --model "$(GGUF_MODEL_PATH)" --embed-model "$(EMBED_MODEL_PATH)" --whisper-model "$(WHISPER_MODEL_PATH)" --audio "$(AUDIO_TEST_WAV)"
else
@echo "Skipping C unit tests (SKIP_UNITTEST=$(SKIP_UNITTEST))."
endif
Expand Down
Loading
Loading