## Features - 🔧 InferenceProvider interface with pluggable architecture - 🔑 OpenRouter API key and OAuth PKCE authentication providers - 🛠️ Tool calling support with automatic agent loop execution - 📱 Interactive test UI with model selection and chat interface - 🎯 Provider-level filtering to tool-capable models only - ⚛️ React context with proper state management for UI reactivity ## Components - Core types and interfaces in src/types/inference.ts - OpenRouter client with shared utilities for both auth methods - Two separate providers (composition over inheritance): - OpenRouterApiProvider for API key authentication - OpenRouterOAuthProvider for OAuth PKCE flow - React InferenceContext with useInference hook - Interactive test UI with tool calling demonstration - OAuth callback routing with proper namespacing ## Tool Calling - 3 test tools: weather, calculator, time - Automatic tool execution and agent loop - Visual tool call display with arguments and results - Error handling for malformed tool calls ## Bug Fixes - Fixed model selection UI reactivity issue - Fixed OAuth popup race condition causing "cancelled" errors - Proper cleanup of event listeners and intervals ## Documentation - Comprehensive interface design document - Implementation strategy and key design decisions - Testing approach and security considerations 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
9.2 KiB
InferenceProvider Interface Design
This document outlines the design for the InferenceProvider interface and its implementations, which abstracts LLM inference capabilities for the MCP client.
Goals
- Provider Agnostic: Support multiple inference providers (OpenRouter, Anthropic, OpenAI, Google, etc.)
- Tool Calling: Native support for function/tool calling with MCP tools
- Authentication: Flexible auth patterns (API keys, OAuth, etc.)
- Model Selection: Dynamic model listing and selection
- Error Handling: Consistent error handling across providers
Core Interface
interface InferenceProvider {
// Provider identification
readonly name: string;
readonly id: string;
// Authentication state
readonly isAuthenticated: boolean;
readonly authError?: string;
// Available models
readonly models: Model[];
readonly selectedModel?: Model;
// Core inference method
generateResponse(request: InferenceRequest): Promise<InferenceResponse>;
// Model management
loadModels(): Promise<Model[]>;
selectModel(modelId: string): void;
// Authentication
authenticate(config: AuthConfig): Promise<void>;
logout(): void;
// Provider-specific capabilities
getCapabilities(): ProviderCapabilities;
}
Supporting Types
Model
interface Model {
id: string;
name: string;
description?: string;
contextLength: number;
inputCost?: number; // per token
outputCost?: number; // per token
provider: string;
capabilities: ModelCapabilities;
}
interface ModelCapabilities {
supportsTools: boolean;
supportsVision: boolean;
maxTokens: number;
}
Inference Request/Response
interface InferenceRequest {
messages: ChatMessage[];
tools?: Tool[]; // toolChoice defaults to 'auto' when tools provided
maxTokens?: number;
temperature?: number;
stopSequences?: string[];
}
interface InferenceResponse {
message: ChatMessage;
usage: TokenUsage;
stopReason: 'stop' | 'max_tokens' | 'tool_calls' | 'error';
error?: string;
}
interface ChatMessage {
role: 'user' | 'assistant' | 'system' | 'tool';
content: string | ContentBlock[];
toolCalls?: ToolCall[];
toolCallId?: string; // for tool response messages
}
interface ToolCall {
id: string;
type: 'function';
function: {
name: string;
arguments: string; // JSON string
};
}
Authentication
interface AuthConfig {
type: 'api_key' | 'oauth';
apiKey?: string;
oauthConfig?: {
clientId?: string;
redirectUri?: string;
scopes?: string[];
};
}
interface ProviderCapabilities {
authMethods: ('api_key' | 'oauth')[];
supportsModelListing: boolean;
supportsToolCalling: boolean;
requiresAuth: boolean;
}
Provider Implementations
OpenRouter Providers
For simplicity, we implement two separate providers using composition:
OpenRouterApiProvider:
- API key authentication only
- Simpler auth flow and error handling
- Direct API access
OpenRouterOAuthProvider:
- OAuth PKCE authentication only
- Browser-based auth popup flow
- Token refresh handling
Shared Configuration:
interface OpenRouterBaseConfig {
baseUrl?: string; // defaults to https://openrouter.ai/api/v1
defaultModel?: string;
httpReferrer?: string;
appName?: string;
}
interface OpenRouterApiConfig extends OpenRouterBaseConfig {
apiKey: string;
}
interface OpenRouterOAuthConfig extends OpenRouterBaseConfig {
clientId?: string; // for custom OAuth apps
redirectUri?: string;
}
Shared Logic: Both providers use composition with shared utilities:
OpenRouterClient- HTTP client and request formattingOpenRouterModelParser- Model list parsing and capabilitiesOpenRouterToolFormatter- Tool call formatting
Future Providers
AnthropicProvider:
- OAuth integration with Anthropic Console
- Claude model family support
- Message batching optimization
OpenAIProvider:
- OpenAI API key or OAuth
- GPT model family support
- Function calling optimization
GoogleProvider:
- Google Cloud OAuth
- Gemini model family support
- Tool calling with Google AI format
React Integration
Provider Context
interface InferenceContextValue {
provider: InferenceProvider | null;
isLoading: boolean;
error: string | null;
// Actions
setProvider: (provider: InferenceProvider) => void;
clearProvider: () => void;
generateResponse: (request: InferenceRequest) => Promise<InferenceResponse>;
selectModel: (modelId: string) => void;
loadModels: () => Promise<Model[]>;
// Convenience getters
models: Model[];
selectedModel: Model | undefined;
isAuthenticated: boolean;
}
const InferenceContext = createContext<InferenceContextValue | null>(null);
Hook Usage
const useInference = () => {
const context = useContext(InferenceContext);
if (!context) {
throw new Error('useInference must be used within InferenceProvider');
}
return context;
};
Error Handling
Error Types
interface InferenceError {
type: 'auth' | 'network' | 'rate_limit' | 'invalid_request' | 'provider_error';
message: string;
details?: any;
retryable: boolean;
}
Error Scenarios
- Authentication failures: Invalid API key, expired tokens
- Rate limiting: Provider-specific rate limits
- Network errors: Connection issues, timeouts
- Validation errors: Invalid tool schemas, malformed requests
- Provider errors: Service unavailable, model not found
Implementation Strategy
Phase 1: Basic OpenRouter Support ✅
- Implement core InferenceProvider interface
- Create OpenRouterApiProvider with API key auth
- Basic model listing and selection (filtered to tool-capable models only)
- Simple tool calling support (toolChoice=auto)
- React context with proper state management for UI reactivity
Phase 2: OAuth and Enhanced Features ✅
- Create OpenRouterOAuthProvider with PKCE auth
- Implement error recovery and retries
- Add usage tracking and cost estimation
- Improve tool call validation
Current Status
- ✅ Two OpenRouter providers (API key & OAuth) implemented
- ✅ Provider-level model filtering (only tool-capable models)
- ✅ React context with proper UI state synchronization
- ✅ Interactive test UI with tool calling demonstration
- ✅ OAuth callback routing for future MCP server integration
Phase 3: Multi-Provider Support
- Add AnthropicProvider
- Add OpenAIProvider
- Provider switching UI
- Unified configuration management
Testing Strategy
Unit Tests
- Mock provider implementations for testing
- Tool call serialization/deserialization
- Error handling scenarios
- Authentication flows
Integration Tests
- Real provider API integration (with test keys)
- End-to-end tool calling workflows
- Provider switching scenarios
- Authentication persistence
Security Considerations
- API Key Storage: Secure storage in browser (encrypted localStorage)
- OAuth Flow: Secure popup-based OAuth with PKCE
- Token Refresh: Automatic token refresh handling
- Rate Limiting: Client-side rate limiting to prevent abuse
- Input Validation: Strict validation of tool calls and responses
Key Design Decisions
1. Provider-Level Model Filtering
All providers filter models to only return those that support tool calling. This simplifies consumers and prevents tool-calling errors:
// In OpenRouterClient.fetchModels()
return response.data
.filter(model => model.supported_parameters?.includes('tools'))
.map(this.parseModel);
Benefits:
- No tool capability checking needed in UI components
- Eliminates "model doesn't support tools" errors
- Cleaner separation of concerns
2. React State Management for UI Reactivity
The React context tracks model selection in React state to ensure UI updates when models are selected:
const [selectedModelId, setSelectedModelId] = useState<string | undefined>();
// In selectModel callback
provider.selectModel(modelId); // Update provider internal state
setSelectedModelId(modelId); // Update React state → triggers re-render
// In context value - compute selectedModel from React-tracked ID
selectedModel: selectedModelId ? provider?.models.find(m => m.id === selectedModelId) : undefined
Why this pattern:
- Provider internal state changes don't trigger React re-renders automatically
- React needs to track state changes to update the UI
- Keeps provider logic decoupled from React specifics
3. Composition Over Inheritance
Two separate OpenRouter providers (API & OAuth) use shared utilities rather than complex inheritance:
OpenRouterClient- HTTP client and request formatting- Shared model parsing and tool formatting logic
- Clean separation of authentication concerns
Benefits:
- Simpler code paths (no branching auth logic)
- Easier testing and maintenance
- Clear separation of API vs OAuth concerns
Future Enhancements
- Caching: Response caching for repeated requests
- Load Balancing: Multi-provider load balancing
- Analytics: Usage analytics and monitoring
- Custom Models: Support for custom/fine-tuned models
- Advanced Tool Choice: Support for specific tool selection (beyond auto)
- Streaming Support: Real-time response streaming (if needed later)