Provider Extension
Add AI capabilities via ProviderExtensionTyped: return LlmProvider with chat() and stream_chat(), plus ToolProvider list_tools() and call_tool().
On this page
Extensions add AI capabilities by implementing ProviderExtensionTyped and returning LlmProvider and ToolProvider implementations.
LLM Providers
fn llm_providers(&self) -> Vec<Arc<dyn LlmProvider>> {
vec![
Arc::new(OpenAiProvider::new(self.config.openai_key.clone())),
Arc::new(AnthropicProvider::new(self.config.anthropic_key.clone())),
]
}
LlmProvider Trait
use systemprompt_provider_contracts::{
ChatRequest, ChatResponse, ChatStream, LlmProvider, LlmProviderResult,
};
pub struct OpenAiProvider {
api_key: String,
default_model: String,
}
#[async_trait]
impl LlmProvider for OpenAiProvider {
async fn chat(&self, request: &ChatRequest) -> LlmProviderResult<ChatResponse> {
// Call the OpenAI API
}
async fn stream_chat(&self, request: &ChatRequest) -> LlmProviderResult<ChatStream> {
// Stream from the OpenAI API
}
fn default_model(&self) -> &str {
&self.default_model
}
fn supports_model(&self, model: &str) -> bool {
model.starts_with("gpt-")
}
fn supports_streaming(&self) -> bool {
true
}
fn supports_tools(&self) -> bool {
true
}
}
Tool Providers
fn tool_providers(&self) -> Vec<Arc<dyn ToolProvider>> {
vec![
Arc::new(DatabaseToolProvider::new(self.pool.clone())),
Arc::new(FileSystemToolProvider::new(self.storage_path.clone())),
]
}
ToolProvider Trait
use std::collections::HashMap;
use systemprompt_identifiers::McpServerId;
use systemprompt_provider_contracts::{
ToolCallRequest, ToolCallResult, ToolContext, ToolDefinition, ToolProvider,
ToolProviderResult,
};
pub struct DatabaseToolProvider {
pool: Arc<PgPool>,
}
#[async_trait]
impl ToolProvider for DatabaseToolProvider {
async fn list_tools(
&self,
agent_name: &str,
context: &ToolContext,
) -> ToolProviderResult<Vec<ToolDefinition>> {
Ok(vec![
ToolDefinition::new("query", "database")
.with_description("Execute a SQL query"),
ToolDefinition::new("insert", "database")
.with_description("Insert a record"),
])
}
async fn call_tool(
&self,
request: &ToolCallRequest,
service_id: &McpServerId,
context: &ToolContext,
) -> ToolProviderResult<ToolCallResult> {
match request.name.as_str() {
"query" => self.execute_query(request).await,
"insert" => self.execute_insert(request).await,
other => Err(unknown_tool_error(other)),
}
}
async fn refresh_connections(&self, agent_name: &str) -> ToolProviderResult<()> {
Ok(())
}
async fn health_check(&self) -> ToolProviderResult<HashMap<String, bool>> {
Ok(HashMap::from([("database".to_string(), true)]))
}
}
ToolDefinition::new(name, service_id) takes the tool name and the service id it belongs to. Use the builder methods with_description, with_input_schema, and with_output_schema to attach a description and JSON schemas.
Typed Extension
use systemprompt::extension::prelude::ProviderExtensionTyped;
impl ProviderExtensionTyped for MyExtension {
fn llm_providers(&self) -> Vec<Arc<dyn LlmProvider>> {
vec![Arc::new(OpenAiProvider::default())]
}
fn tool_providers(&self) -> Vec<Arc<dyn ToolProvider>> {
vec![Arc::new(DatabaseToolProvider::new(self.pool.clone()))]
}
}
Both methods have empty default implementations, so implement only the side your extension provides.
Configuration
Configure providers in services/ai/config.yaml:
ai:
default_provider: anthropic
default_max_output_tokens: 4096
providers:
anthropic:
enabled: true
default_model: claude-sonnet-4-20250514
openai:
enabled: true
default_model: gpt-4-turbo
API keys are captured during just setup-local (or via systemprompt admin config secret), not stored inline in the YAML.