Models and Inference Providers¶
This document details the model abstraction layer, provider routing, dynamic agent inheritance, OpenRouter catalog caching, and local inference hardening (Ollama, vLLM, and OpenAI-compatible endpoints) in Zocrates.
1. Overview and Architectural Goals¶
Zocrates uses the Google Agent Development Kit (ADK) as its multi-agent orchestrator. Remote and local language models are unified through google.adk.models.lite_llm.LiteLlm, backed by litellm. This abstraction decouples agent definitions from specific providers and enables flexible hybridization—allowing researchers to run local and cloud models simultaneously across different pipeline stages.
graph TD
subgraph "Zotero Frontend (Firefox Gecko)"
UI[Preferences UI / Sidebar Chat]
end
subgraph "Zocrates Backend (FastAPI)"
EP[REST Endpoints /llm/* & /config]
FACTORY[LLM Factory zocrates/llm.py]
CACHE[Disk & Memory Model Cache]
EMBED[OpenRouterEmbedder zocrates/rag/embeddings.py]
AGENTS[ADK Agents: coordinator / search / synthesizer]
end
subgraph "External & Local Providers"
OR[OpenRouter API /v1/models & /v1/embeddings/models]
OLLAMA[Local Ollama Daemon :11434]
VLLM[Local vLLM / OpenAI Server :8000]
end
UI -- HTTP /config & /llm/openrouter_models --> EP
EP -- get_llm_model() --> FACTORY
EP -- fetch_and_cache_openrouter_models() --> CACHE
CACHE -- Async Network Fetch --> OR
FACTORY -- LiteLlm Instance --> AGENTS
EMBED -- litellm.embedding() --> OR
FACTORY -- Local Ollama API (ollama_chat/) --> OLLAMA
FACTORY -- Local OpenAI API --> VLLM
2. Provider Routing and Configuration Schema¶
2.1 Provider Routing Table¶
| Provider Category | Provider Identifier | Base URL Default | Key Required? | Example Model Identifier |
|---|---|---|---|---|
| OpenRouter (Remote LLM) | openrouter_llm |
https://openrouter.ai/api/v1 |
Yes (OPENROUTER_API_KEY) |
google/gemini-flash, qwen/qwen, deepseek/deepseek-flash |
| OpenRouter (Cloud Embeddings) | cloud |
https://openrouter.ai/api/v1 |
Yes (OPENROUTER_API_KEY) |
openrouter/nvidia/nemotron-3-embed-1b:free, openrouter/openai/text-embedding-3-small |
| OpenRouter (Decision Models) | openrouter (Verification) |
https://openrouter.ai/api/alpha/decisions |
Yes (OPENROUTER_API_KEY) |
~typesafe/jev-latest (Default), typesafe/jev-1.14, inception/mercury-decide |
| Laya (Local Decision Model) | local_laya |
http://127.0.0.1:8005/v1/systemone |
No | laya (native tensor batching via laya-serve) |
| Ollama (Local LLM) | ollama |
http://localhost:11434 |
No | llama3, mistral, qwen (routed internally via ollama_chat/) |
| vLLM (Local LLM) | vllm |
http://localhost:8000/v1 |
Optional ("dummy") |
Qwen/Qwen-Coder, meta-llama/Llama-3 |
| Custom OpenAI API | openai_compatible |
User-configured | Optional | custom-model-id |
2.2 Secure Credential and Configuration Persistence¶
- Plain-Text Key Stripping: Sensitive API keys (
OPENROUTER_API_KEY) are stripped before persisting settings to disk. Keys are stored in Zotero's encrypted Login Manager (Services.logins/Services.passwords) and passed in-memory to the backend runtime via secure local endpoints. - Configuration Path:
<zotero-data-dir>/zocrates-data/zocrates_config.json - Configuration Schema:
{
"llm_provider": "openrouter_llm",
"llm_model": "google/gemini-flash",
"llm_base_url": "",
"llm_temperature": null,
"llm_max_tokens": 16384,
"ollama_num_ctx": 16384,
"llm_extra_options": {
"num_ctx": 16384
},
"embedding_provider": "local",
"embedding_model": "jinaai/jina-embeddings-v5-text-nano",
"override_agent_models": {
"coordinator": {
"model": null,
"thinking_level": "medium"
},
"search": {
"model": null,
"thinking_level": "low"
},
"synthesizer": {
"model": null,
"thinking_level": "high"
},
"online_search": {
"model": null,
"thinking_level": "low"
},
"online_synthesizer": {
"model": null,
"thinking_level": "high"
}
}
}
- Bare Model Slugs & Provider Fallback:
- Model names in configuration and UI are stored as bare, provider-agnostic slugs (e.g.,
"google/gemini-flash","llama3"). - Provider runtime prefixes (
openrouter/,ollama_chat/,openai/) are deterministically attached by_resolve_model_name(). - For Ollama, bare slugs or slugs prefixed with
ollama/are normalized internally toollama_chat/<slug>to ensure native execution over Ollama's/api/chatendpoint. - If a user switches from OpenRouter to Ollama without selecting a local model, cross-provider mismatch detection catches the slash-containing slug and automatically falls back to
PROVIDER_DEFAULT_MODELS["ollama"](llama3.1:8b).
2.3 Single Source of Truth ("No Secret Config Options")¶
Zocrates enforces that zocrates_config.json is the transparent, canonical source of truth for all system configurations:
- No Hidden Parameters: Every configurable parameter, role template, and override block is explicitly materialized in
zocrates_config.jsonon disk with its defaults. - Automatic Disk Backfill: When
load_zocrates_config()reads an existingzocrates_config.jsonthat lacks new keys or subagent override blocks, it backfills them into the file under atomicFileLockand updates_last_config_mtime. - First-Time Service Seeding: On initial registration via
service_manager.py, the config written to disk contains all default structures. - Frontend Persistence Guarantee: In
preferences.js,saveConfigJson()defensively preserves all existing keys and initializes missing templates, preventing accidental deletion of power-user settings.
3. Dynamic Multi-Agent Model Inheritance and Overrides¶
graph TD
GLOBAL[Global Config: llm_provider & llm_model] --> CHECK{Subagent override: model is null?}
CHECK -- Yes (Default) --> INHERIT[Dynamically Inherit Global Provider & Model]
CHECK -- No (Explicit Pin) --> PINNED[Lock to Specific Model & Provider]
INHERIT --> C[coordinator_agent: thinking medium]
INHERIT --> S[library_search_agent: thinking low]
INHERIT --> SYN[library_synthesizer_agent: thinking high]
INHERIT --> OS[online_search_agent: thinking low]
INHERIT --> OSYN[online_synthesizer_agent: thinking high]
PINNED --> CUSTOM[Custom Role Execution]
3.1 Dynamic Model Inheritance Across All Roles¶
All five subagents in override_agent_models (coordinator, search, synthesizer, online_search, online_synthesizer) default to model: null. They dynamically inherit the active global model. When a user switches from OpenRouter to a local model (Ollama or vLLM), all subagents automatically follow the local model without any risk of 401 Unauthorized crashes on cloud endpoints.
3.2 Subagent Model Pinning¶
If a researcher specifies a model slug for a specific subagent (e.g. {"synthesizer": {"model": "openrouter/anthropic/claude-sonnet"}}), that subagent remains locked to its pinned model while other agents follow the global setting. This enables advanced hybrid configurations:
- Offline Search + Cloud Synthesis: Run the ReAct search agent locally via Ollama to query local vector and keyword indices privately without network traffic, while routing final synthesis through frontier cloud reasoning models.
- Cost-Optimized Retrieval: Pin lightweight, low-cost models to high-frequency retrieval loops while reserving reasoning models for synthesis.
3.3 Role-Tailored Thinking Effort (ROLE_DEFAULT_THINKING)¶
set_agent_models() automatically attaches types.GenerateContentConfig for each role based on ROLE_DEFAULT_THINKING:
coordinator_agent:"medium"(balanced conversational dialogue)search_subagent:"low"(fast tool execution without wasting reasoning tokens on parent dialogue)synthesizer_agent:"high"(deep literature synthesis and epistemic gap evaluation)online_search_agent:"low"(autonomous web search across scholarly APIs)online_synthesizer_agent:"high"(structured novel paper recommendation and import staging)
Thinking can be explicitly tuned per agent or disabled via "none", "off", "false", or "disabled".
3.4 In-Memory Hot-Swapping and Pre-Flight Freshness Check¶
- Zero-Restart Updates: When updating settings via
POST /config,set_agent_models()hot-swaps active agent models in memory without process restarts. - Silent Pre-Flight Check: At the start of
POST /chat,check_config_freshness()runs an OSstat()check (~3µs) againstzocrates_config.json. If a user manually edited the config file on disk, the backend silently reloads the configuration and updates the agent tree before processing the turn.
4. OpenRouter Catalog Fetching and 24-Hour Disk Caching¶
4.1 Endpoints and Cache Storage¶
- Catalog Endpoints:
- Chat/Completion LLMs:
GET https://openrouter.ai/api/v1/models - Cloud Embeddings:
GET https://openrouter.ai/api/v1/embeddings/models - Disk Cache Files:
<zotero-data-dir>/zocrates-data/openrouter_models_cache/llm_models.json<zotero-data-dir>/zocrates-data/openrouter_models_cache/embedding_models.json- Expiration (TTL): 24 hours (86,400 seconds) (
CACHE_TTL_SECONDS = 86400).
4.2 Three-Tier Cache Resolution Pipeline¶
graph TD
A[Preferences UI requests /llm/openrouter_models] --> B{In-Memory Cache Valid?}
B -- Yes --> RET[Return Cached Model List]
B -- No --> C{Disk Cache Valid & < 24h old?}
C -- Yes --> POP[Read Disk Cache & Populate Memory Cache] --> RET
C -- No or force_refresh=True --> D{API Key Configured?}
D -- Yes --> FETCH[Async Network Fetch from OpenRouter]
FETCH --> WRITE[Update In-Memory Cache & Write Disk Cache] --> RET
D -- No --> FALLBACK[Return Empty or Stale Cache]
- In-Memory Cache: Checked first (
_OPENROUTER_LLM_CACHE/_OPENROUTER_EMBED_CACHE) if unexpired andforce_refresh=False. - Disk Cache: Read from
<zotero-data-dir>/zocrates-data/openrouter_models_cache/if unexpired andforce_refresh=False. Populates memory cache instantly without requiring API keys or network latency. - Network Fetch: Invoked via
asyncio.to_thread(_sync_fetch_openrouter_models, ...)when cache is expired orforce_refresh=True(requiresOPENROUTER_API_KEY).
4.3 Catalog Filtering Logic¶
- Free Tier Detection: Parses pricing fields:
float(pricing.get("prompt", -1)) == 0.0andfloat(pricing.get("completion", -1)) == 0.0. - Vision / VLM Model Detection: When filtering vision-capable models (
modality="image"), verifies that both"text"and"image"are present ininput_modalitiesand"text"is inoutput_modalities.
5. OpenRouter Cloud Embeddings¶
OpenRouterEmbedder: Wraps OpenRouter cloud embedding models vialitellm.embedding.- Dynamic Dimension Auto-Probing: OpenRouter catalog metadata does not specify vector dimensionality.
OpenRouterEmbedderdetectsdimension = len(embeddings[0])on the first call. If.dimensionis accessed before model execution, it auto-probes the model with a single text token ("test") to detect exact dimensionality. - Sanitized Collection Naming: ChromaDB vector store collections are dynamically named with the model slug and probed dimension (e.g.
zocrates_openrouter_nvidia_nemotron_3_embed_1b_free_1024d), preventing dimension mismatch errors when users switch embedding models.
6. Local LLM Hardening Architecture¶
To ensure foolproof reliability for local LLM users (Ollama, vLLM, LM Studio, and llama-server), Zocrates implements a multi-layer hardening pipeline:
graph TD
A[User Enters Model Slug e.g. lfm2.5-thinking:1.2b] --> B[Model Name Normalization _resolve_model_name]
B -->|Ollama| C[ollama_chat/ Wire Prefix]
B -->|vLLM / Local OpenAI| D[openai/ Wire Prefix]
C --> E[Base URL Formatting _format_base_url]
D --> E
E --> F[Top-Level Context Bounding num_ctx=16384]
F --> G[Context Limit Clamping max_tokens=4096 on vLLM]
G --> H[Drop Unsupported Params drop_params=True]
H --> I[Thinking Capability Probe is_ollama_thinking_supported]
I --> J[Protected Output Hygiene _strip_thinking_tags]
J --> K[Auxiliary Systems Fail-Open Reranker / Predictor / Summarizer]
6.1 Wire Protocol Hardening (ollama_chat/)¶
In LiteLLM, models prefixed with ollama/ route to Ollama's legacy /api/generate endpoint, which lacks native tool calling and emits raw JSON token strings for thoughts. Zocrates normalizes bare slugs and ollama/<slug> inputs to ollama_chat/<slug>, forcing LiteLLM to use Ollama's /api/chat endpoint with full tool-calling and native thought streaming support.
6.2 Base URL Normalization¶
- Normalizes schemes to prevent missing protocol crashes (e.g.
localhost:11434→http://localhost:11434). - Strips trailing
/v1from Ollama URLs (http://localhost:11434/v1→http://localhost:11434) because LiteLLM's Ollama provider attaches its own path. - Appends
/v1to vLLM URLs (http://localhost:8000→http://localhost:8000/v1) because vLLM implements OpenAI-compatible routing.
6.3 Context Window Protection & Top-Level num_ctx¶
- Google ADK clobbers the
extra_bodydictionary during turn execution. To ensure context sizes are respected without VRAM thrashing, Zocrates passesnum_ctx = cfg.get("ollama_num_ctx", 16384)as a top-level kwarg toLiteLlm. - vLLM default
max_tokensis clamped from 16,384 to 4,096 to prevent HTTP 400 context ceiling errors on standard 8k/16k context window models.
6.4 Thinking and Reasoning Adaptation¶
- Ollama rejects
think: truewith HTTP 400 if the underlying model is not a thinking model. is_ollama_thinking_supported()dynamically probes Ollama's/api/showendpoint to inspect model templates and capabilities (caching results in_OLLAMA_THINKING_SUPPORT_CACHE), safely suppressing thinking parameters for standard non-reasoning models.- For OpenAI-compatible and vLLM servers,
"minimal"reasoning effort is mapped to"low"to comply with strict schema validators.
6.5 Protected Output Hygiene (_strip_thinking_tags)¶
- Strips
<think>...</think>scratchpads while strictly protecting code blocks (``) and inline backticks () using unique UUID masking. - Stripping of unclosed
<think>tags is anchored to the beginning of the text (^\s*<think>[\s\S]*$), preventing truncation of responses from cloud models that discuss<think>tags in normal prose.
6.6 Future-Proof Multimodal Detection¶
- Replaces fragile hardcoded model lists with boundary-agnostic regexes (
(?:[-_./]|\b|[0-9a-z])vl\d*(?:[-_.:]|$|\b)) and generalized architecture families (Gemma, Qwen VL, DeepSeek, Phi, Mistral). - In Ollama, queries
/api/tagsand/api/showto inspect native model capabilities.
6.7 Auxiliary System Fail-Open Resilience¶
- Cross-Encoder Reranker (
reranker.py): Fails open to raw cross-encoder / vector search rank order when no cloud API key is present. - Predictive Ghost Input (
predictor.py): ReturnsNonegracefully without throwing exceptions when running offline. - Document Summaries (
summary_generator.py): Falls back to deterministic 3-step extractive summarization without network dependencies. - Cost Calculator (
cost_calculator.py): Evaluates local models to$0.000000with zero token costs.
6.8 Context-Aware Preferences UX¶
- When a local provider (Ollama, vLLM, Custom OpenAI) is selected, OpenRouter vendor filter chips, model search bar, count badges, and metadata cards are cleanly hidden.
- The model text input displays a clean placeholder:
"Enter the model name". - Switching back to OpenRouter instantly restores all catalog search controls and metadata cards.
7. REST API Endpoint Specifications¶
7.1 GET /config¶
Returns active LLM and embedding configuration details.
- Response:
{ "status": "success", "llm_provider": "openrouter_llm", "llm_model": "google/gemini-flash", "llm_base_url": "", "llm_temperature": null, "llm_max_tokens": 16384, "llm_extra_options": {"num_ctx": 16384}, "embedding_provider": "local", "embedding_model": "jinaai/jina-embeddings-v5-text-nano", "has_openrouter_key": true }
7.2 POST /config¶
Updates LLM and embedding parameters, persists secure keys in memory, and hot-swaps active agent models.
- Request Body:
- Response: Updated configuration dictionary.
7.3 GET /llm/openrouter_models¶
Retrieves filtered OpenRouter models for dropdown population in Zotero Preferences.
- Query Parameters:
target_type:"llm"(default) or"embedding".force_refresh:trueorfalse.free_only:trueorfalse.min_context: Integer (e.g.16384).- Response:
7.4 POST /llm/test¶
Performs a lightweight completion test against the configured LLM endpoint (OpenRouter or local Ollama/vLLM) to verify connectivity.
- Request Body (Optional):
- Response:
7.5 POST /llm/test_embedding¶
Performs a test embedding call against the specified embedding model and provider (Local Jina v5 or Cloud OpenRouter Embeddings) to verify connectivity and dimensionality.
- Request Body (Optional):
- Response: