LLM provider adapters. src/engine/turn_engine.py's ReAct loop never talks to llama_cpp or an
HTTP client directly — it only ever calls provider.stream_completion(...) /
provider.complete(...) through the interface defined here, so swapping or adding a backend
never touches the turn engine.
- base.py -
LLMProvider, the abstract interface:load()/unload()lifecycle hooks andcomplete()/stream_completion(), both shaped like llama.cpp's owncreate_chat_completionresponse format (the lowest common denominator every adapter normalizes its own wire format to). - llama_cpp_provider.py -
LlamaCppProvider: local GGUF models viallama_cpp.Llama, including the MTMD vision handler for multimodal models. - openai_compatible_provider.py -
OpenAICompatibleProvider: any OpenAI-wire-format HTTP endpoint (OpenAI, Groq, Together, OpenRouter, a local vLLM/Ollama server, ...) viabase_url+api_key_env+model_namefrom the model card. - init.py -
PROVIDERS(provider name → adapter class) andbuild_provider(model_card), which src/engine/model_agent.py'sModelAgentcalls once per model load.
- Write
src/llm/<name>_provider.pyimplementingLLMProvider's four methods (load,unload,complete,stream_completion). - Register it in
__init__.py'sPROVIDERSdict under whatever string you want model cards to use forprovider. - Add a model card in src/models/configs/api.py (or a new
configs/<name>.py, imported from models/init.py) with thatprovidervalue and aprovider_configdict of whatever your adapter's__init__needs.
No changes to src/engine/, main.py, or the tool/prompt/memory systems are ever required —
they only see the model card's provider field and this interface.
Read from environment variables only, named by the model card's api_key_env — never written
to a file in the repo, never logged. That variable can be exported in your shell, or placed in
a .env file at the repo root (gitignored); OpenAICompatibleProvider.load()
(openai_compatible_provider.py) checks os.environ first, then
falls back to reading that one key out of .env if the shell doesn't have it — no separate
loader module, no dependency, and a real exported var always wins. See
src/models/configs/api.py for the env vars the registered API model
cards expect.
A failed API call (rate limit, bad key, network error, timeout) raises ProviderError
(defined in base.py) rather than a raw requests exception, so
src/cli/turn.py can catch it uniformly alongside local decode failures and
keep the session alive instead of crashing.