Problem
Neo's REM cycle alternates between chat model (SemanticGraphExtractor.executeTriVectorExtraction, TopologyInferenceEngine.extractTopology, GoldenPathSynthesizer.synthesizeGoldenPath) and embedding model (memory ingestion, per-session new-memory vectorization). Per session: chat → embedding → chat. 2 switches × ~10 sessions per REM cycle = ~20 switches.
If the underlying provider holds only ONE model resident:
- Every switch evicts the resident model + reloads the other from disk
- ~18s cold-prefill cost per switch (per neo PR #12076 benchmark anchor on local LM Studio gemma-4-31b-it)
- ~6 minutes pure model-swap overhead per REM cycle regardless of actual inference time
- Defeats
keep_alive=-1 (neomjs/neo#12089) entirely — the OTHER model's load triggers eviction of the keep_alive-pinned model
Operator framing 2026-05-27 ~09:00Z:
"our own default for our used providers => we MUST always use TWO model support (embedding and chat model coexistence)"
Why this is Neo-level, not consumer-level
Operator-side compose-yml fixes (e.g., OLLAMA_MAX_LOADED_MODELS=2) are workarounds for a Neo-level mandate. The substrate-correct shape: Neo's providers DECLARE the multi-model requirement + BOOT-TIME PROBE the server-side capacity + WARN if insufficient. Operator-side env vars satisfy the requirement; Neo enforces it has been satisfied.
Fix shape (both provider families — symmetry mandate)
Ollama provider
Neo can't directly control OLLAMA_MAX_LOADED_MODELS (server-process startup config, not API-mutable), but can:
- Declare expected capacity via new config field
ollama.requireParallelModels (default 2 for chat+embedding; can grow if operator deploys additional models)
- Probe at boot via
GET <host>/api/ps to verify the running Ollama instance can hold N models OR currently has both pre-warmed
- WARN at boot when capacity check fails — operator-actionable log line with the specific env var to set
OpenAiCompatible provider
Same shape — LM Studio, llama.cpp, vLLM each have different mechanisms:
- LM Studio (newer versions): "JIT load" or "auto-eviction" UI settings; OpenAI-compat endpoint addresses each via
model parameter
- llama.cpp: typically one-model-per-server; multi-model requires separate server instances
- vLLM: multi-model API with per-model state
Neo's provider can't universally enforce, but can:
- Declare expected capacity via
openAiCompatible.requireParallelModels (default 2)
- Probe at boot via
GET <host>/v1/models + cross-reference against expected chat + embedding model names
- WARN at boot when probe shows only one model accessible OR when behavior probe (small chat call followed by small embedding call within keep_alive window) shows cold-reload cost on the second call
Contract Ledger
| Surface |
Source of Authority |
Proposed Behavior |
Fallback / Edge Case |
NEW ollama.requireParallelModels config field |
This ticket |
Default 2 (chat + embedding); env-overridable via NEO_OLLAMA_REQUIRE_PARALLEL_MODELS; consumed by boot-time probe in Ollama provider |
Operator can override to 1 if deploying chat-only (e.g., disable Sandman entirely) |
NEW openAiCompatible.requireParallelModels config field |
This ticket — symmetry |
Same shape; env-overridable via NEO_OPENAI_COMPATIBLE_REQUIRE_PARALLEL_MODELS |
Same |
| Ollama boot-time capacity probe |
New helper in ai/services/graph/providerReadiness.mjs (or sibling module if Sub 6 Part A #12072 lands first via PR #12084) |
At provider init: GET <host>/api/ps to see currently loaded models; verify count + named models match aiConfig.ollama.model + aiConfig.ollama.embeddingModel; WARN if either is absent |
Probe failure (e.g., ollama not responding) does NOT block boot — separate concern handled by existing provider-readiness substrate |
| OpenAiCompatible boot-time capacity probe |
Same module |
At provider init: GET <host>/v1/models + assert both model + embeddingModel are listed; behavior probe optional (small chat + small embedding within keep_alive window; flag if cold-reload cost on call 2) |
Same |
| Operator-cookbook documentation |
NEW section in learn/agentos/ |
Why multi-model coexistence matters + per-provider env var reference + how to verify (docker compose exec local-model ollama ps for docker-compose-style deployments) |
Cross-references neomjs/neo#12089 keep_alive substrate evolution |
Acceptance Criteria
Avoided Traps
- ❌ Don't attempt to mutate the underlying server's loaded-models cap via API — Ollama's
OLLAMA_MAX_LOADED_MODELS is a process-startup setting, not API-mutable. Neo declares the requirement; operator satisfies it via server-side config.
- ❌ Don't fail boot on capacity probe failure — boot-blocking would regress existing deployments. WARN + observable signal is sufficient; operators act on the WARN.
- ❌ Don't bundle with #12089 — that ticket is keep_alive default substrate; this ticket is multi-model coexistence substrate. Two distinct mandates. Operator-explicit separation.
- ❌ Don't enforce the capacity probe at every callsite — boot-time check is sufficient signal; per-call check would add latency without operational value.
Related
- Epic #12065 (Orchestrator-as-SSOT REM pipeline) — multi-model coexistence prevents the ~6min/cycle model-swap overhead class
- neomjs/neo#12089 — keep_alive=-1 default switch (companion mandate; without multi-model coexistence, keep_alive is defeated by OTHER model's load)
- neomjs/neo#12072 / PR #12084 — Sub 6 Part A provider-readiness substrate (natural home for the capacity probe helper)
- Operator-side docker-mcp env var fix consumed by this Neo-side mandate (parallel deployment-side work)
- PR #12076 benchmark — 18s cold-prefill empirical anchor
Operator anchor
"our own default for our used providers => we MUST always use TWO model support (embedding and chat model coexistence)" — @tobiu 2026-05-27 ~09:00Z
Problem
Neo's REM cycle alternates between chat model (
SemanticGraphExtractor.executeTriVectorExtraction,TopologyInferenceEngine.extractTopology,GoldenPathSynthesizer.synthesizeGoldenPath) and embedding model (memory ingestion, per-session new-memory vectorization). Per session: chat → embedding → chat. 2 switches × ~10 sessions per REM cycle = ~20 switches.If the underlying provider holds only ONE model resident:
keep_alive=-1(neomjs/neo#12089) entirely — the OTHER model's load triggers eviction of the keep_alive-pinned modelOperator framing 2026-05-27 ~09:00Z:
Why this is Neo-level, not consumer-level
Operator-side compose-yml fixes (e.g.,
OLLAMA_MAX_LOADED_MODELS=2) are workarounds for a Neo-level mandate. The substrate-correct shape: Neo's providers DECLARE the multi-model requirement + BOOT-TIME PROBE the server-side capacity + WARN if insufficient. Operator-side env vars satisfy the requirement; Neo enforces it has been satisfied.Fix shape (both provider families — symmetry mandate)
Ollama provider
Neo can't directly control
OLLAMA_MAX_LOADED_MODELS(server-process startup config, not API-mutable), but can:ollama.requireParallelModels(default2for chat+embedding; can grow if operator deploys additional models)GET <host>/api/psto verify the running Ollama instance can hold N models OR currently has both pre-warmedOpenAiCompatible provider
Same shape — LM Studio, llama.cpp, vLLM each have different mechanisms:
modelparameterNeo's provider can't universally enforce, but can:
openAiCompatible.requireParallelModels(default2)GET <host>/v1/models+ cross-reference against expected chat + embedding model namesContract Ledger
ollama.requireParallelModelsconfig field2(chat + embedding); env-overridable viaNEO_OLLAMA_REQUIRE_PARALLEL_MODELS; consumed by boot-time probe in Ollama provider1if deploying chat-only (e.g., disable Sandman entirely)openAiCompatible.requireParallelModelsconfig fieldNEO_OPENAI_COMPATIBLE_REQUIRE_PARALLEL_MODELSai/services/graph/providerReadiness.mjs(or sibling module if Sub 6 Part A #12072 lands first via PR #12084)GET <host>/api/psto see currently loaded models; verify count + named models matchaiConfig.ollama.model+aiConfig.ollama.embeddingModel; WARN if either is absentGET <host>/v1/models+ assert bothmodel+embeddingModelare listed; behavior probe optional (small chat + small embedding within keep_alive window; flag if cold-reload cost on call 2)learn/agentos/docker compose exec local-model ollama psfor docker-compose-style deployments)Acceptance Criteria
ollama.requireParallelModels(default2) inai/config.template.mjs; env-overridable viaNEO_OLLAMA_REQUIRE_PARALLEL_MODELSopenAiCompatible.requireParallelModels(default2) — symmetric with OllamaGET /api/ps; WARN log line[provider/ollama] expected N+ models loaded (chat=X, embedding=Y); observed N loaded — model swap penalty likely; set OLLAMA_MAX_LOADED_MODELS=N+ in ollama server envGET /v1/models; analogous WARN log linelearn/agentos/operator-cookbook describing the multi-model mandate + per-provider env var matrix + verification commandsAvoided Traps
OLLAMA_MAX_LOADED_MODELSis a process-startup setting, not API-mutable. Neo declares the requirement; operator satisfies it via server-side config.Related
Operator anchor