Skip to content

feat(llm): switch default embedder nomic → EmbeddingGemma 300M#1952

Merged
kovtcharov-amd merged 15 commits into
mainfrom
feat/embeddinggemma-embedder
Jul 8, 2026
Merged

feat(llm): switch default embedder nomic → EmbeddingGemma 300M#1952
kovtcharov-amd merged 15 commits into
mainfrom
feat/embeddinggemma-embedder

Conversation

@kovtcharov-amd

@kovtcharov-amd kovtcharov-amd commented Jul 7, 2026

Copy link
Copy Markdown
Collaborator

Why this matters

The current llama.cpp server can't load nomic-embed-text-v2-moe — GAIA's default embedder — so on any up-to-date Lemonade build, RAG, code-index, and agent-memory embedding silently break (the model fails to load; retrieval and memory stop working). This switches the default to EmbeddingGemma 300M, which loads cleanly on current llama.cpp. It's 768-dim (same as nomic), so index shapes and retrieval behavior are unchanged; existing indexes/caches re-embed automatically on next run.

EmbeddingGemma is a Lemonade custom user-model: /v1/pull registers it as user.embeddinggemma-300m-GGUF (checkpoint ggml-org/embeddinggemma-300M-GGUF:Q8_0, recipe=llamacpp, embedding=true — the flag sets the embeddings label explicitly, sidestepping the #1745 auto-label bug). Note that Lemonade then lists it under the stripped id embeddinggemma-300m-GGUF while /load and /embeddings accept either form — so the availability check is user.-prefix-tolerant to avoid a re-pull loop.

Live validation (Lemonade Server 10.9.0)

  • ensure_model_downloaded("user.embeddinggemma-300m-GGUF") resolves the registered model instantly (no re-pull hang)
  • gaia init --profile chat download path resolves both models cleanly
  • /embeddings returns 768-dim vectors under the user.-prefixed name
  • Full RAG round-trip: index a doc → correct chunk retrieved → correct answer, using EmbeddingGemma
  • Unit tests green (test_lemonade_client, test_memory_store, test_code_index, test_tool_call_priority, test_llamacpp_backend, test_rag), incl. new pull-shape + namespace-matcher tests; black/isort clean

Still to verify (couldn't run in this env)

  • Cold registration from empty — on 10.9.0 the model was already registered, so a truly-fresh /pull register-from-scratch wasn't exercised end-to-end.
  • RAG eval vs baseline — the eval's Claude judge needs the anthropic SDK (not installed) and a real ANTHROPIC_API_KEY (currently a dummy value); run gaia eval agent --category rag_quality --agent-type doc and --compare to tests/fixtures/eval_baselines/gemma-4-e4b-d71cd914/scorecard_rag_quality.json in an env with judge access.

The current llama.cpp server can't load nomic-embed-text-v2-moe, which
broke RAG, code-index, and agent-memory embedding on any up-to-date
Lemonade build. Default to EmbeddingGemma 300M instead.

Lemonade ships no built-in EmbeddingGemma, so it's registered as the
custom user-model user.embeddinggemma-300m-GGUF (checkpoint
ggml-org/embeddinggemma-300M-GGUF:Q8_0, recipe llamacpp) with the
embedding=true pull flag — which sets the embeddings label explicitly
and avoids the #1745 auto-label bug. It's 768-dim, same as nomic, so
FAISS index shapes and EMBEDDING_DIM stay unchanged.

Memory vectors are model-specific and the dim check can't catch a model
swap (both 768-dim), so memory now records the embedder name and
clears+re-embeds stale vectors when it changes. RAG/code-index caches
already self-invalidate on embedding_model change.
@github-actions github-actions Bot added documentation Documentation changes devops DevOps/infrastructure changes rag RAG system changes llm LLM backend changes tests Test changes performance Performance-critical changes agents agent::emr Medical-intake (EMR) agent changes labels Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Verdict: Approve — safe to merge as-is; the notes below are optional polish, not blockers.

This swaps GAIA's default embedder from nomic-embed-text-v2-moe (which the current llama.cpp server can't load, silently breaking RAG / code-index / agent-memory) to EmbeddingGemma 300M. Both are 768-dim, so index shapes and retrieval behavior are unchanged. The change is thorough: the new model is registered as a user. custom model with the correct pull shape (checkpoint + recipe=llamacpp + explicit embedding=true to sidestep the #1745 auto-label bug), agent-memory auto-clears stale vectors on the model change so they re-embed, and every doc surface that named the old model is updated in lockstep.

The bottom line reviewers should know: this only registers the pull shape — the live Lemonade contract (fresh cold-pull on a real box) still needs the boundary test the author left unchecked in the test plan. Nothing in the diff can prove that end-to-end, which is exactly the right call to flag.

No security concerns.

🔍 Technical details

🟢 Minor / nits

1. code_index/sdk.py ignores the ensure_model_downloaded() return value (src/gaia/code_index/sdk.py:668)
The RAG SDK raises loudly when registration returns False (rag/sdk.py:930), but the code-index path discards the return. In practice the subsequent load_model() still fails loudly with an actionable RuntimeError, so this isn't a silent-fallback violation — just an inconsistency. Optional tightening for parity:

            if mr and self.config.embedding_model.startswith("user."):
                if not self._llm_client.ensure_model_downloaded(
                    self.config.embedding_model,
                    checkpoint=mr.checkpoint,
                    recipe=mr.recipe,
                    embedding=mr.embedding,
                ):
                    raise RuntimeError(
                        f"Failed to register/download embedding model "
                        f"{self.config.embedding_model!r}. Ensure Lemonade Server "
                        f"is running, then retry."
                    )

2. Procedures embeddings are cleared but not re-backfilled on the migration run (src/gaia/agents/base/memory.py:445)
clear_all_embeddings() NULLs both the knowledge and procedures tables, but Step 3's _backfill_embeddings() only re-embeds knowledge (get_items_without_embeddings reads the knowledge table). Procedures rely on skill-synthesis to re-embed on a later pass, so immediately after the nomic→Gemma migration, procedural-memory recall is degraded until skill-synthesis reruns. This is off the hot path and the subsystem is experimental, so it's acceptable — but worth a one-line comment at the clear site noting procedures re-embed lazily via skill-synthesis, not via this backfill, so a future reader doesn't assume Step 3 covers them.

3. EMR disk-space note (docs/guides/emr.mdx) — the per-model row dropped from 522 MB → ~334 MB but the "at least 25 GB free" guidance is unchanged. Fine (headroom), just noting the two lines were reviewed together.

Strengths

  • Contract-validity test, not just invocationtest_pull_embedding_model_request_shape asserts the outgoing /pull body carries user. prefix + checkpoint + recipe + embedding=true together, exactly the CLAUDE.md "mocks prove the call is valid" bar, and directly guards the gaia init --profile npu fails with status 400 (missing user. prefix for model name) #1655/fix(npu): default 32k context on NPU profile + resolve 4096↔32768 runtime mismatch #1745 regressions.
  • Model-aware migration — clearing vectors on a model change (keyed on stored embedding_model in settings, since the 768-dim check can't distinguish the two models) is the correct fix for the "old vectors silently mismatch new queries" trap, and it ships with a unit test that verifies rows survive while embeddings go NULL.
  • Doc sync is complete — glossary, code-index, emr, RAG/memory SDK specs, CLI profiles table, and the memory-architecture spec all updated in one pass, with migration <Note> blocks explaining the auto-reindex. No surface left contradicting another.
  • Fails loud — RAG registration raises with an actionable message rather than falling back to nomic.

Validated against a live Lemonade Server (10.9.0): EmbeddingGemma is a
custom user-model, so /v1/pull registers it as user.embeddinggemma-300m-GGUF
but /v1/models lists it under the stripped id embeddinggemma-300m-GGUF
(/load and /embeddings accept either form). The availability check in
ensure_model_downloaded compared raw strings, so it never matched the
registered model and re-pulled on every call — an observed hang.

Add a user.-prefix-tolerant name matcher and use it in the availability,
download-wait, and loaded-model checks. Runtime validation now passes:
ensure_model_downloaded returns immediately, gaia init resolves the
embedder, and a full RAG index→retrieve→answer round-trip works.

Also refines the embedder docs (agent-ui/code-index/rag) to match actual
cache-invalidation behavior.
kovtcharov-amd pushed a commit that referenced this pull request Jul 7, 2026
Generalize the embedder hardware test to validate EmbeddingGemma 300M on both
backends GAIA uses: the GGUF/llamacpp variant (user.embeddinggemma-300m-GGUF,
the non-NPU default replacing nomic, #1952) and the FLM/NPU variant
(embed-gemma-300m-FLM, #1744). The vector-level checks — 768-dim, finite,
non-zero, deterministic, distinct-texts-differ — are shared and parametrized
across both backends; the chat+embedder co-residency check stays FLM-specific.

Each backend gates on the live Lemonade catalog and skips when absent, so the
GGUF variant runs on GPU/Vulkan boxes and the stx CI runner while FLM runs on
Ryzen AI NPU runners — neither produces a false failure where its model isn't
present. Verified locally: GGUF variant passes against a real EmbeddingGemma
GGUF model (4 passed), FLM variant skips cleanly with no server error.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🟡 CodeIndexSDK never recognizes the new embedder as already loaded — it reloads it on every gaia-code index call.

_init_embedder builds running from the health endpoint's model IDs, then checks self.config.embedding_model not in running. But Lemonade reports user.-registered models under their stripped id (embeddinggemma-300m-GGUF), while self.config.embedding_model is user.embeddinggemma-300m-GGUF. The membership test always fails, so load_model fires on every invocation even when the embedder is sitting idle and ready. The _model_ids_match helper added in this same PR exists precisely to handle this comparison — it just wasn't applied here.

RAGSDK doesn't have this issue because it unconditionally unloads-then-reloads the embedder in a scoped swap; CodeIndexSDK's "skip if already loaded" optimization is what broke.

🔍 Technical details

src/gaia/code_index/sdk.py:677-683 — newly added block:

health = self._llm_client.health_check()
running = [m.get("id", "") for m in health.get("all_models_loaded", [])]
if self.config.embedding_model not in running:   # ← raw string compare
    self._llm_client.load_model(
        self.config.embedding_model,
        llamacpp_args="--ubatch-size 2048 --split-mode none",
    )

Fix — swap the membership test for _model_ids_match:

from gaia.llm.lemonade_client import _model_ids_match

if not any(_model_ids_match(rid, self.config.embedding_model) for rid in running):
    self._llm_client.load_model(...)

Secondary: ensure_model_downloaded's return value (line 668) is silently discarded. RAGSDK raises RuntimeError on False; CodeIndexSDK should too, or a failed registration produces a confusing downstream embedding error rather than the actionable "Failed to register/download…" message.

kovtcharov
kovtcharov previously approved these changes Jul 7, 2026
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🟡 code_index/sdk.py silently swallows a failed embedder registration — if the user.embeddinggemma-300m-GGUF download fails, gaia-code index will proceed and crash with a confusing embedder error instead of the actionable "Failed to register/download" message you wired up in rag/sdk.py.

The rag/sdk.py block has a comment that says exactly this ("Fail loudly if registration fails") and checks the return value. The identical new block in code_index/sdk.py doesn't.

🔍 Technical details

src/gaia/rag/sdk.py:501-510 (correct pattern):

if not self.llm_client.ensure_model_downloaded(
    self.config.embedding_model,
    checkpoint=mr.checkpoint,
    recipe=mr.recipe,
    embedding=mr.embedding,
):
    raise RuntimeError(
        f"Failed to register/download embedding model ..."
    )

src/gaia/code_index/sdk.py:667-673 (missing the check):

if mr and self.config.embedding_model.startswith("user."):
    self._llm_client.ensure_model_downloaded(   # ← return value ignored
        self.config.embedding_model,
        checkpoint=mr.checkpoint,
        recipe=mr.recipe,
        embedding=mr.embedding,
    )

Fix: mirror the RAG pattern — check the bool return and raise a RuntimeError with an actionable message (referencing gaia init --profile chat or the code-index docs).

…ingGemma

Two fixes for the embedder switch:

1. _download_models passed checkpoint/recipe/embedding=None to
   ensure_model_downloaded for EVERY model, including built-ins — which
   broke test_npu_profile_pulls_builtin_model_without_recipe and risked
   Lemonade treating a built-in pull as a custom registration (#1655).
   Only user.-namespaced models now carry those kwargs; built-ins are
   pulled by name.

2. EmbeddingGemma is validated on Lemonade v10.9.0; older bundled
   llama.cpp builds fail to load it. Bump LEMONADE_VERSION to 10.9.0 and
   floor the chat/rag/all init profiles at 10.9.0 so init fails loudly
   instead of the embedder failing at first RAG index. reset-lemonade.ps1
   fallback bumped to match.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🟡 The code_index embedder "already loaded?" guard doesn't use _model_ids_match, so EmbeddingGemma triggers a spurious load_model call on every cold init of the code-index embedder.

The health endpoint returns the model's stripped id (embeddinggemma-300m-GGUF), but self.config.embedding_model is user.embeddinggemma-300m-GGUF. The plain not in check can never match, so load_model is called even when the embedder is already loaded. The rest of the PR applied _model_ids_match precisely to handle this strip, but missed this spot. Whether the unneeded load_model call evicts-then-reloads the model or is a no-op depends on Lemonade's behaviour, but it's a regression either way.

🔍 Technical details

src/gaia/code_index/sdk.py:678-680 (existing code the PR left unchanged):

running = [m.get("id", "") for m in health.get("all_models_loaded", [])]
if self.config.embedding_model not in running:   # <-- plain `in`, no prefix strip
    self._llm_client.load_model(...)

The _ensure_model_loaded path in lemonade_client.py was updated to use _model_ids_match (diff line +2730) for exactly this reason, confirming the health endpoint returns the stripped form. The same fix is needed here:

from gaia.llm.lemonade_client import _model_ids_match
...
if not any(_model_ids_match(r, self.config.embedding_model) for r in running):
    self._llm_client.load_model(...)

The LEMONADE_VERSION bump to 10.9.0 left hardcoded 10.8.1 references in
docs/cpp/setup.mdx, docs/guides/npu.mdx, and cpp/README.md, which
util/check_doc_versions.py flags as mismatches (fails the validate +
code-quality jobs). Sync them to 10.9.0.
@github-actions github-actions Bot added the cpp label Jul 7, 2026
…mbedder

# Conflicts:
#	src/gaia/agents/base/memory.py
#	src/gaia/agents/base/memory_store.py
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

🔴 registry.py still has the old nomic model name — RAG will fail for all GPU/CPU users of gaia chat.

src/gaia/agents/registry.py:222 was not updated in this push and still reads:

DEFAULT_EMBEDDING_MODEL = "nomic-embed-text-v2-moe-GGUF"

The ChatAgent calls get_embedding_model_for_device(config.device) (line 267 of hub/agents/python/chat/gaia_agent_chat/agent.py) which, for GPU/CPU/unknown devices, returns registry.DEFAULT_EMBEDDING_MODEL. That means it hands the old nomic model id to the RAG SDK — which the current llama.cpp server can't load — instead of user.embeddinggemma-300m-GGUF. Document indexing and RAG will break on the default GPU profile even after gaia init --profile chat installs the new model.

tests/unit/test_npu_flm_embedder.py:38 also asserts the old name, so it continues to pass while testing the wrong thing.

🔍 Technical details

Root cause: registry.py has its own copy of the default embedding model name that was not synced with the lemonade_client.py DEFAULT_EMBEDDING_MODEL rename.

Files to fix:

src/gaia/agents/registry.py:222 — import from lemonade_client instead of duplicating:

# before
DEFAULT_EMBEDDING_MODEL = "nomic-embed-text-v2-moe-GGUF"

# after
from gaia.llm.lemonade_client import DEFAULT_EMBEDDING_MODEL  # "user.embeddinggemma-300m-GGUF"

registry.py:297-298 docstring and hub/agents/python/chat/gaia_agent_chat/agent.py:266 comment still say "nomic embedder" — update to EmbeddingGemma.

tests/unit/test_npu_flm_embedder.py:35-38 — the parametrized test test_non_npu_uses_nomic asserts == "nomic-embed-text-v2-moe-GGUF" and will keep passing while testing the wrong value:

# before
assert get_embedding_model_for_device(device) == "nomic-embed-text-v2-moe-GGUF"

# after (import EMBEDDING_MODEL from registry or lemonade_client)
assert get_embedding_model_for_device(device) == "user.embeddinggemma-300m-GGUF"

Ovtcharov added 3 commits July 7, 2026 17:02
…mbedder

# Conflicts:
#	.github/workflows/test_embeddings.yml
#	.github/workflows/test_rag.yml
#1761 shipped the NPU FLM hardware test as tests/test_gemma_embedder_hw.py
but its workflow (test_npu_embedder.yml) and the file's own docstring both
reference tests/test_npu_flm_embedder_hw.py — so the job failed with
'file or directory not found'. Rename the file to the intended name.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🟡 code_index/sdk.py ignores the return value of ensure_model_downloaded, so a failed EmbeddingGemma registration falls silently through to load_model, giving users a confusing "model not found" error instead of an actionable message. The equivalent block in rag/sdk.py correctly checks the return value and raises a RuntimeError pointing the user to gaia init --profile rag — the code-index path should do the same.

🔍 Technical details

src/gaia/code_index/sdk.py:668 (new block, this PR):

if mr and self.config.embedding_model.startswith("user."):
    self._llm_client.ensure_model_downloaded(   # return value discarded
        self.config.embedding_model,
        checkpoint=mr.checkpoint,
        recipe=mr.recipe,
        embedding=mr.embedding,
    )

Compare to src/gaia/rag/sdk.py:502 (same PR, correct pattern):

if mr and self.config.embedding_model.startswith("user."):
    if not self.llm_client.ensure_model_downloaded(...):
        raise RuntimeError(
            f"Failed to register/download embedding model ..."
        )

Fix: mirror the rag/sdk.py guard — check not self._llm_client.ensure_model_downloaded(...) and raise a RuntimeError with a message directing the user to gaia-code index or gaia init --profile chat.

Two failures introduced on main by the #1748 (embedding cache) × #1761
(per-instance embedder) interaction and #1371 (schedule), which this branch
inherits via the merge:

1. MemoryMixin._embed_text/_get_embedder referenced self._embedding_model
   directly, which is only set in init_memory — so the embedding-cache unit
   tests (bare mixin) failed with AttributeError, and the cache key used the
   module default while the embed call used the instance model (mismatched for
   the NPU FLM embedder). Resolve the active model/dim via helpers that fall
   back to the module default, and key the cache by the same resolved model.

2. schedule/daemon.py reimported datetime inside next_fire_time (W0404) —
   already imported module-level. Removed.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🟡 The ensure_model_downloaded return value isn't checked in code_index/sdk.py — the parallel path in rag/sdk.py correctly raises a RuntimeError on failure, but code-index silently continues. If EmbeddingGemma registration fails (returns False), the code falls through to load_model, which then fails with "Could not load embedding model — Verify Lemonade Server is running" — a misleading diagnosis when the actual problem is a failed registration.

🔍 Technical details

rag/sdk.py:1147–1157 checks the return value and raises with a clear actionable message:

if not self.llm_client.ensure_model_downloaded(
    self.config.embedding_model,
    checkpoint=mr.checkpoint,
    recipe=mr.recipe,
    embedding=mr.embedding,
):
    raise RuntimeError(
        f"Failed to register/download embedding model ..."
    )

code_index/sdk.py:667–673 ignores the return value:

if mr and self.config.embedding_model.startswith("user."):
    self._llm_client.ensure_model_downloaded(   # ← return value dropped
        self.config.embedding_model,
        checkpoint=mr.checkpoint,
        recipe=mr.recipe,
        embedding=mr.embedding,
    )

Fix: mirror rag/sdk.py — guard on the return value and raise before falling through to load_model.

Ovtcharov added 2 commits July 7, 2026 17:49
Root cause of the "Server failed to start" infra failures in the api,
embeddings, and rag workflows: they launched `lemonade-server serve` inside
a Start-Job, but the `lemonade-server` shim was removed in Lemonade v10.5 —
on 10.9.0 it no longer exists, so the command failed instantly, the 60s
health poll timed out, and the finally block discarded the server output
(zero diagnostics).

Fix: route all three through the shared start-lemonade.ps1 launcher (already
used by the 5 healthy Lemonade workflows), which launches LemonadeServer.exe
with a bare --port, captures stdout/stderr to log files, waits for health,
and dumps those logs on failure. Restructure each into start → test →
always-cleanup steps so the detached server's LEMONADE_PROCESS_ID (exported
via GITHUB_ENV) is visible to the test and teardown steps, and surface the
server logs whenever model setup/verify fails.

Adds a -NoModel switch to start-lemonade.ps1 (start server only) so callers
that register a custom user-model themselves (the EmbeddingGemma embedder via
LemonadeClient) can reuse the robust launcher, and dumps server logs on a
startup timeout.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🟡 CodeIndexSDK silently swallows an embedder registration failure where RAGSDK raises — a user who hits this gets a confusing downstream load error instead of the actionable "Failed to register/download" message.

🔍 Technical details

src/gaia/code_index/sdk.py:667–673 calls ensure_model_downloaded() without checking the return value:

if mr and self.config.embedding_model.startswith("user."):
    self._llm_client.ensure_model_downloaded(   # return value discarded
        self.config.embedding_model,
        checkpoint=mr.checkpoint,
        recipe=mr.recipe,
        embedding=mr.embedding,
    )

When that returns False, execution falls through to load_model(), which fails with a generic transport error. Contrast with src/gaia/rag/sdk.py:625–636, which was updated in this same PR to raise a clear RuntimeError on failure. The fix mirrors the RAG pattern:

if mr and self.config.embedding_model.startswith("user."):
    if not self._llm_client.ensure_model_downloaded(
        self.config.embedding_model,
        checkpoint=mr.checkpoint,
        recipe=mr.recipe,
        embedding=mr.embedding,
    ):
        raise RuntimeError(
            f"Failed to register/download embedding model "
            f"{self.config.embedding_model!r}. Ensure Lemonade Server "
            f"is running, then retry (or run `gaia init --profile rag`)."
        )

…run)

Follow-up to the launcher fix. The multi-step split (start server in one
step, test in the next) failed because GitHub Actions reaps a step's child
processes at step end — the detached LemonadeServer.exe was gone by the test
step ("connection refused" on :13305). The healthy workflows (examples, npu)
run start-lemonade.ps1 + tests in a SINGLE step for exactly this reason.

Collapse the api/embeddings/rag jobs back to one step each: start-lemonade.ps1
(-NoModel) then register/verify + pytest in the same shell, with a finally
that stops the server. start-lemonade.ps1 now also sets $env:LEMONADE_PROCESS_ID
on the live process env (not just GITHUB_ENV, which only reaches later steps)
so same-step cleanup works.

The prior push already proved the launcher itself works: CI logs showed
"✅ LEMONADE SERVER READY" in ~2s where the old `lemonade-server serve` shim
timed out.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🔴 CodeIndexSDK._init_embedder() always reloads EmbeddingGemma, even when it's already running. Lemonade's /health response lists models under their stripped id (embeddinggemma-300m-GGUF), but the availability check compares against the full user.embeddinggemma-300m-GGUF — a prefix the stripped form never matches. The guard that was supposed to skip load_model for an already-running embedder is now a no-op, so every fresh CodeIndexSDK instance issues an unnecessary load_model call. This regression didn't exist with nomic-embed-text-v2-moe-GGUF (no user. prefix), and _model_ids_match was added in this PR specifically to solve this mismatch — but wasn't applied at this check.

🟡 Same function also silently drops a False return from ensure_model_downloaded, violating the fail-loudly rule. RAGSDK raises RuntimeError on that path; CodeIndexSDK proceeds silently, shifting the error to a less informative downstream failure.

🔍 Technical details

🔴 user. prefix mismatch in _init_embeddersrc/gaia/code_index/sdk.py:678-679

running = [m.get("id", "") for m in health.get("all_models_loaded", [])]
if self.config.embedding_model not in running:

running holds bare ids ("embeddinggemma-300m-GGUF"); self.config.embedding_model is "user.embeddinggemma-300m-GGUF". The in test always returns False, so load_model fires unconditionally. Fix: import _model_ids_match from the same module and use it:

from gaia.llm.lemonade_client import MODELS, LemonadeClient, _model_ids_match
...
if not any(_model_ids_match(r, self.config.embedding_model) for r in running):

🟡 Silent failure on download — src/gaia/code_index/sdk.py:667-673

if mr and self.config.embedding_model.startswith("user."):
    self._llm_client.ensure_model_downloaded(...)  # return value discarded

RAGSDK raises a named RuntimeError with "Ensure Lemonade Server is running and reachable…" if this returns False. CodeIndexSDK silently continues, deferring to the load_model step where the error will be less informative. Apply the same guard here.

The RAG job reached the embeddings round-trip (server up, models pulled,
embedder registered+loaded) but failed with a raw KeyError: 'data' — a freshly
loaded embedder can briefly return a non-standard body. Retry the embeddings
call up to 5x and, on persistent failure, print the actual response instead of
a KeyError so the fault is diagnosable.
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown
Contributor

🟡 The "is it already running?" check in the code-index embedder init breaks with the new user.-prefixed model, causing an unnecessary load_model call every time the embedder is initialized (even when it's already loaded). This didn't affect the old nomic-embed-text-v2-moe-GGUF because Lemonade reported that name exactly; for user.-registered models Lemonade lists the stripped id (embeddinggemma-300m-GGUF), so the exact not in comparison never matches. The _model_ids_match helper added in this PR handles precisely this case — it just isn't used here.

🔍 Technical details

src/gaia/code_index/sdk.py lines 677-683:

running = [m.get("id", "") for m in health.get("all_models_loaded", [])]
if self.config.embedding_model not in running:
    self._llm_client.load_model(
        self.config.embedding_model,
        llamacpp_args="--ubatch-size 2048 --split-mode none",
    )

With embedding_model = "user.embeddinggemma-300m-GGUF", if the health endpoint returns entries with the stripped id "embeddinggemma-300m-GGUF" (consistent with how _model_ids_match's own docstring describes /v1/models — and with _ensure_model_loaded in lemonade_client.py needing _model_ids_match for the same health data), the check always resolves to True and load_model is called unconditionally. Because _init_embedder guards against double-init with if self._embedder is not None: return, this fires at most once per CodeIndexSDK instance; but that's still a redundant reload on an already-warm embedder.

The fix — import and apply _model_ids_match that was already added in this PR:

from gaia.llm.lemonade_client import MODELS, LemonadeClient, _model_ids_match
...
if not any(_model_ids_match(m, self.config.embedding_model) for m in running):
    self._llm_client.load_model(...)

Note: rag/sdk.py avoids the same trap by always doing unload_model + load_model (no guard), so it isn't affected.

… sticks

Root cause of the RAG/embeddings 501 'This server does not support embeddings.
Start it with --embeddings': a shared runner had embeddinggemma-300m-GGUF
already registered WITHOUT the embeddings label (stale registration from a
prior job / #1745 auto-label bug), so ensure_model_downloaded short-circuited
on 'already downloaded' and never applied the label — llama-server then loaded
the model without --embeddings.

For custom embedding models, delete any existing registration first so the
subsequent pull re-registers with embedding=True and the label is guaranteed.
Verified locally: delete + re-register + /v1/embeddings returns dim=768.
@kovtcharov-amd kovtcharov-amd enabled auto-merge July 8, 2026 02:40
@kovtcharov-amd kovtcharov-amd added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 7b65379 Jul 8, 2026
85 checks passed
@kovtcharov-amd kovtcharov-amd deleted the feat/embeddinggemma-embedder branch July 8, 2026 02:41
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agent::emr Medical-intake (EMR) agent changes agents cpp devops DevOps/infrastructure changes documentation Documentation changes installer Installer changes llm LLM backend changes performance Performance-critical changes rag RAG system changes tests Test changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants