import 'dart:convert'; import 'package:http/http.dart' as http; /// Represents the availability status of a model for a specific provider class ModelProviderAvailability { final String providerId; final String status; // 'live' or 'staging' final String task; // e.g., 'conversational' final String? mappedLLMProviderId; // Our internal provider ID const ModelProviderAvailability({ required this.providerId, required this.status, required this.task, this.mappedLLMProviderId, }); bool get isLive => status == 'live'; factory ModelProviderAvailability.fromJson( String hfProviderId, Map json, ) { return ModelProviderAvailability( providerId: json['providerId'] ?? hfProviderId, status: json['status'] ?? 'unknown', task: json['task'] ?? 'unknown', ); } } /// Cached model availability information class ModelAvailabilityCache { final String modelId; final List providers; final DateTime lastUpdated; const ModelAvailabilityCache({ required this.modelId, required this.providers, required this.lastUpdated, }); bool get isExpired { final now = DateTime.now(); final difference = now.difference(lastUpdated); return difference.inSeconds > 30; // 30 seconds cache duration } List get liveProviders => providers.where((p) => p.isLive).toList(); } /// Service for querying and caching model availability from Hugging Face API class ModelAvailabilityService { static const String _baseUrl = 'https://huggingface.co/api/models'; // Cache for model availability data final Map _cache = {}; /// Mapping from HF provider IDs to our internal LLM provider IDs static const Map _providerMapping = { 'cerebras': 'cerebras', 'cohere': 'cohere', 'fal-ai': 'fal-ai', 'featherless': 'featherless', 'fireworks': 'fireworks', 'groq': 'groq', 'hf-inference': 'hf-inference', 'hyperbolic': 'hyperbolic', 'nebius': 'nebius', 'novita': 'novita', 'nscale': 'nscale', 'replicate': 'replicate', 'sambanova': 'sambanova', 'together': 'together', }; /// Get model availability, using cache if available and not expired Future getModelAvailability(String modelId) async { // Check cache first final cached = _cache[modelId]; if (cached != null && !cached.isExpired) { return cached; } // Fetch fresh data from API try { final availability = await _fetchModelAvailability(modelId); if (availability != null) { _cache[modelId] = availability; } return availability; } catch (e) { // If API call fails and we have cached data, return it even if expired if (cached != null) { return cached; } rethrow; } } /// Fetch model availability from Hugging Face API Future _fetchModelAvailability( String modelId, ) async { final url = '$_baseUrl/$modelId?expand[]=inferenceProviderMapping'; try { final response = await http.get( Uri.parse(url), headers: {'Accept': 'application/json', 'User-Agent': '#tikslop-App/1.0'}, ); if (response.statusCode == 200) { final data = json.decode(response.body) as Map; return _parseModelAvailability(modelId, data); } else if (response.statusCode == 404) { // Model not found, return empty availability return ModelAvailabilityCache( modelId: modelId, providers: [], lastUpdated: DateTime.now(), ); } else { throw ModelAvailabilityException( 'Failed to fetch model availability: HTTP ${response.statusCode}', ); } } catch (e) { if (e is ModelAvailabilityException) { rethrow; } throw ModelAvailabilityException('Network error: $e'); } } /// Parse the API response into ModelAvailabilityCache ModelAvailabilityCache _parseModelAvailability( String modelId, Map data, ) { final providers = []; final inferenceMapping = data['inferenceProviderMapping'] as Map?; if (inferenceMapping != null) { for (final entry in inferenceMapping.entries) { final hfProviderId = entry.key; final providerData = entry.value as Map; final availability = ModelProviderAvailability.fromJson( hfProviderId, providerData, ); // Map HF provider ID to our internal provider ID final mappedProviderId = _providerMapping[hfProviderId]; if (mappedProviderId != null) { providers.add( ModelProviderAvailability( providerId: availability.providerId, status: availability.status, task: availability.task, mappedLLMProviderId: mappedProviderId, ), ); } else { // Keep unmapped providers for potential future use providers.add(availability); } } } return ModelAvailabilityCache( modelId: modelId, providers: providers, lastUpdated: DateTime.now(), ); } /// Get list of compatible LLM providers for a model List getCompatibleProviders(String modelId) { final cached = _cache[modelId]; if (cached == null) { return []; } return cached.liveProviders .where((p) => p.mappedLLMProviderId != null) .map((p) => p.mappedLLMProviderId!) .toList(); } /// Check if a specific provider supports a model bool isProviderCompatible(String modelId, String llmProviderId) { final compatibleProviders = getCompatibleProviders(modelId); return compatibleProviders.contains(llmProviderId); } /// Get the provider-specific model name for a given model and provider String? getProviderSpecificModelName(String modelId, String llmProviderId) { final cached = _cache[modelId]; if (cached == null) { return null; } final providerAvailability = cached.liveProviders .where((p) => p.mappedLLMProviderId == llmProviderId) .firstOrNull; return providerAvailability?.providerId; } /// Clear cache for a specific model void clearCache(String modelId) { _cache.remove(modelId); } /// Clear all cached data void clearAllCache() { _cache.clear(); } /// Get cache status for debugging Map getCacheStatus() { return _cache.map((key, value) => MapEntry(key, !value.isExpired)); } } /// Exception thrown when model availability operations fail class ModelAvailabilityException implements Exception { final String message; ModelAvailabilityException(this.message); @override String toString() => 'ModelAvailabilityException: $message'; }