- Delete internal vendor API documentation and handoff docs - Remove vendor-specific AI gateway URLs from CSP whitelist - Replace vendor-specific log prefixes and comments with generic 'Custom REST' - Remove vendor-specific default auth header from custom REST implementation - Remove vendor-specific client header from HTTP requests - Remove backward-compat vendor format identifier from is_custom_rest_format() - Remove LEGACY_API_FORMAT constant and normalizeApiFormat() helper - Update test to not reference legacy format identifier - Update wiki docs to use generic enterprise gateway configuration - Update architecture diagrams and ADR-003 to remove vendor references - Add Buy Me A Coffee link to README - Update .gitignore to exclude internal user guide and ticket files Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2.7 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., an enterprise AI gateway) 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 sameAppState#[async_trait]enables async methods on trait objects (required forreqwest)
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 custom endpoints — the user configures the auth header name and prefix to match their gateway.
Adding a New Provider
- Create
src-tauri/src/ai/<provider>.rsimplementing theProvidertrait - Add a match arm in
create_provider()inprovider.rs - Register the provider type string in
ProviderConfig - 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/ ProviderConfigis stored in the database — provider can be changed without app restarttest_provider_connection()command works uniformly across all providerslist_providers()returns capabilities dynamically (supports streaming, tool calling, etc.)
Negative:
dyn Providerhas 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
reqwestlevel (viamockito)