example-remote-client/src/providers/openrouter/api-provider.ts
Jerome 438ceb333c
Implement complete InferenceProvider system with OpenRouter integration
## 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>
2025-06-10 17:37:11 +01:00

155 lines
No EOL
3.9 KiB
TypeScript

// OpenRouter API Key Provider
import { InferenceProvider } from '@/types/inference';
import type {
Model,
InferenceRequest,
InferenceResponse,
AuthConfig,
ProviderCapabilities,
InferenceError,
} from '@/types/inference';
import { OpenRouterClient } from './client';
import type { OpenRouterApiConfig } from './types';
export class OpenRouterApiProvider extends InferenceProvider {
readonly name = 'OpenRouter (API Key)';
readonly id = 'openrouter-api';
private client: OpenRouterClient;
private apiKey?: string;
private _models: Model[] = [];
private _selectedModel?: Model;
private _authError?: string;
constructor(config?: Partial<OpenRouterApiConfig>) {
super();
this.client = new OpenRouterClient(config || {});
if (config?.apiKey) {
this.apiKey = config.apiKey;
}
if (config?.defaultModel) {
// We'll set this after loading models
}
}
get isAuthenticated(): boolean {
return !!this.apiKey && !this._authError;
}
get authError(): string | undefined {
return this._authError;
}
get models(): Model[] {
return this._models;
}
get selectedModel(): Model | undefined {
return this._selectedModel;
}
async authenticate(config: AuthConfig): Promise<void> {
if (config.type !== 'api_key' || !config.apiKey) {
throw new Error('OpenRouterApiProvider requires API key authentication');
}
this.apiKey = config.apiKey;
this._authError = undefined;
try {
// Test the API key by loading models
await this.loadModels();
} catch (error) {
this._authError = error instanceof Error ? error.message : 'Authentication failed';
this.apiKey = undefined;
throw error;
}
}
logout(): void {
this.apiKey = undefined;
this._authError = undefined;
this._models = [];
this._selectedModel = undefined;
}
async loadModels(): Promise<Model[]> {
if (!this.apiKey) {
throw new Error('Not authenticated');
}
try {
this._models = await this.client.fetchModels(this.apiKey);
// Check if currently selected model is still in the filtered list
if (this._selectedModel && !this._models.find(m => m.id === this._selectedModel!.id)) {
this._selectedModel = undefined;
}
// Set default model if none selected
if (!this._selectedModel && this._models.length > 0) {
// Try to find a good default model
const defaultModel = this._models.find(m =>
m.id.includes('gpt-4') ||
m.id.includes('claude') ||
m.id.includes('llama')
) || this._models[0];
this._selectedModel = defaultModel;
}
return this._models;
} catch (error) {
if (this.isInferenceError(error) && error.type === 'auth') {
this._authError = error.message;
this.apiKey = undefined;
}
throw error;
}
}
selectModel(modelId: string): void {
const model = this._models.find(m => m.id === modelId);
if (!model) {
throw new Error(`Model ${modelId} not found`);
}
this._selectedModel = model;
}
async generateResponse(request: InferenceRequest): Promise<InferenceResponse> {
if (!this.apiKey) {
throw new Error('Not authenticated');
}
if (!this._selectedModel) {
throw new Error('No model selected');
}
try {
return await this.client.generateResponse(
request,
this._selectedModel.id,
this.apiKey
);
} catch (error) {
if (this.isInferenceError(error) && error.type === 'auth') {
this._authError = error.message;
this.apiKey = undefined;
}
throw error;
}
}
getCapabilities(): ProviderCapabilities {
return {
authMethods: ['api_key'],
supportsModelListing: true,
requiresAuth: true,
};
}
private isInferenceError(error: any): error is InferenceError {
return error && typeof error === 'object' && 'type' in error && 'message' in error;
}
}