
TL;DR — Two surfaces of AgentZero Lite expanded at once. The LLM layer went from a single local Gemma-4 to five backends (local Gemma, local Nemotron Nano 8B, Webnori, OpenAI, LM Studio, Ollama) behind one OpenAI-compatible interface. The modality layer grew a voice — a STT/TTS subsystem with four STT engines, two TTS engines, dual-VAD mic capture, and a live pipeline that reuses the same LLM gateway. Neither expansion needed a new architecture; both fell out of the same design principle that already defined the agent: one abstract interface, many implementations.
This is a follow-up to the foundation report — An LLM Is Not an Agent: Five Layers. That doc argued the agent is what surrounds the model. This doc shows what happens when you keep that frame and ask: what else can we slot behind the same interfaces?
flowchart LR
subgraph Before
A1[Local Gemma 4<br/>+ Nemotron]:::dim
T1[Text only]:::dim
end
subgraph After
A2[Local Gemma 4<br/>Local Nemotron<br/>Webnori<br/>OpenAI<br/>LM Studio<br/>Ollama]
T2[Text + Voice<br/>STT × 4 / TTS × 2]
end
A1 -.expanded.-> A2
T1 -.expanded.-> T2
classDef dim fill:#f5f5f5,stroke:#999,color:#666
Same shell. Same actor topology. Same harness. The expansions land inside the boxes the field report drew.
The original story was self-built llama.cpp + LLamaSharp 0.26 running Gemma-4 / Nemotron on-device. That solved one question: can the loop work entirely offline? Yes. But it left the user stuck if they wanted to try a hosted model, or already had Ollama / LM Studio running on the same machine.
The expansion adds four external providers and unifies them behind one HTTP client:
| Provider | Mode | Endpoint | Reason to include |
|---|---|---|---|
| Webnori | Hosted (free, public) | OpenAI-compatible | Zero-friction first try |
| OpenAI | Hosted (paid) | api.openai.com/v1 |
Frontier comparison |
| LM Studio | Local | localhost:1234/v1 |
Already-running local server |
| Ollama | Local | localhost:11434/v1 |
Most common local server |
All four speak the OpenAI Chat Completions wire format, so a single OpenAiCompatibleProvider covers them. Only the base URL and credentials differ.
public interface ILlmProvider
{
string ProviderName { get; }
Task<List<LlmModelInfo>> ListModelsAsync(CancellationToken ct = default);
Task<LlmResponse> CompleteAsync(LlmRequest request, CancellationToken ct = default);
IAsyncEnumerable<LlmStreamChunk> StreamAsync(LlmRequest request, CancellationToken ct = default);
}
Source: Project/ZeroCommon/Llm/Providers/ILlmProvider.cs
The selection lives where the user expects it — a Local / External radio on the LLM settings tab. Picking External reveals provider + model + API-key fields; the chosen provider is persisted in LlmSettingsStore. A factory (LlmProviderFactory) returns the right ILlmProvider at runtime.
flowchart TD
S[LLM tab<br/>Local · External] --> F[LlmProviderFactory]
F -->|Local| LH[LLamaSharp host<br/>Gemma 4 / Nemotron / …]
F -->|External| OAC[OpenAiCompatibleProvider]
OAC -->|baseUrl + apiKey| W[Webnori]
OAC -->|baseUrl + apiKey| O[OpenAI]
OAC -->|baseUrl| LMS[LM Studio]
OAC -->|baseUrl| OLL[Ollama]
The foundation report described AIMODE's dual backend — Gemma 4 via GBNF, Nemotron via the Llama-3.1 chat template, both behind IAgentToolHost. The provider expansion follows the same principle, one layer up:
| Layer | Interface | Implementations |
|---|---|---|
| Tool host | IAgentToolHost |
WorkspaceTerminalToolHost |
| Tool-call backend (AIMODE) | AgentToolLoop / ExternalAgentToolLoop |
Gemma-GBNF, Nemotron-template |
| LLM provider (new) | ILlmProvider |
OpenAiCompatibleProvider × 4 hosts, local LLamaSharp host |
Same shape, different layer. Adding a fifth provider tomorrow is a config entry, not a refactor.