Skip to main content

Provider Extension

Add AI capabilities via ProviderExtensionTyped: return LlmProvider with chat() and stream_chat(), plus ToolProvider list_tools() and call_tool().

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.