# How the Provider Abstraction Works in 99: BaseProvider Deep Dive

> Explore the 99 BaseProvider abstraction for unified AI backend switching. Learn how abstract methods streamline requests for OpenCode, Claude, Cursor-Agent, and Kiro seamlessly.

- Repository: [ThePrimeagen/99](https://github.com/theprimeagen/99)
- Tags: deep-dive
- Published: 2026-02-16

---

**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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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.

```lua
--- @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:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/lua/99/providers.lua), the implementation follows a strict sequence:

1. **Initialization** (lines 53-58): Calls `observer.on_start()` and logs the temporary file location where the prompt is stored.
2. **Command Building** (lines 68-70): Invokes `_build_command` to translate the user query into a system command array specific to the external tool.
3. **Process Spawning** (lines 71-102): Executes `vim.system(command, …)` with callbacks for `stdout`, `stderr`, and completion. These callbacks forward output to the supplied observer and respect request cancellation signals.
4. **Completion Handling** (lines 108-129): On process exit, non-zero codes trigger a "failed" status. Success invokes `_retrieve_response` to read the temporary file and delivers the result to the observer.
5. **Process Storage** (lines 131-133): The `vim.SystemObj` is stored on the request object, enabling later cancellation via `request: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:

```lua
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`](https://github.com/ThePrimeagen/99/blob/main/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:

```lua
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:

```lua
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:

1. Create a new module that inherits from `BaseProvider` using `setmetatable`.
2. Implement `_build_command` to return the appropriate CLI arguments.
3. Implement `_get_provider_name` for logging identification.

```lua
-- 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`](https://github.com/ThePrimeagen/99/blob/main/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.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/providers.lua)** defines `BaseProvider` with abstract methods `_build_command` and `_get_provider_name` that concrete providers must implement.
- The `make_request` method handles the complete lifecycle using `vim.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.lua`](https://github.com/ThePrimeagen/99/blob/main/lua/99/request/init.lua)** selects providers based on `context._99.provider_override` or defaults to `OpenCodeProvider`.
- 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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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`](https://github.com/ThePrimeagen/99/blob/main/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.