tftsr-devops_investigation/docs/architecture/adrs/ADR-003-provider-trait-pattern.md
Shaun Arman fdb4fc03b9 docs(architecture): add C4 diagrams, ADRs, and architecture overview
Comprehensive architecture documentation covering:

- docs/architecture/README.md: Full C4 model diagrams (system context,
  container, component), data flow sequences, security architecture,
  AI provider class diagram, CI/CD pipeline, and deployment diagrams.
  All diagrams use Mermaid for version-controlled diagram-as-code.

- docs/architecture/adrs/ADR-001: Tauri vs Electron decision rationale
- docs/architecture/adrs/ADR-002: SQLCipher encryption choices and
  cipher_page_size=16384 rationale for Apple Silicon
- docs/architecture/adrs/ADR-003: Provider trait + factory pattern
- docs/architecture/adrs/ADR-004: Regex + Aho-Corasick PII detection
- docs/architecture/adrs/ADR-005: Auto-generate encryption keys at
  runtime (documents the fix from PR #24)
- docs/architecture/adrs/ADR-006: Zustand state management rationale

- docs/wiki/Architecture.md: Updated module table (14 migrations, not
  10), corrected integrations description, updated startup sequence to
  reflect key auto-generation, added links to new ADR docs.

- README.md: Fixed stale database paths (tftsr → trcaa) and updated
  env var descriptions to reflect auto-generation behavior.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 09:35:35 -05:00

2.8 KiB

ADR-003: Provider Trait Pattern for AI Backends

Status: Accepted Date: 2025-Q3 Deciders: sarman


Context

The application must support multiple AI providers (OpenAI, Anthropic, Google Gemini, Mistral, Ollama) with different API formats, authentication methods, and response structures. Provider selection must be runtime-configurable by the user without recompiling.

Additionally, enterprise environments may need custom AI endpoints (e.g., MSI GenAI gateway at genai-service.commandcentral.com) that speak OpenAI-compatible APIs with custom auth headers.


Decision

Use a Rust trait object (Box<dyn Provider>) with a factory function (create_provider(config: ProviderConfig)) that dispatches to concrete implementations at runtime.


Rationale

The Provider trait:

#[async_trait]
pub trait Provider: Send + Sync {
    fn name(&self) -> &str;
    async fn chat(&self, messages: Vec<Message>, config: &ProviderConfig) -> Result<ChatResponse>;
    fn info(&self) -> ProviderInfo;
}

Why trait objects over generics:

  • Provider type is not known at compile time (user configures at runtime)
  • Box<dyn Provider> allows storing different providers in the same AppState
  • #[async_trait] enables async methods on trait objects (required for reqwest)

ProviderConfig design: The config struct uses Option<String> fields for provider-specific settings:

pub struct ProviderConfig {
    pub custom_endpoint_path: Option<String>,
    pub custom_auth_header: Option<String>,
    pub custom_auth_prefix: Option<String>,
    pub api_format: Option<String>,   // "openai" | "custom_rest"
}

This allows a single OpenAiProvider implementation to handle both standard OpenAI and arbitrary OpenAI-compatible endpoints — the user configures the auth header name and prefix to match their gateway.


Adding a New Provider

  1. Create src-tauri/src/ai/<provider>.rs implementing the Provider trait
  2. Add a match arm in create_provider() in provider.rs
  3. Register the provider type string in ProviderConfig
  4. Add UI in src/pages/Settings/AIProviders.tsx

No changes to command handlers or IPC layer required.


Consequences

Positive:

  • New providers require zero changes outside ai/
  • ProviderConfig is stored in the database — provider can be changed without app restart
  • test_provider_connection() command works uniformly across all providers
  • list_providers() returns capabilities dynamically (supports streaming, tool calling, etc.)

Negative:

  • dyn Provider has a small vtable dispatch overhead (negligible for HTTP-bound operations)
  • Each provider implementation must handle its own error types and response parsing
  • Testing requires mocking at the reqwest level (via mockito)