From d8ec7a192c1c7f80c2ebccfcea01dc3fc7df7492 Mon Sep 17 00:00:00 2001 From: Eljaja Date: Mon, 31 Aug 2026 13:23:45 +0300 Subject: [PATCH] Select the embedding provider and model by configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `openai_embed_batch` hardcoded the `/openai/` gateway location and `text-embedding-3-large`, so remote embeddings could only reach api.openai.com. Build the base URL from `embeddingprovider` instead — the same `//` convention `lib_llm_ext.AIProvider` already uses for chat, so the existing Nginx locations supply each provider's key — and take the model from a new `embedding_model`. `initKnowledge` tested for `OpenAI` and fell through to `Local`, which loaded the SentenceTransformer model even for a remote provider. Test for `Local` instead, mirroring the dispatch in `embed`. `scripts/omegaclaw` pinned the embedding backend to `Local` for every provider but `OpenAI` and never passed a model, so the launcher could not express this. `EMBEDDING_PROVIDER` and `EMBEDDING_MODEL` now override it; with neither set the arguments it emits are unchanged. Defaults are unchanged. Verified against ASI Cloud with `embeddingprovider=ASICloud embedding_model=WhereIsAI/UAE-Large-V1`: remember and query round-trip through the gateway, recall is correct, and the local model is never loaded — agent RSS 155 MiB instead of 1.7 GiB. --- Autotests/test_openai_runtime_embeddings.py | 19 ++++++++++++++++--- config/config.yaml | 4 +++- docs/reference-configuration.md | 3 ++- docs/reference-internals-extension-points.md | 8 ++++++-- docs/reference-skills-memory.md | 2 +- scripts/omegaclaw | 12 ++++++++++++ src/loop.metta | 6 +++--- src/rag.py | 8 +++++--- 8 files changed, 48 insertions(+), 14 deletions(-) diff --git a/Autotests/test_openai_runtime_embeddings.py b/Autotests/test_openai_runtime_embeddings.py index 1bf6bf30..e5c3e557 100644 --- a/Autotests/test_openai_runtime_embeddings.py +++ b/Autotests/test_openai_runtime_embeddings.py @@ -9,12 +9,13 @@ MEMORY_METTA_PATH = REPO_ROOT / "src" / "memory.metta" -def load_rag_module(monkeypatch): +def load_rag_module(monkeypatch, config=None, expected_model="text-embedding-3-large"): created_clients = [] + settings = {"GATEWAY_URL": "http://gateway:8080", **(config or {})} class FakeEmbeddings: def create(self, *, model, input): - assert model == "text-embedding-3-large" + assert model == expected_model assert input == ["runtime probe"] return types.SimpleNamespace( data=[types.SimpleNamespace(embedding=[0.1, 0.2, 0.3])] @@ -32,7 +33,7 @@ def __init__(self, *, base_url=None, api_key=None): chromadb_module = types.ModuleType("chromadb") config_module = types.ModuleType("config") config_module.config_get_by_key = ( - lambda key, default=None: "http://gateway:8080" if key == "GATEWAY_URL" else default + lambda key, default=None: settings.get(key, default) ) llm_module = types.ModuleType("lib_llm_ext") llm_module.initLocalEmbedding = lambda: None @@ -59,6 +60,18 @@ def test_runtime_openai_embedding_uses_proxy_and_returns_single_vector(monkeypat assert clients[0].api_key == "unused" +def test_runtime_embedding_uses_the_configured_provider_and_model(monkeypatch): + rag, clients = load_rag_module( + monkeypatch, + config={"embeddingprovider": "ASICloud", + "embedding_model": "WhereIsAI/UAE-Large-V1"}, + expected_model="WhereIsAI/UAE-Large-V1", + ) + + assert rag.openai_embed("runtime probe") == [0.1, 0.2, 0.3] + assert clients[0].base_url == "http://gateway:8080/asicloud/" + + def test_memory_metta_routes_openai_embeddings_to_rag_wrapper(): memory_metta = MEMORY_METTA_PATH.read_text(encoding="utf-8") diff --git a/config/config.yaml b/config/config.yaml index 5a505504..5a1e8936 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -34,8 +34,10 @@ maxRecallItems: 20 maxEpisodeRecallLines: 20 # Tail of `memory/history.metta` included in the prompt (chars) maxHistory: 30000 -# `Local` (Python-side model) or `OpenAI` (requires `OPENAI_API_KEY`) +# `Local` (Python-side model) or the id of a provider serving OpenAI-compatible embeddings: `OpenAI`, `ASICloud` embeddingprovider: Local +# Model asked of a non-`Local` embeddingprovider +embedding_model: "text-embedding-3-large" # Policy diff --git a/docs/reference-configuration.md b/docs/reference-configuration.md index ee4ffe9c..565912aa 100644 --- a/docs/reference-configuration.md +++ b/docs/reference-configuration.md @@ -31,7 +31,8 @@ This reads a command-line override via `argk` (`name=value` on the MeTTa command | `maxRecallItems` | 20 | Items returned by `query`. | | `maxEpisodeRecallLines` | 20 | Lines returned by `episodes`. | | `maxHistory` | 30000 (chars) | Tail of `memory/history.metta` included in the prompt. | -| `embeddingprovider` | `Local` | `Local` (Python-side model) or `OpenAI`. | +| `embeddingprovider` | `Local` | `Local` (Python-side model), or the id of a provider that serves an OpenAI-compatible `/embeddings` endpoint — `OpenAI` and `ASICloud` are known to. The gateway supplies that provider's key. | +| `embedding_model` | `text-embedding-3-large` | Model asked of a non-`Local` `embeddingprovider`. | ## Channels (`src/channels.metta`, `initChannels`) diff --git a/docs/reference-internals-extension-points.md b/docs/reference-internals-extension-points.md index c0ce4acc..d74b10fd 100644 --- a/docs/reference-internals-extension-points.md +++ b/docs/reference-internals-extension-points.md @@ -53,10 +53,14 @@ In `src/memory.metta`, the `embed` function dispatches on `embeddingprovider`: (= (embed $str) (if (== (embeddingprovider) Local) (py-call (lib_llm_ext.useLocalEmbedding (string-safe $str))) - (useGPTEmbedding (string-safe $str)))) + (py-call (rag.openai_embed (string-safe $str))))) ``` -To add a new backend, add a branch and implement the Python function. +Any value other than `Local` is a provider id: the remote branch posts +`embedding_model` to `//`, the +location that already injects that provider's key. Switching vendor is +configuration, not code — provided the vendor serves embeddings at all. It +changes the vector space, so reset the ChromaDB store when you do. ## Change the reasoning library diff --git a/docs/reference-skills-memory.md b/docs/reference-skills-memory.md index 2658f8fb..a9e93d63 100644 --- a/docs/reference-skills-memory.md +++ b/docs/reference-skills-memory.md @@ -30,7 +30,7 @@ The result of the ChromaDB write (internally). The agent treats a successful cal ### Notes / Limits - Text is passed through `string-safe` before embedding, which escapes newlines, quotes, and apostrophes. -- Embedding provider is selected by `embeddingprovider` (`Local` or `OpenAI`). +- Embedding provider is selected by `embeddingprovider`, the model by `embedding_model`. - Nothing deduplicates automatically — repeated `remember` calls store multiple items. --- diff --git a/scripts/omegaclaw b/scripts/omegaclaw index 76a21e02..70635d98 100755 --- a/scripts/omegaclaw +++ b/scripts/omegaclaw @@ -497,6 +497,10 @@ help() { echo -e "\t-l set Python logging config file" echo -e "\t--version, -v show the OmegaClaw version" echo -e "\t--help, -h show this help" + echo + echo -e "Environment:" + echo -e "\tEMBEDDING_PROVIDER override the embedding backend (default depends on provider)" + echo -e "\tEMBEDDING_MODEL set the model asked of a non-Local EMBEDDING_PROVIDER" } options() { @@ -586,6 +590,10 @@ options() { } start() { + # Each provider above picks a default embedding backend. EMBEDDING_PROVIDER + # overrides it, so a remote provider can serve embeddings too. + embeddingprovider="${EMBEDDING_PROVIDER:-${embeddingprovider}}" + docker rm -f omegaclaw 2>/dev/null || true docker pull "${image}" 2>/dev/null || true @@ -656,6 +664,10 @@ start() { docker_cmd+=("openaiapi_url=${openaiapi_url}") fi + if [ -n "${EMBEDDING_MODEL:-}" ]; then + docker_cmd+=("embedding_model=${EMBEDDING_MODEL}") + fi + if [ -n "${openclaw_url:-}" ]; then docker_cmd+=("openclaw_url=${openclaw_url}") docker_cmd+=("openClawEnabled=enabled") diff --git a/src/loop.metta b/src/loop.metta index 2b7b7e95..f110c599 100644 --- a/src/loop.metta +++ b/src/loop.metta @@ -26,9 +26,9 @@ (= (initKnowledge) (progn (log INFO "loop" "Initializing knowledge base") - (if (== (embeddingprovider) OpenAI) - (log INFO "loop" (py-call (rag.init_knowledge "OpenAI"))) - (log INFO "loop" (py-call (rag.init_knowledge "Local")))))) + (if (== (embeddingprovider) Local) + (log INFO "loop" (py-call (rag.init_knowledge "Local"))) + (log INFO "loop" (py-call (rag.init_knowledge "OpenAI")))))) (= (getContext) (string-safe (py-str ("PROMPT: " (getPrompt (provider)) " SKILLS: " (getSkills) diff --git a/src/rag.py b/src/rag.py index f9274fd6..6861220e 100644 --- a/src/rag.py +++ b/src/rag.py @@ -139,14 +139,16 @@ def _chunk_markdown(text, filename): # --- Embedding ----------------------------------------------------------- def openai_embed_batch(texts): - """Embed a list of texts via OpenAI. Returns list of float vectors.""" + """Embed a list of texts via an OpenAI-compatible API. Returns list of float vectors.""" + model = config_get_by_key("embedding_model", EMBEDDING_MODEL) proxy_url = config_get_by_key("GATEWAY_URL") if proxy_url: - client = openai.OpenAI(base_url=f"{proxy_url.rstrip('/')}/openai/", api_key="unused") + prefix = str(config_get_by_key("embeddingprovider", "OpenAI")).lower() + client = openai.OpenAI(base_url=f"{proxy_url.rstrip('/')}/{prefix}/", api_key="unused") else: client = openai.OpenAI() try: - resp = client.embeddings.create(model=EMBEDDING_MODEL, input=texts) + resp = client.embeddings.create(model=model, input=texts) except Exception as e: raise RuntimeError(f"Embedding request failed: {e}") from e return [item.embedding for item in resp.data]