Providers
Every provider amber ships is a plugin: there is no core provider list to add to. With no plugins registered, /provider listis empty. A provider plugin owns its presets, its protocol, and its provider-specific features.
ProviderCapability
A provider contributes presets plus, optionally, thedialect (wire protocol) they speak:
struct ProviderCapability::Preset {
std::string name; // provider name, e.g. "gemini"
std::string api_base; // endpoint root
std::string default_model;
bool requires_key = true;
};// Own protocol (Gemini): provide the dialect factory.
caps.push_back(std::make_unique<agent::ProviderCapability>(
"gemini",
[] { return make_gemini_dialect(); },
std::vector<agent::ProviderCapability::Preset>{preset}));
// Shared protocol (DeepSeek speaks OpenAI wire): pass NO factory.
caps.push_back(std::make_unique<agent::ProviderCapability>(
"openai",
std::function<std::unique_ptr<agent::Dialect>()>{}, // empty
std::vector<agent::ProviderCapability::Preset>{preset}));The first argument is the flavor, the wire protocol identifier.register_dialect puts the factory into the same table the built-ins use; the presets become provider rows through the ordinary repository merge, so they appear in /provider list and the command drawer with no new concept.
Shared protocol vs own protocol
- Shared (an OpenAI-compatible gateway): pass no dialect factory. You contribute presets and leave the shared
openaiprotocol alone, so switching your plugin off cannot take the shared protocol down with it. - Own (Gemini, Anthropic): provide a factory. Your dialect is registered under your plugin id, so disabling the plugin removes exactly that flavor.
The Dialect port
Everything wire-specific lives here, endpoints, auth headers, request body, buffered parse, stream decoding, model listing, usage mapping, retry classification, overflow hints. A provider plugin must notopen its own HTTP client: lib/http_transport.cpp is the single curl mechanism for every provider.
class Dialect {
public:
virtual std::string flavor() const = 0;
virtual std::string chat_url(const Config& cfg) const = 0;
virtual std::string models_url(const Config& cfg) const = 0; // "" = none
virtual std::vector<std::string> auth_headers(const Config& cfg) const = 0;
virtual json build_chat_body(const Config& cfg,
const std::vector<Message>& messages,
const std::vector<std::shared_ptr<Tool>>& tools,
bool stream) const = 0;
virtual Message parse_completion(const std::string& raw) const = 0;
virtual std::unique_ptr<StreamDecoder> make_decoder(
Message& out, StreamDecoder::ChunkSink on_chunk, std::string debug_path) const = 0;
virtual ServerInfo parse_models_response(const std::string& body,
const std::string& preferred_model) const = 0;
virtual std::vector<ModelInfo> parse_model_list_response(const std::string& body) const = 0;
virtual TokenUsage parse_usage(const std::string& raw) const = 0;
virtual bool is_retryable(long http_code, const std::string& body) const = 0;
virtual int context_overflow_hint(const std::string& error_body) const = 0;
};
void register_dialect(const std::string& flavor,
std::function<std::unique_ptr<Dialect>()> factory,
const std::string& owner = "");Pure methods (build_chat_body, parse_*) are unit-testable without libcurl. Full contract:docs/spec/llm-client/dialect.md.
flowchart TB
provider["provider plugin<br/>flavor + presets + optional dialect factory"] --> select["Config::flavor set at selection"]
select --> make["make_dialect(flavor)<br/>resolved exactly once per client"]
make -->|known flavor| dialect
make -->|unknown flavor| fallback["openai fallback<br/>a typo never breaks a session"]
fallback --> dialect["Dialect implementation<br/>endpoints · auth · body shape"]
dialect --> request["build request body and headers"]
request --> transport["HTTP transport via libcurl<br/>retry · timeout · cancel check"]
transport --> endpoint["OpenAI-compatible endpoint"]
endpoint --> decode["stream decoder: SSE framing"]
decode --> chunks["StreamChunk per event → AgentHooks"]
decode --> usage["TokenUsage mapping"]
decode --> errors["is_retryable classification<br/>typed ApiError"]
request --> normalize["tool calls normalized to the internal shape<br/>at the dialect edge, buffered and streamed alike"]Wallet and allowance
If the vendor has an account balance or usage windows, declare aWalletCapability / AllowanceCapability. Supply only the fetch; the runtime owns polling, caching and rendering:
caps.push_back(std::make_unique<agent::WalletCapability>(
[](const agent::Config& cfg) -> std::optional<double> {
if (cfg.api_key.empty()) return std::nullopt;
const auto body = agent::http_get_with_bearer(
"https://api.deepseek.com/user/balance", cfg.api_key);
if (!body) return std::nullopt;
const double balance = parse_deepseek_balance(*body);
return balance < 0 ? std::nullopt : std::optional<double>(balance);
}));Return std::nullopt when there is nothing honest to report, a provider with no account API declares nothing and the bar shows-, never a fabricated number.
Flavor resolution
A flavor nobody provides falls back to openai: correct for a typo in a user provider file. A flavor provided by a disabled plugin fails loudly, naming the plugin and the fix, rather than silently speaking the wrong protocol to a configured endpoint.
Adding one, step by step
1. Config-only first: if the endpoint speaks the OpenAI wire protocol, a *user* needs no code at all, a provider file is enough. 2. New directory plugins/<id>/ (+ its object in Makefile.in). 3. Shared protocol? -> pass no dialect factory. Own protocol? -> write the dialect and register it. 4. Declare ProviderCapability (+ Wallet/Allowance if the vendor has them). 5. Register in make_bundled_plugins(). 6. Test hermetically: body/parse/stream fixtures, no live network.
The shipped set
Nine providers ship as plugins, each small enough to read in one sitting:
| Plugin | What it shows |
|---|---|
plugins/custom/ | Presets only, no endpoint, the user's own endpoint |
plugins/openrouter/ | Shared protocol + per-key wallet |
plugins/kilocode/ | Shared protocol + provider-specific wallet |
plugins/opencode_go/ | Shared protocol + allowance windows |
plugins/opencode_zen/ | Shared protocol, presets only |
plugins/commandcode/ | Shared protocol + 5h/weekly/monthly allowance |
plugins/deepseek/ | Shared protocol + prepaid-balance wallet |
plugins/anthropic/ | A vendor protocol the plugin itself provides |
plugins/gemini/ | A vendor protocol with a different streaming model |
The other bundled plugins are not providers. The tool set is one plugin per tunable unit (plugins/tool_search/, tool_read/,tool_write/, tool_bash/, tool_process/,tool_plan/, tool_task/,tool_skills/), the search backends are one plugin each (plugins/search_grep/, plugins/search_semantic/), and alongside them plugins/clock/ (the bar's clock),plugins/metrics/ (an event observer with no contributions),plugins/hello/ (the command example) andplugins/amber_self/ (the amber-config skill),23 bundled plugins in total. Seethe developer guide.