How the Provider Abstraction Works in 99: BaseProvider Deep Dive
The provider abstraction in 99 uses a BaseProvider class with abstract methods _build_command and _get_provider_name to unify OpenCode, Claude, Cursor-Agent, and Kiro under a single interface, enabling seamless AI backend switching without changing request logic.
The provider abstraction is the architectural backbone of ThePrimeagen's 99, a Neovim plugin that integrates multiple AI coding assistants. By defining a strict contract in lua/99/providers.lua, the codebase decouples AI backend specifics from the request lifecycle, allowing users to switch between OpenCode, Claude, and other providers without modifying core logic.
Core Contract: The BaseProvider Interface
The file lua/99/providers.lua establishes the contract that every concrete provider must satisfy. The BaseProvider class requires implementations for two private helpers: self:_build_command(query, request) to construct the command-line invocation, and self:_get_provider_name() to return a human-readable identifier for logging.
--- @class _99.Providers.BaseProvider
--- @field _build_command fun(self: _99.Providers.BaseProvider, query: string, request: _99.Request): string[]
--- @field _get_provider_name fun(self: _99.Providers.BaseProvider): string
local BaseProvider = {}
Concrete providers inherit from this base by setting their metatable's __index to BaseProvider, as seen in lines 36-38:
local OpenCodeProvider = setmetatable({}, { __index = BaseProvider })
Request Lifecycle: How the Provider Abstraction Executes Commands
The BaseProvider:make_request method orchestrates the entire request lifecycle across all AI backends. According to the source in lua/99/providers.lua, the implementation follows a strict sequence:
- Initialization (lines 53-58): Calls
observer.on_start()and logs the temporary file location where the prompt is stored. - Command Building (lines 68-70): Invokes
_build_commandto translate the user query into a system command array specific to the external tool. - Process Spawning (lines 71-102): Executes
vim.system(command, …)with callbacks forstdout,stderr, and completion. These callbacks forward output to the supplied observer and respect request cancellation signals. - Completion Handling (lines 108-129): On process exit, non-zero codes trigger a "failed" status. Success invokes
_retrieve_responseto read the temporary file and delivers the result to the observer. - Process Storage (lines 131-133): The
vim.SystemObjis stored on the request object, enabling later cancellation viarequest:cancel().
This unified flow means concrete providers never handle process management directly—they only specify how to build the command.
Concrete Implementations of the Provider Abstraction
Each concrete provider implements only the two abstract methods defined by the base class. For example, OpenCodeProvider constructs its command array as follows:
function OpenCodeProvider._build_command(_, query, request)
return { "opencode", "run", "--agent", "build", "-m",
request.context.model, query }
end
function OpenCodeProvider._get_provider_name()
return "OpenCodeProvider"
end
The same pattern repeats for ClaudeCodeProvider, CursorAgentProvider, and KiroProvider. Because they inherit make_request through the metatable mechanism, each provider requires only 10-15 lines of code to integrate a completely different AI backend.
Integration with the Request System
The lua/99/request/init.lua file bridges user commands to the provider abstraction. When creating a request, the system selects a provider using either the default (OpenCodeProvider) or a user-specified override:
local provider = context._99.provider_override or Providers.OpenCodeProvider
When Request:start executes, it builds the full prompt, writes it to a temporary file, and delegates to the provider's make_request method:
self.provider:make_request(
prompt,
self,
observer_from_request(self, observer)
)
The observer object (_99.Providers.Observer) relays lifecycle events back to the UI layer, updating throbbers, streaming stdout/stderr, and handling completion states without knowing which AI tool generated the response.
Extending the Provider Abstraction
Adding support for a new AI backend requires only three steps, demonstrating the abstraction's extensibility:
- Create a new module that inherits from
BaseProviderusingsetmetatable. - Implement
_build_commandto return the appropriate CLI arguments. - Implement
_get_provider_namefor logging identification.
-- my_provider.lua
local BaseProvider = require("99.providers").BaseProvider
local MyProvider = setmetatable({}, { __index = BaseProvider })
function MyProvider._build_command(_, query, request)
return { "my-ai-cli", "--model", request.context.model, query }
end
function MyProvider._get_provider_name()
return "MyProvider"
end
return MyProvider
Register the new provider in lua/99/providers.lua's return table, then activate it by setting context._99.provider_override = Providers.MyProvider. No other files require modification.
Summary
lua/99/providers.luadefinesBaseProviderwith abstract methods_build_commandand_get_provider_namethat concrete providers must implement.- The
make_requestmethod handles the complete lifecycle usingvim.system, including process spawning, cancellation support, and response retrieval. - Providers inherit behavior via
setmetatable({}, { __index = BaseProvider }), requiring only command-building logic. lua/99/request/init.luaselects providers based oncontext._99.provider_overrideor defaults toOpenCodeProvider.- The observer pattern decouples UI updates from backend execution, ensuring the interface remains consistent across all AI tools.
Frequently Asked Questions
What is the provider abstraction in 99?
The provider abstraction is an interface defined by BaseProvider in lua/99/providers.lua that standardizes how 99 communicates with external AI tools. It requires implementations to define how to build CLI commands and identify themselves, while the base class handles process execution, streaming output, and cancellation uniformly across all backends.
How do I switch AI providers in 99?
Set context._99.provider_override to your desired provider before creating the request. For example, assign Providers.ClaudeCodeProvider to use Claude instead of the default OpenCode. The request system in lua/99/request/init.lua checks this override field when initializing the provider instance.
Can I add custom AI backends to 99?
Yes. Create a Lua module that calls setmetatable({}, { __index = BaseProvider }), then implement _build_command to return your CLI command array and _get_provider_name to return a string identifier. Register the module in providers.lua, and it becomes available for use immediately without modifying request logic or UI code.
How does 99 handle request cancellation?
When make_request spawns a process via vim.system, it stores the returned vim.SystemObj on the request object at lines 131-133. Calling request:cancel() later accesses this stored object to terminate the running process, ensuring the observer receives appropriate cleanup signals even when switching contexts mid-request.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →