## 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>
155 lines
No EOL
3.9 KiB
TypeScript
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;
|
|
}
|
|
} |