How to Implement a Custom Provider for a Proprietary LLM API in Goose: A Complete Guide
To integrate a private or proprietary LLM into Goose, you implement the Provider and ProviderDef traits, configure an ApiClient with your authentication method, and register the implementation via ProviderRegistry::register or a JSON declarative configuration.
Goose is an open-source agent framework from Block that abstracts LLM interactions through a unified provider interface. If your organization runs a custom model behind a private API, you can make it appear as a first-class provider by following the architecture patterns defined in crates/goose/src/providers/base.rs.
Understanding the Provider Architecture
Before writing code, you need to understand the three core abstractions that Goose uses to manage LLM integrations.
The Provider Trait
The Provider trait in crates/goose/src/providers/base.rs (lines 50-78) defines the runtime contract for streaming completions. Every provider must implement stream(), which returns a MessageStream of response chunks. The trait also includes optional methods for embeddings and OAuth configuration.
The ProviderDef Trait
The ProviderDef trait in crates/goose/src/providers/base.rs (lines 82-96) supplies static metadata and construction logic. It requires two methods:
metadata()– Returns aProviderMetadatastruct containing the display name, supported models, and configuration keys.from_env()– A factory function that reads environment variables or config files and returns an initialized provider instance.
The ProviderRegistry
The ProviderRegistry in crates/goose/src/providers/provider_registry.rs (lines 77-84) maintains a map of provider names to constructor functions. You register your implementation using register::<YourProvider>() or register_with_name() for dynamic registration.
Supporting Components
ApiClient(crates/goose/src/providers/api_client.rs): Handles HTTP requests, timeouts, header injection, and authentication methods (Bearer tokens, OAuth, etc.).ConfigKey(crates/goose/src/providers/base.rslines 58-76): Describes configuration fields exposed to users in the CLI or UI, including whether they are secret (stored in keychain) or required.DeclarativeProviderConfig(crates/goose/src/config/declarative_providers.rslines 60-71): Enables zero-code provider registration via JSON files placed in$XDG_CONFIG_HOME/goose/custom_providers/.
Step-by-Step Implementation
Follow these steps to build a complete provider for a proprietary API endpoint.
Step 1: Create the Provider Module
Create a new file at crates/goose/src/providers/mycorp.rs. This module will contain your struct and trait implementations.
use super::base::{
Provider, ProviderDef, ProviderMetadata, ProviderUsage, MessageStream,
stream_from_single_message, ConfigKey,
};
use super::api_client::{ApiClient, AuthMethod};
use crate::{model::ModelConfig, config::Config};
use anyhow::{Result, anyhow};
use async_trait::async_trait;
use futures::future::BoxFuture;
use rmcp::model::Tool;
Step 2: Define the Provider Struct
Your struct must hold an ApiClient instance and the ModelConfig:
pub struct MyCorpProvider {
api_client: ApiClient,
model: ModelConfig,
name: String,
}
Step 3: Implement ProviderDef for Static Metadata
Implement the metadata constructor to define how users configure your provider:
#[async_trait]
impl ProviderDef for MyCorpProvider {
type Provider = Self;
fn metadata() -> ProviderMetadata {
ProviderMetadata::new(
"mycorp", // Internal ID
"MyCorp LLM", // Display name in UI
"Proprietary LLM hosted at https://api.mycorp.com/v1",
"mycorp-7b", // Default model
vec!["mycorp-7b", "mycorp-13b"], // Supported models
"https://docs.mycorp.com/models", // Documentation URL
vec![
ConfigKey::new("MYCORP_API_KEY", true, true, None, true),
ConfigKey::new("MYCORP_CUSTOM_HEADER", false, false, None, false),
],
)
.with_setup_steps(vec![
"Create an API key in the MyCorp console",
"Run `goose configure` and paste the key when prompted",
])
}
fn from_env(
model: ModelConfig,
_extensions: Vec<crate::config::ExtensionConfig>,
) -> BoxFuture<'static, Result<Self::Provider>> {
Box::pin(async move {
let cfg = Config::global();
let api_key = cfg.get_secret::<String>("MYCORP_API_KEY")
.map_err(|_| anyhow!("Missing MYCORP_API_KEY"))?;
let base_url = cfg.get_param::<String>("MYCORP_HOST")
.unwrap_or_else(|_| "https://api.mycorp.com/v1".to_string());
let mut client = ApiClient::with_timeout(
base_url,
AuthMethod::BearerToken(api_key),
std::time::Duration::from_secs(600),
)?;
if let Ok(custom) = cfg.get_secret::<String>("MYCORP_CUSTOM_HEADER") {
client = client.with_header("X-MyCorp-Custom", &custom)?;
}
Ok(Self {
api_client: client,
model,
name: "mycorp".to_string(),
})
})
}
}
Step 4: Implement the Provider Trait for Runtime Logic
The stream() method builds the request payload, calls your API, and converts the response:
#[async_trait]
impl Provider for MyCorpProvider {
fn get_name(&self) -> &str {
&self.name
}
fn get_model_config(&self) -> ModelConfig {
self.model.clone()
}
async fn stream(
&self,
model_config: &ModelConfig,
session_id: &str,
system: &str,
messages: &[crate::conversation::message::Message],
tools: &[Tool],
) -> Result<MessageStream, super::errors::ProviderError> {
let payload = serde_json::json!({
"model": model_config.model_name,
"system": system,
"messages": messages,
"tools": tools,
"stream": true,
});
let response = self
.api_client
.response_post(Some(session_id), "chat/completions", &payload)
.await
.map_err(|e| super::errors::ProviderError::RequestFailed(e.to_string()))?;
// Parse response - adjust based on your API's format
let json = response.json().await?;
let message = super::formats::openai::response_to_message(&json)?;
let usage = super::formats::openai::get_usage(
json.get("usage").unwrap_or(&serde_json::Value::Null)
);
Ok(stream_from_single_message(message, ProviderUsage::new(
model_config.model_name.clone(),
usage
)))
}
}
Step 5: Register Your Provider
Add your provider to the registry in crates/goose/src/providers/init.rs:
use crate::providers::mycorp::MyCorpProvider;
pub fn register_all(registry: &mut ProviderRegistry) {
// Existing providers...
registry.register::<MyCorpProvider>(true); // true = preferred (shown first)
}
Alternative: Declarative Registration
If your API is OpenAI-compatible, you can skip Rust coding entirely. Create a JSON file at $XDG_CONFIG_HOME/goose/custom_providers/mycorp.json:
{
"name": "mycorp",
"engine": "openai",
"display_name": "MyCorp LLM",
"description": "Proprietary hosted model",
"api_key_env": "MYCORP_API_KEY",
"base_url": "https://api.mycorp.com/v1",
"models": [
{ "name": "mycorp-7b", "context_limit": 32768 }
],
"headers": { "X-MyCorp-Custom": "true" },
"supports_streaming": true
}
The declarative loader (declarative_providers.rs lines 60-71) automatically builds a provider using the OpenAI implementation with your custom endpoint and headers.
Summary
- Implement
ProviderDefincrates/goose/src/providers/base.rsto define metadata and construction logic for your proprietary LLM. - Implement
Providerto handle the actual HTTP requests and response streaming viaApiClient. - Use
ConfigKeyto declare environment variables and secrets that users must configure. - Register via
ProviderRegistryfor compiled-in providers, or use JSON declarative configs for OpenAI-compatible endpoints without code changes. - Reference
openai.rsas the canonical implementation showing streaming, authentication, and response parsing patterns.
Frequently Asked Questions
Do I need to modify the core Goose codebase to add a custom provider?
For compiled-in providers, you must add a new file under crates/goose/src/providers/ and register it in init.rs. However, if your proprietary API follows the OpenAI request/response format, you can use the declarative JSON approach by placing a configuration file in $XDG_CONFIG_HOME/goose/custom_providers/ without touching any Rust code.
How do I handle OAuth instead of API keys for authentication?
Use ConfigKey::new_oauth instead of the standard constructor in your metadata() function. You must then override the configure_oauth() method in your Provider implementation to handle the device-code or authorization-code flow, as the default implementation in base.rs (lines 47-63) returns an error.
What is the difference between programmatic and declarative provider registration?
Programmatic registration requires implementing the Provider and ProviderDef traits in Rust and calling registry.register::<YourProvider>(). This gives you full control over request formatting and response parsing. Declarative registration uses JSON files that the loader transforms into calls to register_with_name, leveraging existing engine implementations like OpenAI but with custom base URLs and headers.
How do I test my custom provider without calling the live API?
Study testprovider.rs in the Goose repository, which implements a record/replay provider for testing. You can create a mock provider that implements the Provider trait to return static responses, or use the ApiClient configuration to point to a local test server during development.
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 →