From d0362039f29da601e23820427be5643aee79d093 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Thu, 28 Aug 2025 13:52:23 -0700 Subject: [PATCH 001/318] fix: enhance NPM publish process with unique timestamp versioning for continuous releases --- .github/workflows/ci.yml | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0e0f40a0f..b8ddbf3a2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -150,10 +150,25 @@ jobs: npm ci npm run build - # Publish with 'next' tag for continuous releases from master - # Fail fast if any publish fails - cd Common && npm publish --tag next --access public - cd ../Admin && npm publish --tag next --access public - cd ../Core && npm publish --tag next --access public + # Generate unique timestamp suffix for continuous releases + TIMESTAMP=$(date +%s) + + # Update version and publish Common first (others depend on it) + cd Common + CURRENT_VERSION=$(node -p "require('./package.json').version") + npm version "${CURRENT_VERSION}-next.${TIMESTAMP}" --no-git-tag-version + npm publish --tag next --access public + + # Update version and publish Admin + cd ../Admin + CURRENT_VERSION=$(node -p "require('./package.json').version") + npm version "${CURRENT_VERSION}-next.${TIMESTAMP}" --no-git-tag-version + npm publish --tag next --access public + + # Update version and publish Core + cd ../Core + CURRENT_VERSION=$(node -p "require('./package.json').version") + npm version "${CURRENT_VERSION}-next.${TIMESTAMP}" --no-git-tag-version + npm publish --tag next --access public env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} \ No newline at end of file From 8ad343751fb46c8de897d460c4720b5374dc8069 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Thu, 28 Aug 2025 15:27:51 -0700 Subject: [PATCH 002/318] fix: add SDK building process to development startup script for WebUI dependencies --- scripts/start-dev.sh | 47 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/scripts/start-dev.sh b/scripts/start-dev.sh index 40c8628a2..8c2cc8b11 100755 --- a/scripts/start-dev.sh +++ b/scripts/start-dev.sh @@ -97,6 +97,7 @@ clean_volumes() { rm -rf ./ConduitLLM.WebUI/.next 2>/dev/null || true rm -rf ./ConduitLLM.WebUI/node_modules 2>/dev/null || true rm -rf ./SDKs/Node/*/node_modules 2>/dev/null || true + rm -rf ./SDKs/Node/*/dist 2>/dev/null || true log_info "Volumes cleaned" } @@ -122,9 +123,52 @@ build_containers() { log_info "Containers built (CACHEBUST: $cachebust)" } +build_sdks() { + log_info "Building SDK packages for WebUI..." + + # Check if SDKs need building + if [[ ! -d "./SDKs/Node/Common/dist" ]] || [[ ! -d "./SDKs/Node/Core/dist" ]] || [[ ! -d "./SDKs/Node/Admin/dist" ]]; then + log_info "SDK packages not built, building now..." + + # Build Common SDK first (dependency for others) + if [[ -d "./SDKs/Node/Common" ]]; then + log_info "Building Common SDK..." + (cd ./SDKs/Node/Common && npm install && npm run build) || { + log_error "Failed to build Common SDK" + exit 1 + } + fi + + # Build Core SDK + if [[ -d "./SDKs/Node/Core" ]]; then + log_info "Building Core SDK..." + (cd ./SDKs/Node/Core && npm install && npm run build) || { + log_error "Failed to build Core SDK" + exit 1 + } + fi + + # Build Admin SDK + if [[ -d "./SDKs/Node/Admin" ]]; then + log_info "Building Admin SDK..." + (cd ./SDKs/Node/Admin && npm install && npm run build) || { + log_error "Failed to build Admin SDK" + exit 1 + } + fi + + log_info "SDK packages built successfully" + else + log_info "SDK packages already built, skipping..." + fi +} + rebuild_webui() { log_info "Restarting WebUI container to fix Next.js issues..." + # Ensure SDKs are built (WebUI depends on them) + build_sdks + # Stop and remove WebUI container docker compose -f docker-compose.yml -f docker-compose.dev.yml stop webui 2>/dev/null || true docker compose -f docker-compose.yml -f docker-compose.dev.yml rm -f webui 2>/dev/null || true @@ -230,6 +274,9 @@ main() { # Build containers build_containers "$build_flag" + # Build SDKs (required for WebUI) + build_sdks + # Start development environment start_development } From 24b33f49a8226bb0321f2807483bc4e5f112ba1e Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Thu, 28 Aug 2025 15:53:59 -0700 Subject: [PATCH 003/318] fix: add 'Provider Errors' item to Security & Monitoring section in Sidebar --- ConduitLLM.WebUI/src/components/layout/Sidebar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ConduitLLM.WebUI/src/components/layout/Sidebar.tsx b/ConduitLLM.WebUI/src/components/layout/Sidebar.tsx index 8a0381a61..3d7583fc4 100755 --- a/ConduitLLM.WebUI/src/components/layout/Sidebar.tsx +++ b/ConduitLLM.WebUI/src/components/layout/Sidebar.tsx @@ -47,6 +47,7 @@ const navigationSections = [ { id: 'ip-filtering', label: 'IP Filtering', href: '/ip-filtering', icon: IconShield }, { id: 'system-info', label: 'System Info', href: '/system-info', icon: IconInfoCircle }, { id: 'error-queues', label: 'Error Queues', href: '/error-queues', icon: IconBugOff }, + { id: 'provider-errors', label: 'Provider Errors', href: '/provider-errors', icon: IconBugOff }, ] }, { From aaf827e080a56ab542bfb4b7c93135bedad42ce2 Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Thu, 28 Aug 2025 15:54:22 -0700 Subject: [PATCH 004/318] fix: linter errors fixed in SDK tests --- .../__tests__/conversation-export.test.ts | 46 +++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/SDKs/Node/Core/src/chat/utils/__tests__/conversation-export.test.ts b/SDKs/Node/Core/src/chat/utils/__tests__/conversation-export.test.ts index ee3b39fdd..1e4e54eed 100644 --- a/SDKs/Node/Core/src/chat/utils/__tests__/conversation-export.test.ts +++ b/SDKs/Node/Core/src/chat/utils/__tests__/conversation-export.test.ts @@ -379,12 +379,12 @@ describe('ConversationImporter', () => { const result = ConversationImporter.fromJSON(json); expect(result.success).toBe(true); - if (result.success) { - expect(result.data!).toHaveLength(2); - expect(result.data![0].id).toBe('msg1'); - expect(result.data![0].role).toBe('user'); - expect(result.data![0].content).toBe('Hello'); - expect(result.data![0].timestamp).toBeInstanceOf(Date); + if (result.success && result.data) { + expect(result.data).toHaveLength(2); + expect(result.data[0].id).toBe('msg1'); + expect(result.data[0].role).toBe('user'); + expect(result.data[0].content).toBe('Hello'); + expect(result.data[0].timestamp).toBeInstanceOf(Date); } }); @@ -392,9 +392,9 @@ describe('ConversationImporter', () => { const result = ConversationImporter.fromJSON('invalid json'); expect(result.success).toBe(false); - if (!result.success) { - expect(result.errors!).toHaveLength(1); - expect(result.errors![0].code).toBe('INVALID_JSON'); + if (!result.success && result.errors) { + expect(result.errors).toHaveLength(1); + expect(result.errors[0].code).toBe('INVALID_JSON'); } }); @@ -413,7 +413,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.errors!.some(e => e.code === 'INVALID_ROLE')).toBe(true); + expect(result.errors?.some(e => e.code === 'INVALID_ROLE')).toBe(true); } }); @@ -434,9 +434,9 @@ describe('ConversationImporter', () => { expect(result.success).toBe(true); if (result.success) { - expect(result.data!).toHaveLength(10); - expect(result.warnings!).toHaveLength(1); - expect(result.warnings![0].code).toBe('MESSAGE_LIMIT_EXCEEDED'); + expect(result.data).toHaveLength(10); + expect(result.warnings).toHaveLength(1); + expect(result.warnings?.[0].code).toBe('MESSAGE_LIMIT_EXCEEDED'); } }); }); @@ -455,7 +455,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.errors![0].code).toBe('INVALID_FORMAT'); + expect(result.errors?.[0].code).toBe('INVALID_FORMAT'); } }); @@ -464,7 +464,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.errors![0].code).toBe('MISSING_MESSAGES'); + expect(result.errors?.[0].code).toBe('MISSING_MESSAGES'); } }); @@ -482,7 +482,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.errors!.some(e => e.code === 'MISSING_REQUIRED_FIELD')).toBe(true); + expect(result.errors?.some(e => e.code === 'MISSING_REQUIRED_FIELD')).toBe(true); } }); @@ -502,9 +502,9 @@ describe('ConversationImporter', () => { expect(result.success).toBe(true); if (result.success) { - expect(result.data!).toHaveLength(1); - expect(result.data![0].role).toBe('user'); // Should default invalid role - expect(result.warnings!.some(w => w.code === 'INVALID_ROLE')).toBe(true); + expect(result.data).toHaveLength(1); + expect(result.data?.[0].role).toBe('user'); // Should default invalid role + expect(result.warnings?.some(w => w.code === 'INVALID_ROLE')).toBe(true); } }); @@ -524,7 +524,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(false); if (!result.success) { - expect(result.errors!.some(e => e.code === 'INVALID_TIMESTAMP')).toBe(true); + expect(result.errors?.some(e => e.code === 'INVALID_TIMESTAMP')).toBe(true); } }); @@ -545,7 +545,7 @@ describe('ConversationImporter', () => { expect(result.success).toBe(true); if (result.success) { - expect(result.data![0].timestamp).toBeInstanceOf(Date); + expect(result.data?.[0].timestamp).toBeInstanceOf(Date); } }); }); @@ -670,9 +670,9 @@ describe('ConversationImporter', () => { const importResult = ConversationImporter.fromJSON(json); expect(importResult.success).toBe(true); - if (importResult.success) { + if (importResult.success && importResult.data) { expect(importResult.data).toHaveLength(2); - const imported = importResult.data!; + const imported = importResult.data; expect(imported[0].id).toBe('msg1'); expect(imported[0].content).toBe('Hello world!'); expect(imported[0].timestamp.getTime()).toBe(originalMessages[0].timestamp.getTime()); From d2179b888d72147b4f486fc86d9364afddc8ad9f Mon Sep 17 00:00:00 2001 From: Nick Nassiri Date: Fri, 29 Aug 2025 12:21:24 -0700 Subject: [PATCH 005/318] Refactor audio services: remove audio transcription and translation functionality - Deleted AudioService, AudioServiceBase, AudioServiceHybrid, AudioServiceSpeech, and AudioServiceTranscription classes. - Removed related types and imports from streaming model. - Cleaned up audio utility functions and ensured no references remain in the codebase. --- .../AudioConfigurationController.cs | 447 ---- .../ModelCapabilitiesController.cs | 24 - .../Controllers/ModelController.cs | 6 - .../Controllers/ModelCostsController.cs | 4 +- .../Controllers/RouterController.cs | 318 --- ...llbackConfigurationRepositoryExtensions.cs | 77 - .../Extensions/RepositoryExtensions.cs | 12 - .../Extensions/ServiceCollectionExtensions.cs | 6 - .../Interfaces/IAdminAudioCostService.cs | 81 - .../Interfaces/IAdminAudioProviderService.cs | 76 - .../Interfaces/IAdminAudioUsageService.cs | 61 - .../Interfaces/IAdminRouterService.cs | 70 - .../ModelCapabilities/CapabilitiesDto.cs | 67 - .../CreateCapabilitiesDto.cs | 68 - .../UpdateCapabilitiesDto.cs | 28 - .../Services/AdminAudioCostService.cs | 369 --- .../Services/AdminAudioProviderService.cs | 305 --- .../Services/AdminAudioUsageService.cs | 454 ---- .../AdminModelCostService.ImportExport.cs | 8 - .../Services/AdminModelCostService.Parsers.cs | 34 +- .../Services/AdminRouterService.cs | 201 -- .../AdminVirtualKeyService.Discovery.cs | 6 - .../DTOs/Audio/AudioCostDto.cs | 158 -- .../DTOs/Audio/AudioCostImportDto.cs | 18 - .../DTOs/Audio/AudioProviderConfigDto.cs | 165 -- .../DTOs/Audio/AudioUsageDto.cs | 450 ---- .../DTOs/Audio/RealtimeSessionDto.cs | 154 -- .../DTOs/Audio/TextToSpeechRequestDto.cs | 30 - .../DTOs/BulkImportResult.cs | 23 + .../DTOs/BulkModelMappingDto.cs | 14 - .../DTOs/ModelCostDto.Create.cs | 19 - .../DTOs/ModelCostDto.Update.cs | 19 - ConduitLLM.Configuration/DTOs/ModelCostDto.cs | 21 +- .../DTOs/ModelCostExportDto.cs | 4 - .../Data/ConfigurationDbContext.cs | 118 - .../Entities/AudioCost.cs | 81 - .../Entities/AudioProviderConfig.cs | 84 - .../Entities/AudioUsageLog.cs | 126 - .../Entities/FallbackConfigurationEntity.cs | 63 - .../Entities/FallbackModelMappingEntity.cs | 53 - .../Entities/ModelCapabilities.cs | 29 - .../Entities/ModelCost.cs | 45 - .../Entities/ModelDeploymentEntity.cs | 118 - .../Entities/ModelProviderMapping.cs | 37 - .../Entities/RouterConfigEntity.cs | 78 - .../Enums/PricingModel.cs | 6 +- .../Enums/ProviderType.cs | 6 +- .../Extensions/ServiceCollectionExtensions.cs | 9 - .../Interfaces/IAudioCostRepository.cs | 60 - .../IAudioProviderConfigRepository.cs | 55 - .../Interfaces/IAudioUsageLogRepository.cs | 67 - .../Interfaces/IConfigurationDbContext.cs | 33 - .../IFallbackConfigurationRepository.cs | 72 - .../IFallbackModelMappingRepository.cs | 71 - .../Interfaces/IModelDeploymentRepository.cs | 73 - .../Interfaces/IRouterConfigRepository.cs | 64 - ...0829050036_RemoveRoutingSystem.Designer.cs | 2265 +++++++++++++++++ .../20250829050036_RemoveRoutingSystem.cs | 213 ++ ...50829061847_RemoveAudioColumns.Designer.cs | 1773 +++++++++++++ .../20250829061847_RemoveAudioColumns.cs | 130 + ...pdateSnapshotAfterAudioRemoval.Designer.cs | 1743 +++++++++++++ ...9082745_UpdateSnapshotAfterAudioRemoval.cs | 24 + .../ConduitDbContextModelSnapshot.cs | 522 ---- .../Options/RouterOptions.cs | 94 - .../ProviderDefaultModels.cs | 61 - .../Repositories/AudioCostRepository.cs | 172 -- .../AudioProviderConfigRepository.cs | 130 - .../Repositories/AudioUsageLogRepository.cs | 324 --- .../FallbackConfigurationRepository.cs | 261 -- .../FallbackModelMappingRepository.cs | 243 -- .../Repositories/ModelDeploymentRepository.cs | 216 -- .../Repositories/RouterConfigRepository.cs | 231 -- ConduitLLM.Core/Conduit.cs | 145 +- .../Extensions/ServiceCollectionExtensions.cs | 51 +- .../Interfaces/IAudioAlertingService.cs | 270 -- .../Interfaces/IAudioAuditLogger.cs | 196 -- .../Interfaces/IAudioCapabilityDetector.cs | 252 -- .../Interfaces/IAudioCdnService.cs | 250 -- .../Interfaces/IAudioConnectionPool.cs | 164 -- .../Interfaces/IAudioContentFilter.cs | 48 - .../Interfaces/IAudioEncryptionService.cs | 115 - .../Interfaces/IAudioMetricsCollector.cs | 575 ----- .../Interfaces/IAudioPiiDetector.cs | 200 -- .../Interfaces/IAudioProcessingService.cs | 373 --- .../Interfaces/IAudioQualityTracker.cs | 403 --- ConduitLLM.Core/Interfaces/IAudioRouter.cs | 246 -- .../Interfaces/IAudioRoutingStrategy.cs | 256 -- .../Interfaces/IAudioStreamCache.cs | 210 -- .../Interfaces/IAudioTracingService.cs | 449 ---- .../Interfaces/IAudioTranscriptionClient.cs | 113 - ConduitLLM.Core/Interfaces/IConduit.cs | 6 - .../Interfaces/IHybridAudioService.cs | 146 -- ConduitLLM.Core/Interfaces/ILLMRouter.cs | 206 -- .../Interfaces/IModelCapabilityService.cs | 42 +- .../Interfaces/IModelSelectionStrategy.cs | 37 - .../Interfaces/IRealtimeAudioClient.cs | 223 -- .../Interfaces/IRealtimeConnectionManager.cs | 118 - .../Interfaces/IRealtimeMessageTranslator.cs | 166 -- .../IRealtimeMessageTranslatorFactory.cs | 35 - .../Interfaces/IRealtimeProxyService.cs | 154 -- .../Interfaces/IRealtimeSessionStore.cs | 88 - .../Interfaces/IRealtimeUsageTracker.cs | 168 -- .../Interfaces/IRouterConfigRepository.cs | 24 - ConduitLLM.Core/Interfaces/IRouterService.cs | 56 - .../Interfaces/ITextToSpeechClient.cs | 148 -- .../Models/Audio/AudioProviderCapabilities.cs | 352 --- .../Models/Audio/AudioTranscriptionRequest.cs | 246 -- .../Audio/AudioTranscriptionResponse.cs | 198 -- .../Models/Audio/ContentFilterResult.cs | 100 - .../Models/Audio/HybridAudioModels.cs | 461 ---- .../Models/Audio/RealtimeMessages.cs | 552 ---- .../Models/Audio/RealtimeSession.cs | 278 -- .../Models/Audio/RealtimeSessionConfig.cs | 315 --- .../Models/Audio/TextToSpeechRequest.cs | 252 -- .../Models/Audio/TextToSpeechResponse.cs | 286 --- ConduitLLM.Core/Models/Audio/TtsCacheEntry.cs | 23 - ConduitLLM.Core/Models/AudioCostResult.cs | 20 - ConduitLLM.Core/Models/CachedModelCost.cs | 19 - .../Configuration/ModelConfiguration.cs | 29 - .../Models/ProviderCapabilities.cs | 2 - .../Models/Realtime/ConnectionModels.cs | 99 - ConduitLLM.Core/Models/RefundResult.cs | 81 - .../Models/Routing/FallbackConfiguration.cs | 23 - .../Models/Routing/RouterConfig.cs | 183 -- ConduitLLM.Core/Models/Usage.cs | 10 - .../Providers/BaseProviderMetadata.cs | 4 +- .../Providers/Metadata/AllProviders.cs | 40 - .../Metadata/OpenAIProviderMetadata.cs | 4 +- ConduitLLM.Core/Routing/AudioRouter.cs | 274 -- .../CostOptimizedRoutingStrategy.cs | 207 -- .../LanguageOptimizedRoutingStrategy.cs | 192 -- .../LatencyBasedRoutingStrategy.cs | 162 -- .../QualityBasedRoutingStrategy.cs | 347 --- .../DefaultLLMRouter.ChatCompletion.cs | 434 ---- .../Routing/DefaultLLMRouter.Embedding.cs | 448 ---- .../DefaultLLMRouter.ModelSelection.cs | 243 -- .../Routing/DefaultLLMRouter.Streaming.cs | 242 -- ConduitLLM.Core/Routing/DefaultLLMRouter.cs | 468 ---- ConduitLLM.Core/Routing/RoutingStrategy.cs | 64 - .../HighestPriorityModelSelectionStrategy.cs | 52 - .../LeastCostModelSelectionStrategy.cs | 53 - .../LeastLatencyModelSelectionStrategy.cs | 52 - .../ModelSelectionStrategyFactory.cs | 83 - .../RoundRobinModelSelectionStrategy.cs | 43 - .../SimpleModelSelectionStrategy.cs | 25 - .../Services/AudioAlertingService.Core.cs | 150 -- .../AudioAlertingService.Evaluation.cs | 139 - .../AudioAlertingService.Management.cs | 139 - .../AudioAlertingService.Notifications.cs | 237 -- .../Services/AudioAlertingService.cs | 5 - ConduitLLM.Core/Services/AudioAuditLogger.cs | 196 -- .../Services/AudioCapabilityDetector.cs | 354 --- ConduitLLM.Core/Services/AudioCdnService.cs | 376 --- .../Services/AudioConnectionPool.cs | 440 ---- .../Services/AudioContentFilter.cs | 215 -- .../Services/AudioEncryptionService.cs | 200 -- .../AudioMetricsCollector.Recording.cs | 185 -- .../AudioMetricsCollector.Retrieval.cs | 358 --- .../Services/AudioMetricsCollector.Support.cs | 83 - .../Services/AudioMetricsCollector.cs | 56 - ConduitLLM.Core/Services/AudioPiiDetector.cs | 261 -- .../AudioProcessingService.Caching.cs | 87 - .../Services/AudioProcessingService.Core.cs | 81 - .../AudioProcessingService.Helpers.cs | 108 - .../AudioProcessingService.Metadata.cs | 162 -- .../AudioProcessingService.Processing.cs | 204 -- .../Services/AudioProcessingService.cs | 22 - .../Services/AudioQualityTracker.cs | 509 ---- ConduitLLM.Core/Services/AudioStreamCache.cs | 380 --- .../Services/AudioTracingService.Contexts.cs | 192 -- .../Services/AudioTracingService.Core.cs | 203 -- .../Services/AudioTracingService.Search.cs | 133 - .../Services/AudioTracingService.Utilities.cs | 148 -- .../Services/AudioTracingService.cs | 5 - .../ConfigurationModelCapabilityService.cs | 37 - .../CostCalculationService.PricingModels.cs | 14 +- .../Services/CostCalculationService.cs | 8 +- .../DatabaseModelCapabilityService.cs | 175 -- .../Services/HybridAudioService.Metrics.cs | 93 - .../Services/HybridAudioService.Processing.cs | 249 -- .../Services/HybridAudioService.Sessions.cs | 161 -- .../Services/HybridAudioService.cs | 80 - .../Services/HybridAudioServiceStreaming.cs | 337 --- .../Services/MonitoringAudioService.Core.cs | 41 - .../MonitoringAudioService.Realtime.cs | 129 - .../MonitoringAudioService.TextToSpeech.cs | 200 -- .../MonitoringAudioService.Transcription.cs | 112 - .../MonitoringAudioService.Utilities.cs | 103 - .../Services/MonitoringAudioService.cs | 22 - .../Services/ProviderMetadataRegistry.cs | 4 - .../Services/RealtimeSessionManager.cs | 250 -- .../Services/RealtimeSessionStore.cs | 289 --- ConduitLLM.Core/Services/RouterService.cs | 461 ---- .../Services/VirtualKeyTrackingAudioRouter.cs | 218 -- ConduitLLM.Core/Validation/UsageValidator.cs | 5 - .../Controllers/AudioController.cs | 409 --- .../Controllers/DiscoveryController.cs | 12 - .../Controllers/EmbeddingsController.cs | 11 +- .../Controllers/HybridAudioController.cs | 317 --- .../Controllers/ModelsController.cs | 31 +- .../Controllers/ProviderModelsController.cs | 7 +- .../Controllers/RealtimeController.cs | 262 -- .../Extensions/AudioServiceExtensions.cs | 107 - ConduitLLM.Http/Program.CoreServices.cs | 26 +- .../Services/DiscoveryCacheWarmingService.cs | 6 - .../Services/GracefulShutdownService.cs | 300 --- .../Services/RealtimeConnectionManager.cs | 389 --- .../RealtimeMessageTranslatorFactory.cs | 87 - .../RealtimeProxyService.ProxyOperations.cs | 200 -- .../Services/RealtimeProxyService.Tracking.cs | 358 --- .../Services/RealtimeProxyService.cs | 165 -- .../Services/RealtimeUsageTracker.cs | 307 --- .../Services/RedisModelCostCache.Helpers.cs | 4 - ConduitLLM.Http/Startup.Production.cs | 6 +- ConduitLLM.Providers/AWSTranscribeClient.cs | 380 --- .../Common/Models/ModelCapabilities.cs | 22 - .../Common/Models/ProviderRealtimeMessage.cs | 33 - .../DatabaseAwareLLMClientFactory.cs | 13 - ConduitLLM.Providers/OpenAIRealtimeSession.cs | 249 -- .../Providers/ElevenLabs/ElevenLabsClient.cs | 437 ---- .../Providers/ElevenLabs/ElevenLabsModels.cs | 30 - .../ElevenLabs/ElevenLabsRealtimeSession.cs | 258 -- .../Services/ElevenLabsRealtimeService.cs | 175 -- .../Services/ElevenLabsTextToSpeechService.cs | 161 -- .../Services/ElevenLabsVoiceService.cs | 61 - .../Providers/OpenAI/OpenAIClient.Audio.cs | 413 --- .../OpenAI/OpenAIClient.Capabilities.cs | 6 +- .../OpenAI/OpenAIClient.RealtimeAudio.cs | 284 --- .../OpenAI/OpenAIClient.Utilities.cs | 108 +- .../Providers/OpenAI/OpenAIClient.cs | 5 +- .../Providers/OpenAI/OpenAIModels.Audio.cs | 94 - .../OpenAICompatibleClient.Utilities.cs | 4 +- .../Ultravox/UltravoxClient.Realtime.cs | 182 -- .../Providers/Ultravox/UltravoxClient.cs | 201 -- .../Ultravox/UltravoxRealtimeSession.cs | 254 -- .../ElevenLabsRealtimeTranslator.cs | 440 ---- .../Translators/OpenAIRealtimeTranslatorV2.cs | 409 --- .../RealtimeMessageTranslatorFactory.cs | 124 - .../Translators/UltravoxRealtimeTranslator.cs | 384 --- .../CapabilitiesDtoTests.Mapping.cs | 99 - .../CapabilitiesDtoTests.Serialization.cs | 30 - .../CapabilitiesDtoTests.Validation.cs | 96 +- .../Models/Models/ModelDtoTests.Mapping.cs | 9 - .../AdminAudioUsageServiceTests.ByKey.cs | 78 - .../AdminAudioUsageServiceTests.ByProvider.cs | 62 - .../AdminAudioUsageServiceTests.Cleanup.cs | 33 - .../AdminAudioUsageServiceTests.Export.cs | 101 - ...AudioUsageServiceTests.RealtimeSessions.cs | 157 -- .../AdminAudioUsageServiceTests.Setup.cs | 149 -- .../AdminAudioUsageServiceTests.Summary.cs | 70 - .../AdminAudioUsageServiceTests.UsageLogs.cs | 90 - .../Services/AdminAudioUsageServiceTests.cs | 21 - .../Architecture/ModelArchitectureTests.cs | 9 - .../AudioProviderTypeMigrationTests.cs | 223 -- ...udioUsageLogRepositoryTests.CreateAsync.cs | 79 - ...ioUsageLogRepositoryTests.GetPagedAsync.cs | 195 -- ...UsageLogRepositoryTests.GetUsageSummary.cs | 53 - .../AudioUsageLogRepositoryTests.Helpers.cs | 57 - ...dioUsageLogRepositoryTests.OtherMethods.cs | 138 - .../AudioUsageLogRepositoryTests.Setup.cs | 40 - .../Core/Models/UsageSerializationTests.cs | 8 - ...ioCostCalculationServiceTests.EdgeCases.cs | 139 - ...dioCostCalculationServiceTests.Realtime.cs | 171 -- ...udioCostCalculationServiceTests.Refunds.cs | 276 -- .../AudioCostCalculationServiceTests.Setup.cs | 149 -- ...ostCalculationServiceTests.TextToSpeech.cs | 150 -- ...stCalculationServiceTests.Transcription.cs | 238 -- ...AudioEncryptionServiceTests.Concurrency.cs | 405 --- .../AudioEncryptionServiceTests.Core.cs | 35 - .../AudioEncryptionServiceTests.Decrypt.cs | 156 -- .../AudioEncryptionServiceTests.Encrypt.cs | 170 -- ...oEncryptionServiceTests.KeyAndIntegrity.cs | 108 - .../AudioMetricsCollectorTests.Advanced.cs | 369 --- .../AudioMetricsCollectorTests.Aggregation.cs | 376 --- .../AudioMetricsCollectorTests.Core.cs | 41 - ...ioMetricsCollectorTests.RealtimeRouting.cs | 175 -- ...oMetricsCollectorTests.TranscriptionTts.cs | 195 -- .../Core/Services/AudioStreamCacheTests.cs | 428 ---- .../Core/Services/ProviderRegistryTests.cs | 14 - .../RouterServiceTests.Configuration.cs | 83 - .../RouterServiceTests.Constructor.cs | 35 - .../Services/RouterServiceTests.Fallbacks.cs | 110 - .../RouterServiceTests.Initialization.cs | 79 - .../RouterServiceTests.ModelDeployments.cs | 249 -- .../Core/Services/RouterServiceTests.Setup.cs | 34 - .../Core/Services/RouterServiceTests.cs | 16 - .../Core/Validation/UsageValidatorTests.cs | 5 +- .../Builders/ModelProviderMappingBuilder.cs | 6 - .../Http/Controllers/AudioControllerTests.cs | 423 --- ...DiscoveryControllerGetCapabilitiesTests.cs | 5 +- .../GetModelsResponseStructureTests.cs | 3 - ...HybridAudioControllerTests.ProcessAudio.cs | 292 --- .../HybridAudioControllerTests.Session.cs | 195 -- ...dioControllerTests.StatusAndConstructor.cs | 136 - .../Controllers/HybridAudioControllerTests.cs | 55 - .../Http/Controllers/ModelsControllerTests.cs | 281 -- .../RealtimeControllerTests.Connect.cs | 208 -- .../RealtimeControllerTests.Core.cs | 75 - .../RealtimeControllerTests.GetConnections.cs | 96 - ...timeControllerTests.TerminateConnection.cs | 93 - .../ModelMappingIntegrationTests.cs | 253 -- .../Providers/OpenAIClientTests.Audio.cs | 367 --- .../OpenAIClientTests.Capabilities.cs | 226 -- .../DefaultLLMRouterTests.ChatCompletion.cs | 209 -- .../DefaultLLMRouterTests.Embedding.cs | 89 - .../DefaultLLMRouterTests.Initialization.cs | 85 - .../DefaultLLMRouterTests.OtherTests.cs | 31 - ...DefaultLLMRouterTests.RoutingStrategies.cs | 53 - .../DefaultLLMRouterTests.Streaming.cs | 93 - .../Routing/DefaultLLMRouterTests.cs | 161 -- .../model-costs/add/PricingConfigSections.tsx | 53 - .../src/app/model-costs/add/page.tsx | 4 - .../src/app/model-costs/add/types.ts | 4 - .../src/app/model-costs/add/validation.ts | 5 - .../components/EditModelCostModal.tsx | 26 - .../components/ImportModelCostsModal.tsx | 4 - .../components/ModelCostFormSections.tsx | 47 - .../components/ModelCostFormTypes.ts | 5 - .../components/PricingModelSelector.tsx | 6 - .../app/model-costs/utils/costFormatters.ts | 6 - .../src/app/model-costs/utils/csvHelpers.ts | 13 - .../src/app/models/[id]/providers/page.tsx | 14 + .../components/BulkActions.tsx | 194 -- .../components/ProviderList.tsx | 97 - .../components/ProviderRow.tsx | 279 -- .../components/ProviderStats.tsx | 232 -- .../ProviderPriorityManager/index.tsx | 341 --- .../utils/priorityHelpers.ts | 242 -- .../components/ProvidersTab.tsx | 1 - .../ProvidersTab/ProviderPriorityList.tsx | 340 --- .../components/ProvidersTab/index.tsx | 139 - .../components/ProviderSelection.tsx | 298 --- .../components/RuleEvaluationTrace.tsx | 274 -- .../RoutingTester/components/TestForm.tsx | 414 --- .../RoutingTester/components/TestHistory.tsx | 257 -- .../RoutingTester/components/TestResults.tsx | 275 -- .../RoutingTester/hooks/useRoutingTest.ts | 383 --- .../components/RoutingTester/index.tsx | 261 -- .../RoutingTester/utils/testHelpers.ts | 353 --- .../components/ActionSelector/ActionRow.tsx | 252 -- .../components/ActionSelector/index.tsx | 153 -- .../ConditionBuilder/ConditionRow.tsx | 228 -- .../components/ConditionBuilder/index.tsx | 203 -- .../RuleBuilder/components/RuleMetadata.tsx | 141 - .../RuleBuilder/components/RuleSummary.tsx | 243 -- .../RuleBuilder/hooks/useRuleValidation.ts | 233 -- .../components/RuleBuilder/index.tsx | 243 -- .../RuleBuilder/utils/ruleBuilder.ts | 431 ---- .../routing-settings/components/RulesTab.tsx | 1 - .../components/RulesTab/RuleEditor.tsx | 363 --- .../components/RulesTab/RulesList.tsx | 172 -- .../components/RulesTab/index.tsx | 209 -- .../components/TestingTab.tsx | 1 - .../components/TestingTab/RuleTester.tsx | 303 --- .../components/TestingTab/index.tsx | 11 - .../app/routing-settings/hooks/useModels.ts | 60 - .../hooks/useProviderPriorities.ts | 35 - .../routing-settings/hooks/useProviders.ts | 58 - .../routing-settings/hooks/useRoutingRules.ts | 237 -- .../src/app/routing-settings/page.tsx | 76 - .../src/app/routing-settings/types/models.ts | 26 - .../src/app/routing-settings/types/routing.ts | 196 -- .../app/routing-settings/utils/helpContent.ts | 177 -- .../src/components/models/ViewModelModal.tsx | 1 - ConduitLLM.WebUI/src/hooks/useCoreApi.ts | 109 - .../src/hooks/useDiscoveryModelsWithParams.ts | 3 - .../src/lib/constants/modelCapabilities.ts | 5 - ConduitLLM.WebUI/src/lib/navigation/items.ts | 10 +- ConduitLLM.WebUI/src/utils/modelHelpers.ts | 5 - ConduitLLM.WebUI/src/utils/typeGuards.ts | 9 - README.md | 6 - .../Node/Admin/src/FetchConduitAdminClient.ts | 3 - SDKs/Node/Admin/src/index.ts | 2 - .../Admin/src/models/audioConfiguration.ts | 493 ---- .../src/services/AudioConfigurationService.ts | 487 ---- SDKs/Node/Core/src/FetchConduitCoreClient.ts | 3 - SDKs/Node/Core/src/index.ts | 33 - SDKs/Node/Core/src/models/audio.ts | 446 ---- SDKs/Node/Core/src/models/streaming.ts | 5 - SDKs/Node/Core/src/services/AudioService.ts | 106 - .../Core/src/services/AudioServiceBase.ts | 93 - .../Core/src/services/AudioServiceHybrid.ts | 101 - .../Core/src/services/AudioServiceSpeech.ts | 121 - .../src/services/AudioServiceTranscription.ts | 166 -- SDKs/Node/Core/src/services/AudioUtils.ts | 108 - 385 files changed, 6267 insertions(+), 56898 deletions(-) delete mode 100644 ConduitLLM.Admin/Controllers/AudioConfigurationController.cs delete mode 100644 ConduitLLM.Admin/Controllers/RouterController.cs delete mode 100644 ConduitLLM.Admin/Extensions/FallbackConfigurationRepositoryExtensions.cs delete mode 100644 ConduitLLM.Admin/Interfaces/IAdminAudioCostService.cs delete mode 100644 ConduitLLM.Admin/Interfaces/IAdminAudioProviderService.cs delete mode 100644 ConduitLLM.Admin/Interfaces/IAdminAudioUsageService.cs delete mode 100644 ConduitLLM.Admin/Interfaces/IAdminRouterService.cs delete mode 100644 ConduitLLM.Admin/Services/AdminAudioCostService.cs delete mode 100644 ConduitLLM.Admin/Services/AdminAudioProviderService.cs delete mode 100644 ConduitLLM.Admin/Services/AdminAudioUsageService.cs delete mode 100644 ConduitLLM.Admin/Services/AdminRouterService.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/AudioCostDto.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/AudioCostImportDto.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/AudioProviderConfigDto.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/AudioUsageDto.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/RealtimeSessionDto.cs delete mode 100644 ConduitLLM.Configuration/DTOs/Audio/TextToSpeechRequestDto.cs create mode 100644 ConduitLLM.Configuration/DTOs/BulkImportResult.cs delete mode 100644 ConduitLLM.Configuration/Entities/AudioCost.cs delete mode 100644 ConduitLLM.Configuration/Entities/AudioProviderConfig.cs delete mode 100644 ConduitLLM.Configuration/Entities/AudioUsageLog.cs delete mode 100644 ConduitLLM.Configuration/Entities/FallbackConfigurationEntity.cs delete mode 100644 ConduitLLM.Configuration/Entities/FallbackModelMappingEntity.cs delete mode 100644 ConduitLLM.Configuration/Entities/ModelDeploymentEntity.cs delete mode 100644 ConduitLLM.Configuration/Entities/RouterConfigEntity.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IAudioCostRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IAudioProviderConfigRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IAudioUsageLogRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IFallbackConfigurationRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IFallbackModelMappingRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IModelDeploymentRepository.cs delete mode 100644 ConduitLLM.Configuration/Interfaces/IRouterConfigRepository.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.Designer.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.Designer.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.Designer.cs create mode 100644 ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.cs delete mode 100644 ConduitLLM.Configuration/Options/RouterOptions.cs delete mode 100644 ConduitLLM.Configuration/Repositories/AudioCostRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/AudioProviderConfigRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/AudioUsageLogRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/FallbackConfigurationRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/FallbackModelMappingRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/ModelDeploymentRepository.cs delete mode 100644 ConduitLLM.Configuration/Repositories/RouterConfigRepository.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioAlertingService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioAuditLogger.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioCapabilityDetector.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioCdnService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioConnectionPool.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioContentFilter.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioEncryptionService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioMetricsCollector.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioPiiDetector.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioProcessingService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioQualityTracker.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioRouter.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioRoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioStreamCache.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioTracingService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IAudioTranscriptionClient.cs delete mode 100644 ConduitLLM.Core/Interfaces/IHybridAudioService.cs delete mode 100644 ConduitLLM.Core/Interfaces/ILLMRouter.cs delete mode 100644 ConduitLLM.Core/Interfaces/IModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeAudioClient.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeConnectionManager.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeMessageTranslator.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeMessageTranslatorFactory.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeProxyService.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeSessionStore.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRealtimeUsageTracker.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRouterConfigRepository.cs delete mode 100644 ConduitLLM.Core/Interfaces/IRouterService.cs delete mode 100644 ConduitLLM.Core/Interfaces/ITextToSpeechClient.cs delete mode 100644 ConduitLLM.Core/Models/Audio/AudioProviderCapabilities.cs delete mode 100644 ConduitLLM.Core/Models/Audio/AudioTranscriptionRequest.cs delete mode 100644 ConduitLLM.Core/Models/Audio/AudioTranscriptionResponse.cs delete mode 100644 ConduitLLM.Core/Models/Audio/ContentFilterResult.cs delete mode 100644 ConduitLLM.Core/Models/Audio/HybridAudioModels.cs delete mode 100644 ConduitLLM.Core/Models/Audio/RealtimeMessages.cs delete mode 100644 ConduitLLM.Core/Models/Audio/RealtimeSession.cs delete mode 100644 ConduitLLM.Core/Models/Audio/RealtimeSessionConfig.cs delete mode 100644 ConduitLLM.Core/Models/Audio/TextToSpeechRequest.cs delete mode 100644 ConduitLLM.Core/Models/Audio/TextToSpeechResponse.cs delete mode 100644 ConduitLLM.Core/Models/Audio/TtsCacheEntry.cs delete mode 100644 ConduitLLM.Core/Models/AudioCostResult.cs delete mode 100644 ConduitLLM.Core/Models/Realtime/ConnectionModels.cs delete mode 100644 ConduitLLM.Core/Models/Routing/FallbackConfiguration.cs delete mode 100644 ConduitLLM.Core/Models/Routing/RouterConfig.cs delete mode 100644 ConduitLLM.Core/Routing/AudioRouter.cs delete mode 100644 ConduitLLM.Core/Routing/AudioRoutingStrategies/CostOptimizedRoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/AudioRoutingStrategies/LanguageOptimizedRoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/AudioRoutingStrategies/LatencyBasedRoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/AudioRoutingStrategies/QualityBasedRoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/DefaultLLMRouter.ChatCompletion.cs delete mode 100644 ConduitLLM.Core/Routing/DefaultLLMRouter.Embedding.cs delete mode 100644 ConduitLLM.Core/Routing/DefaultLLMRouter.ModelSelection.cs delete mode 100644 ConduitLLM.Core/Routing/DefaultLLMRouter.Streaming.cs delete mode 100644 ConduitLLM.Core/Routing/DefaultLLMRouter.cs delete mode 100644 ConduitLLM.Core/Routing/RoutingStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/HighestPriorityModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/LeastCostModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/LeastLatencyModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/ModelSelectionStrategyFactory.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/RoundRobinModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Routing/Strategies/SimpleModelSelectionStrategy.cs delete mode 100644 ConduitLLM.Core/Services/AudioAlertingService.Core.cs delete mode 100644 ConduitLLM.Core/Services/AudioAlertingService.Evaluation.cs delete mode 100644 ConduitLLM.Core/Services/AudioAlertingService.Management.cs delete mode 100644 ConduitLLM.Core/Services/AudioAlertingService.Notifications.cs delete mode 100644 ConduitLLM.Core/Services/AudioAlertingService.cs delete mode 100644 ConduitLLM.Core/Services/AudioAuditLogger.cs delete mode 100644 ConduitLLM.Core/Services/AudioCapabilityDetector.cs delete mode 100644 ConduitLLM.Core/Services/AudioCdnService.cs delete mode 100644 ConduitLLM.Core/Services/AudioConnectionPool.cs delete mode 100644 ConduitLLM.Core/Services/AudioContentFilter.cs delete mode 100644 ConduitLLM.Core/Services/AudioEncryptionService.cs delete mode 100644 ConduitLLM.Core/Services/AudioMetricsCollector.Recording.cs delete mode 100644 ConduitLLM.Core/Services/AudioMetricsCollector.Retrieval.cs delete mode 100644 ConduitLLM.Core/Services/AudioMetricsCollector.Support.cs delete mode 100644 ConduitLLM.Core/Services/AudioMetricsCollector.cs delete mode 100644 ConduitLLM.Core/Services/AudioPiiDetector.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.Caching.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.Core.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.Helpers.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.Metadata.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.Processing.cs delete mode 100644 ConduitLLM.Core/Services/AudioProcessingService.cs delete mode 100644 ConduitLLM.Core/Services/AudioQualityTracker.cs delete mode 100644 ConduitLLM.Core/Services/AudioStreamCache.cs delete mode 100644 ConduitLLM.Core/Services/AudioTracingService.Contexts.cs delete mode 100644 ConduitLLM.Core/Services/AudioTracingService.Core.cs delete mode 100644 ConduitLLM.Core/Services/AudioTracingService.Search.cs delete mode 100644 ConduitLLM.Core/Services/AudioTracingService.Utilities.cs delete mode 100644 ConduitLLM.Core/Services/AudioTracingService.cs delete mode 100644 ConduitLLM.Core/Services/HybridAudioService.Metrics.cs delete mode 100644 ConduitLLM.Core/Services/HybridAudioService.Processing.cs delete mode 100644 ConduitLLM.Core/Services/HybridAudioService.Sessions.cs delete mode 100644 ConduitLLM.Core/Services/HybridAudioService.cs delete mode 100644 ConduitLLM.Core/Services/HybridAudioServiceStreaming.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.Core.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.Realtime.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.TextToSpeech.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.Transcription.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.Utilities.cs delete mode 100644 ConduitLLM.Core/Services/MonitoringAudioService.cs delete mode 100644 ConduitLLM.Core/Services/RealtimeSessionManager.cs delete mode 100644 ConduitLLM.Core/Services/RealtimeSessionStore.cs delete mode 100644 ConduitLLM.Core/Services/RouterService.cs delete mode 100644 ConduitLLM.Core/Services/VirtualKeyTrackingAudioRouter.cs delete mode 100644 ConduitLLM.Http/Controllers/AudioController.cs delete mode 100644 ConduitLLM.Http/Controllers/HybridAudioController.cs delete mode 100644 ConduitLLM.Http/Controllers/RealtimeController.cs delete mode 100644 ConduitLLM.Http/Extensions/AudioServiceExtensions.cs delete mode 100644 ConduitLLM.Http/Services/GracefulShutdownService.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeConnectionManager.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeMessageTranslatorFactory.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeProxyService.ProxyOperations.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeProxyService.Tracking.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeProxyService.cs delete mode 100644 ConduitLLM.Http/Services/RealtimeUsageTracker.cs delete mode 100644 ConduitLLM.Providers/AWSTranscribeClient.cs delete mode 100644 ConduitLLM.Providers/Common/Models/ProviderRealtimeMessage.cs delete mode 100644 ConduitLLM.Providers/OpenAIRealtimeSession.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsClient.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsModels.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsRealtimeSession.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsRealtimeService.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsTextToSpeechService.cs delete mode 100644 ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsVoiceService.cs delete mode 100644 ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Audio.cs delete mode 100644 ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.RealtimeAudio.cs delete mode 100644 ConduitLLM.Providers/Providers/OpenAI/OpenAIModels.Audio.cs delete mode 100644 ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.Realtime.cs delete mode 100644 ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.cs delete mode 100644 ConduitLLM.Providers/Providers/Ultravox/UltravoxRealtimeSession.cs delete mode 100644 ConduitLLM.Providers/Translators/ElevenLabsRealtimeTranslator.cs delete mode 100644 ConduitLLM.Providers/Translators/OpenAIRealtimeTranslatorV2.cs delete mode 100644 ConduitLLM.Providers/Translators/RealtimeMessageTranslatorFactory.cs delete mode 100644 ConduitLLM.Providers/Translators/UltravoxRealtimeTranslator.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByKey.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByProvider.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Cleanup.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Export.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.RealtimeSessions.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Setup.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Summary.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.UsageLogs.cs delete mode 100644 ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.cs delete mode 100644 ConduitLLM.Tests/Configuration/Migrations/AudioProviderTypeMigrationTests.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.CreateAsync.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetPagedAsync.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetUsageSummary.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Helpers.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.OtherMethods.cs delete mode 100644 ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Setup.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.EdgeCases.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Realtime.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Refunds.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Setup.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.TextToSpeech.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Transcription.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Concurrency.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Core.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Decrypt.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Encrypt.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.KeyAndIntegrity.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Advanced.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Aggregation.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Core.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.RealtimeRouting.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.TranscriptionTts.cs delete mode 100644 ConduitLLM.Tests/Core/Services/AudioStreamCacheTests.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.Configuration.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.Constructor.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.Fallbacks.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.Initialization.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.ModelDeployments.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.Setup.cs delete mode 100644 ConduitLLM.Tests/Core/Services/RouterServiceTests.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/AudioControllerTests.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.ProcessAudio.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.Session.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.StatusAndConstructor.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/ModelsControllerTests.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Connect.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Core.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.GetConnections.cs delete mode 100644 ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.TerminateConnection.cs delete mode 100644 ConduitLLM.Tests/Integration/ModelMappingIntegrationTests.cs delete mode 100644 ConduitLLM.Tests/Providers/OpenAIClientTests.Audio.cs delete mode 100644 ConduitLLM.Tests/Providers/OpenAIClientTests.Capabilities.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.ChatCompletion.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Embedding.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Initialization.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.OtherTests.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.RoutingStrategies.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Streaming.cs delete mode 100644 ConduitLLM.Tests/Routing/DefaultLLMRouterTests.cs create mode 100644 ConduitLLM.WebUI/src/app/models/[id]/providers/page.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/BulkActions.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderList.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderRow.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderStats.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/utils/priorityHelpers.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab/ProviderPriorityList.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/components/ProviderSelection.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/components/RuleEvaluationTrace.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/components/TestForm.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/components/TestHistory.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/components/TestResults.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/hooks/useRoutingTest.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RoutingTester/utils/testHelpers.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/ActionSelector/ActionRow.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/ActionSelector/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/ConditionBuilder/ConditionRow.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/ConditionBuilder/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/RuleMetadata.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/components/RuleSummary.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/hooks/useRuleValidation.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RuleBuilder/utils/ruleBuilder.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RulesTab.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RulesTab/RuleEditor.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RulesTab/RulesList.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/RulesTab/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/TestingTab.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/TestingTab/RuleTester.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/components/TestingTab/index.tsx delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/hooks/useModels.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/hooks/useProviderPriorities.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/hooks/useProviders.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/hooks/useRoutingRules.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/page.tsx delete mode 100644 ConduitLLM.WebUI/src/app/routing-settings/types/models.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/types/routing.ts delete mode 100755 ConduitLLM.WebUI/src/app/routing-settings/utils/helpContent.ts delete mode 100755 SDKs/Node/Admin/src/models/audioConfiguration.ts delete mode 100755 SDKs/Node/Admin/src/services/AudioConfigurationService.ts delete mode 100755 SDKs/Node/Core/src/models/audio.ts delete mode 100755 SDKs/Node/Core/src/services/AudioService.ts delete mode 100644 SDKs/Node/Core/src/services/AudioServiceBase.ts delete mode 100644 SDKs/Node/Core/src/services/AudioServiceHybrid.ts delete mode 100644 SDKs/Node/Core/src/services/AudioServiceSpeech.ts delete mode 100644 SDKs/Node/Core/src/services/AudioServiceTranscription.ts delete mode 100644 SDKs/Node/Core/src/services/AudioUtils.ts diff --git a/ConduitLLM.Admin/Controllers/AudioConfigurationController.cs b/ConduitLLM.Admin/Controllers/AudioConfigurationController.cs deleted file mode 100644 index 34295be0d..000000000 --- a/ConduitLLM.Admin/Controllers/AudioConfigurationController.cs +++ /dev/null @@ -1,447 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers -{ - /// - /// Controller for managing audio provider configurations, costs, and usage analytics. - /// - [ApiController] - [Route("api/admin/audio")] - [Authorize(Policy = "MasterKeyPolicy")] - public class AudioConfigurationController : ControllerBase - { - private readonly IAdminAudioProviderService _providerService; - private readonly IAdminAudioCostService _costService; - private readonly IAdminAudioUsageService _usageService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public AudioConfigurationController( - IAdminAudioProviderService providerService, - IAdminAudioCostService costService, - IAdminAudioUsageService usageService, - ILogger logger) - { - _providerService = providerService; - _costService = costService; - _usageService = usageService; - _logger = logger; - } - - #region Provider Configuration Endpoints - - /// - /// Gets all audio provider configurations. - /// - /// Returns the list of audio provider configurations - [HttpGet("providers")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetProviders() - { - var providers = await _providerService.GetAllAsync(); - return Ok(providers); - } - - /// - /// Gets a specific audio provider configuration. - /// - /// The provider configuration ID - /// Returns the audio provider configuration - /// If the provider configuration is not found - [HttpGet("providers/{id}")] - [ProducesResponseType(typeof(AudioProviderConfigDto), 200)] - [ProducesResponseType(404)] - public async Task GetProvider(int id) - { - var provider = await _providerService.GetByIdAsync(id); - if (provider == null) - return NotFound(); - - return Ok(provider); - } - - /// - /// Gets audio provider configurations by provider ID. - /// - /// The provider ID - /// Returns the list of configurations for the provider - [HttpGet("providers/by-id/{providerId}")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetProvidersById(int providerId) - { - var providers = await _providerService.GetByProviderAsync(providerId); - return Ok(providers); - } - - /// - /// Gets enabled providers for a specific audio operation. - /// - /// The operation type (transcription, tts, realtime) - /// Returns the list of enabled providers - [HttpGet("providers/enabled/{operationType}")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetEnabledProviders(string operationType) - { - var providers = await _providerService.GetEnabledForOperationAsync(operationType); - return Ok(providers); - } - - /// - /// Creates a new audio provider configuration. - /// - /// The provider configuration to create - /// Returns the created provider configuration - /// If the configuration is invalid - [HttpPost("providers")] - [ProducesResponseType(typeof(AudioProviderConfigDto), 201)] - [ProducesResponseType(400)] - public async Task CreateProvider([FromBody] CreateAudioProviderConfigDto dto) - { - try - { - var provider = await _providerService.CreateAsync(dto); - return CreatedAtAction(nameof(GetProvider), new { id = provider.Id }, provider); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - } - - /// - /// Updates an audio provider configuration. - /// - /// The provider configuration ID - /// The updated configuration - /// Returns the updated provider configuration - /// If the provider configuration is not found - [HttpPut("providers/{id}")] - [ProducesResponseType(typeof(AudioProviderConfigDto), 200)] - [ProducesResponseType(404)] - public async Task UpdateProvider(int id, [FromBody] UpdateAudioProviderConfigDto dto) - { - var provider = await _providerService.UpdateAsync(id, dto); - if (provider == null) - return NotFound(); - - return Ok(provider); - } - - /// - /// Deletes an audio provider configuration. - /// - /// The provider configuration ID - /// If the provider configuration was deleted - /// If the provider configuration is not found - [HttpDelete("providers/{id}")] - [ProducesResponseType(204)] - [ProducesResponseType(404)] - public async Task DeleteProvider(int id) - { - var deleted = await _providerService.DeleteAsync(id); - if (!deleted) - return NotFound(); - - return NoContent(); - } - - /// - /// Tests audio provider connectivity. - /// - /// The provider configuration ID - /// The operation type to test - /// Returns the test results - /// If the provider configuration is not found - [HttpPost("providers/{id}/test")] - [ProducesResponseType(typeof(AudioProviderTestResult), 200)] - [ProducesResponseType(404)] - public async Task TestProvider(int id, [FromQuery] string operationType = "transcription") - { - try - { - var result = await _providerService.TestProviderAsync(id, operationType); - return Ok(result); - } - catch (KeyNotFoundException) - { - return NotFound(); - } - } - - #endregion - - #region Cost Configuration Endpoints - - /// - /// Gets all audio cost configurations. - /// - /// Returns the list of audio cost configurations - [HttpGet("costs")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetCosts() - { - var costs = await _costService.GetAllAsync(); - return Ok(costs); - } - - /// - /// Gets a specific audio cost configuration. - /// - /// The cost configuration ID - /// Returns the audio cost configuration - /// If the cost configuration is not found - [HttpGet("costs/{id}")] - [ProducesResponseType(typeof(AudioCostDto), 200)] - [ProducesResponseType(404)] - public async Task GetCost(int id) - { - var cost = await _costService.GetByIdAsync(id); - if (cost == null) - return NotFound(); - - return Ok(cost); - } - - /// - /// Gets audio costs by provider. - /// - /// The provider ID - /// Returns the list of costs for the provider - [HttpGet("costs/by-provider/{providerId}")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetCostsByProvider(int providerId) - { - var costs = await _costService.GetByProviderAsync(providerId); - return Ok(costs); - } - - /// - /// Gets the current cost for a specific operation. - /// - /// The provider ID - /// The operation type - /// The model name (optional) - /// Returns the current cost - /// If no cost is found - [HttpGet("costs/current")] - [ProducesResponseType(typeof(AudioCostDto), 200)] - [ProducesResponseType(404)] - public async Task GetCurrentCost( - [FromQuery] int providerId, - [FromQuery] string operationType, - [FromQuery] string? model = null) - { - var cost = await _costService.GetCurrentCostAsync(providerId, operationType, model); - if (cost == null) - return NotFound(); - - return Ok(cost); - } - - /// - /// Creates a new audio cost configuration. - /// - /// The cost configuration to create - /// Returns the created cost configuration - /// If the configuration is invalid - [HttpPost("costs")] - [ProducesResponseType(typeof(AudioCostDto), 201)] - [ProducesResponseType(400)] - public async Task CreateCost([FromBody] CreateAudioCostDto dto) - { - try - { - var cost = await _costService.CreateAsync(dto); - return CreatedAtAction(nameof(GetCost), new { id = cost.Id }, cost); - } - catch (ArgumentException ex) - { - return BadRequest(new { error = ex.Message }); - } - } - - /// - /// Updates an audio cost configuration. - /// - /// The cost configuration ID - /// The updated configuration - /// Returns the updated cost configuration - /// If the cost configuration is not found - [HttpPut("costs/{id}")] - [ProducesResponseType(typeof(AudioCostDto), 200)] - [ProducesResponseType(404)] - public async Task UpdateCost(int id, [FromBody] UpdateAudioCostDto dto) - { - var cost = await _costService.UpdateAsync(id, dto); - if (cost == null) - return NotFound(); - - return Ok(cost); - } - - /// - /// Deletes an audio cost configuration. - /// - /// The cost configuration ID - /// If the cost configuration was deleted - /// If the cost configuration is not found - [HttpDelete("costs/{id}")] - [ProducesResponseType(204)] - [ProducesResponseType(404)] - public async Task DeleteCost(int id) - { - var deleted = await _costService.DeleteAsync(id); - if (!deleted) - return NotFound(); - - return NoContent(); - } - - #endregion - - #region Usage Analytics Endpoints - - /// - /// Gets audio usage logs with pagination and filtering. - /// - /// Query parameters for filtering and pagination - /// Returns paginated usage logs - [HttpGet("usage")] - [ProducesResponseType(typeof(PagedResult), 200)] - public async Task GetUsageLogs([FromQuery] AudioUsageQueryDto query) - { - var logs = await _usageService.GetUsageLogsAsync(query); - return Ok(logs); - } - - /// - /// Gets audio usage summary statistics. - /// - /// Start date for the summary - /// End date for the summary - /// Filter by virtual key (optional) - /// Filter by provider ID (optional) - /// Returns usage summary - [HttpGet("usage/summary")] - [ProducesResponseType(typeof(AudioUsageSummaryDto), 200)] - public async Task GetUsageSummary( - [FromQuery] DateTime startDate, - [FromQuery] DateTime endDate, - [FromQuery] string? virtualKey = null, - [FromQuery] int? providerId = null) - { - var summary = await _usageService.GetUsageSummaryAsync(startDate, endDate, virtualKey, providerId); - return Ok(summary); - } - - /// - /// Gets audio usage by virtual key. - /// - /// The virtual key - /// Start date (optional) - /// End date (optional) - /// Returns usage data for the key - [HttpGet("usage/by-key/{virtualKey}")] - [ProducesResponseType(typeof(AudioKeyUsageDto), 200)] - public async Task GetUsageByKey( - string virtualKey, - [FromQuery] DateTime? startDate = null, - [FromQuery] DateTime? endDate = null) - { - var usage = await _usageService.GetUsageByKeyAsync(virtualKey, startDate, endDate); - return Ok(usage); - } - - /// - /// Gets audio usage by provider. - /// - /// The provider ID - /// Start date (optional) - /// End date (optional) - /// Returns usage data for the provider - [HttpGet("usage/by-provider/{providerId}")] - [ProducesResponseType(typeof(AudioProviderUsageDto), 200)] - public async Task GetUsageByProvider( - int providerId, - [FromQuery] DateTime? startDate = null, - [FromQuery] DateTime? endDate = null) - { - var usage = await _usageService.GetUsageByProviderAsync(providerId, startDate, endDate); - return Ok(usage); - } - - #endregion - - #region Real-time Session Management - - /// - /// Gets real-time session metrics. - /// - /// Returns session metrics - [HttpGet("sessions/metrics")] - [ProducesResponseType(typeof(RealtimeSessionMetricsDto), 200)] - public async Task GetSessionMetrics() - { - var metrics = await _usageService.GetRealtimeSessionMetricsAsync(); - return Ok(metrics); - } - - /// - /// Gets active real-time sessions. - /// - /// Returns list of active sessions - [HttpGet("sessions")] - [ProducesResponseType(typeof(List), 200)] - public async Task GetActiveSessions() - { - var sessions = await _usageService.GetActiveSessionsAsync(); - return Ok(sessions); - } - - /// - /// Gets details of a specific real-time session. - /// - /// The session ID - /// Returns session details - /// If the session is not found - [HttpGet("sessions/{sessionId}")] - [ProducesResponseType(typeof(RealtimeSessionDto), 200)] - [ProducesResponseType(404)] - public async Task GetSessionDetails(string sessionId) - { - var session = await _usageService.GetSessionDetailsAsync(sessionId); - if (session == null) - return NotFound(); - - return Ok(session); - } - - /// - /// Terminates an active real-time session. - /// - /// The session ID - /// If the session was terminated - /// If the session is not found - [HttpDelete("sessions/{sessionId}")] - [ProducesResponseType(204)] - [ProducesResponseType(404)] - public async Task TerminateSession(string sessionId) - { - var terminated = await _usageService.TerminateSessionAsync(sessionId); - if (!terminated) - return NotFound(); - - _logger.LogInformation("Terminated real-time session {SessionId}", sessionId.Replace(Environment.NewLine, "")); - return NoContent(); - } - - #endregion - } -} diff --git a/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs b/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs index cb94b7261..ba7dbd948 100644 --- a/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs +++ b/ConduitLLM.Admin/Controllers/ModelCapabilitiesController.cs @@ -138,9 +138,6 @@ public async Task Create([FromBody] CreateCapabilitiesDto dto) MaxTokens = dto.MaxTokens, MinTokens = dto.MinTokens, SupportsVision = dto.SupportsVision, - SupportsAudioTranscription = dto.SupportsAudioTranscription, - SupportsTextToSpeech = dto.SupportsTextToSpeech, - SupportsRealtimeAudio = dto.SupportsRealtimeAudio, SupportsImageGeneration = dto.SupportsImageGeneration, SupportsVideoGeneration = dto.SupportsVideoGeneration, SupportsEmbeddings = dto.SupportsEmbeddings, @@ -148,9 +145,6 @@ public async Task Create([FromBody] CreateCapabilitiesDto dto) SupportsFunctionCalling = dto.SupportsFunctionCalling, SupportsStreaming = dto.SupportsStreaming, TokenizerType = dto.TokenizerType, - SupportedVoices = dto.SupportedVoices, - SupportedLanguages = dto.SupportedLanguages, - SupportedFormats = dto.SupportedFormats }; await _repository.CreateAsync(capabilities); @@ -205,12 +199,6 @@ public async Task Update(int id, [FromBody] UpdateCapabilitiesDto capabilities.MinTokens = dto.MinTokens.Value; if (dto.SupportsVision.HasValue) capabilities.SupportsVision = dto.SupportsVision.Value; - if (dto.SupportsAudioTranscription.HasValue) - capabilities.SupportsAudioTranscription = dto.SupportsAudioTranscription.Value; - if (dto.SupportsTextToSpeech.HasValue) - capabilities.SupportsTextToSpeech = dto.SupportsTextToSpeech.Value; - if (dto.SupportsRealtimeAudio.HasValue) - capabilities.SupportsRealtimeAudio = dto.SupportsRealtimeAudio.Value; if (dto.SupportsImageGeneration.HasValue) capabilities.SupportsImageGeneration = dto.SupportsImageGeneration.Value; if (dto.SupportsVideoGeneration.HasValue) @@ -225,12 +213,6 @@ public async Task Update(int id, [FromBody] UpdateCapabilitiesDto capabilities.SupportsStreaming = dto.SupportsStreaming.Value; if (dto.TokenizerType.HasValue) capabilities.TokenizerType = dto.TokenizerType.Value; - if (dto.SupportedVoices != null) - capabilities.SupportedVoices = dto.SupportedVoices; - if (dto.SupportedLanguages != null) - capabilities.SupportedLanguages = dto.SupportedLanguages; - if (dto.SupportedFormats != null) - capabilities.SupportedFormats = dto.SupportedFormats; await _repository.UpdateAsync(capabilities); @@ -289,9 +271,6 @@ private static CapabilitiesDto MapToDto(ModelCapabilities capabilities) MaxTokens = capabilities.MaxTokens, MinTokens = capabilities.MinTokens, SupportsVision = capabilities.SupportsVision, - SupportsAudioTranscription = capabilities.SupportsAudioTranscription, - SupportsTextToSpeech = capabilities.SupportsTextToSpeech, - SupportsRealtimeAudio = capabilities.SupportsRealtimeAudio, SupportsImageGeneration = capabilities.SupportsImageGeneration, SupportsVideoGeneration = capabilities.SupportsVideoGeneration, SupportsEmbeddings = capabilities.SupportsEmbeddings, @@ -299,9 +278,6 @@ private static CapabilitiesDto MapToDto(ModelCapabilities capabilities) SupportsFunctionCalling = capabilities.SupportsFunctionCalling, SupportsStreaming = capabilities.SupportsStreaming, TokenizerType = capabilities.TokenizerType, - SupportedVoices = capabilities.SupportedVoices, - SupportedLanguages = capabilities.SupportedLanguages, - SupportedFormats = capabilities.SupportedFormats }; } } diff --git a/ConduitLLM.Admin/Controllers/ModelController.cs b/ConduitLLM.Admin/Controllers/ModelController.cs index 3f54195f4..3a98da665 100644 --- a/ConduitLLM.Admin/Controllers/ModelController.cs +++ b/ConduitLLM.Admin/Controllers/ModelController.cs @@ -404,17 +404,11 @@ private static ModelCapabilitiesDto MapCapabilitiesToDto(ModelCapabilities capab SupportsVision = capabilities.SupportsVision, SupportsFunctionCalling = capabilities.SupportsFunctionCalling, SupportsStreaming = capabilities.SupportsStreaming, - SupportsAudioTranscription = capabilities.SupportsAudioTranscription, - SupportsTextToSpeech = capabilities.SupportsTextToSpeech, - SupportsRealtimeAudio = capabilities.SupportsRealtimeAudio, SupportsImageGeneration = capabilities.SupportsImageGeneration, SupportsVideoGeneration = capabilities.SupportsVideoGeneration, SupportsEmbeddings = capabilities.SupportsEmbeddings, MaxTokens = capabilities.MaxTokens, TokenizerType = capabilities.TokenizerType, - SupportedVoices = capabilities.SupportedVoices, - SupportedLanguages = capabilities.SupportedLanguages, - SupportedFormats = capabilities.SupportedFormats }; } } diff --git a/ConduitLLM.Admin/Controllers/ModelCostsController.cs b/ConduitLLM.Admin/Controllers/ModelCostsController.cs index 51a12193f..bef5f11e5 100644 --- a/ConduitLLM.Admin/Controllers/ModelCostsController.cs +++ b/ConduitLLM.Admin/Controllers/ModelCostsController.cs @@ -383,7 +383,7 @@ public async Task ExportJson([FromQuery] int? providerId = null) /// CSV file containing model costs /// Import result with statistics [HttpPost("import/csv")] - [ProducesResponseType(typeof(BulkImportResult), StatusCodes.Status200OK)] + // [ProducesResponseType(typeof(BulkImportResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task ImportCsv(IFormFile file) @@ -430,7 +430,7 @@ public async Task ImportCsv(IFormFile file) /// JSON file containing model costs /// Import result with statistics [HttpPost("import/json")] - [ProducesResponseType(typeof(BulkImportResult), StatusCodes.Status200OK)] + // [ProducesResponseType(typeof(BulkImportResult), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] [ProducesResponseType(StatusCodes.Status500InternalServerError)] public async Task ImportJson(IFormFile file) diff --git a/ConduitLLM.Admin/Controllers/RouterController.cs b/ConduitLLM.Admin/Controllers/RouterController.cs deleted file mode 100644 index 5ff643b9d..000000000 --- a/ConduitLLM.Admin/Controllers/RouterController.cs +++ /dev/null @@ -1,318 +0,0 @@ -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Core.Models.Routing; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Admin.Controllers; - -/// -/// Controller for managing router configurations and deployments -/// -[ApiController] -[Route("api/[controller]")] -[Authorize(Policy = "MasterKeyPolicy")] -public class RouterController : ControllerBase -{ - private readonly IAdminRouterService _routerService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the RouterController - /// - /// The router service - /// The logger - public RouterController( - IAdminRouterService routerService, - ILogger logger) - { - _routerService = routerService ?? throw new ArgumentNullException(nameof(routerService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Gets the current router configuration - /// - /// The router configuration - [HttpGet("config")] - [ProducesResponseType(typeof(RouterConfig), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetRouterConfig() - { - try - { - var config = await _routerService.GetRouterConfigAsync(); - return Ok(config); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving router configuration"); - return StatusCode(StatusCodes.Status500InternalServerError, "Error retrieving router configuration"); - } - } - - /// - /// Updates the router configuration - /// - /// The new router configuration - /// Success response - [HttpPut("config")] - [Authorize(Policy = "MasterKeyPolicy")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task UpdateRouterConfig([FromBody] RouterConfig config) - { - try - { - if (config == null) - { - return BadRequest("Router configuration cannot be null"); - } - - bool success = await _routerService.UpdateRouterConfigAsync(config); - if (success) - { - return Ok("Router configuration updated successfully"); - } - else - { - return StatusCode(StatusCodes.Status500InternalServerError, "Failed to update router configuration"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating router configuration"); - return StatusCode(StatusCodes.Status500InternalServerError, "Error updating router configuration"); - } - } - - /// - /// Gets all model deployments - /// - /// List of all model deployments - [HttpGet("deployments")] - [ProducesResponseType(typeof(List), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelDeployments() - { - try - { - var deployments = await _routerService.GetModelDeploymentsAsync(); - return Ok(deployments); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving model deployments"); - return StatusCode(StatusCodes.Status500InternalServerError, "Error retrieving model deployments"); - } - } - - /// - /// Gets a specific model deployment - /// - /// The name of the deployment - /// The model deployment - [HttpGet("deployments/{deploymentName}")] - [ProducesResponseType(typeof(ModelDeployment), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetModelDeployment(string deploymentName) - { - try - { - var deployment = await _routerService.GetModelDeploymentAsync(deploymentName); - if (deployment == null) - { - return NotFound("Deployment not found"); - } - return Ok(deployment); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error retrieving model deployment {DeploymentName}".Replace(Environment.NewLine, ""), deploymentName.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "Error retrieving model deployment"); - } - } - - /// - /// Creates or updates a model deployment - /// - /// The deployment to save - /// Success response - [HttpPost("deployments")] - [Authorize(Policy = "MasterKeyPolicy")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task CreateOrUpdateModelDeployment([FromBody] ModelDeployment deployment) - { - try - { - if (deployment == null) - { - return BadRequest("Model deployment cannot be null"); - } - - if (string.IsNullOrWhiteSpace(deployment.DeploymentName)) - { - return BadRequest("Deployment name cannot be empty"); - } - - if (string.IsNullOrWhiteSpace(deployment.ModelAlias)) - { - return BadRequest("Model alias cannot be empty"); - } - - bool success = await _routerService.SaveModelDeploymentAsync(deployment); - if (success) - { - return Ok("Model deployment saved successfully"); - } - else - { - return StatusCode(StatusCodes.Status500InternalServerError, "Failed to save model deployment"); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error saving model deployment"); - return StatusCode(StatusCodes.Status500InternalServerError, "Error saving model deployment"); - } - } - - /// - /// Deletes a model deployment - /// - /// The name of the deployment to delete - /// Success response - [HttpDelete("deployments/{deploymentName}")] - [Authorize(Policy = "MasterKeyPolicy")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task DeleteModelDeployment(string deploymentName) - { - try - { - bool success = await _routerService.DeleteModelDeploymentAsync(deploymentName); - if (success) - { - return Ok("Deployment deleted successfully"); - } - else - { - return NotFound("Deployment not found"); - } - } - catch (Exception ex) - { -_logger.LogError(ex, "Error deleting model deployment {DeploymentName}".Replace(Environment.NewLine, ""), deploymentName.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "Error deleting model deployment"); - } - } - - /// - /// Gets all fallback configurations - /// - /// Dictionary mapping primary models to their fallback models - [HttpGet("fallbacks")] - [ProducesResponseType(typeof(Dictionary>), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task GetFallbackConfigurations() - { - try - { - var fallbacks = await _routerService.GetFallbackConfigurationsAsync(); - return Ok(fallbacks); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error retrieving fallback configurations"); - return StatusCode(StatusCodes.Status500InternalServerError, "Error retrieving fallback configurations"); - } - } - - /// - /// Sets a fallback configuration - /// - /// The primary model - /// The fallback models - /// Success response - [HttpPost("fallbacks/{primaryModel}")] - [Authorize(Policy = "MasterKeyPolicy")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task SetFallbackConfiguration(string primaryModel, [FromBody] List fallbackModels) - { - try - { - if (string.IsNullOrWhiteSpace(primaryModel)) - { - return BadRequest("Primary model cannot be empty"); - } - - if (fallbackModels == null || fallbackModels.Count() == 0) - { - return BadRequest("Fallback models cannot be empty"); - } - - bool success = await _routerService.SetFallbackConfigurationAsync(primaryModel, fallbackModels); - if (success) - { - return Ok($"Fallback configuration for model '{primaryModel}' saved successfully"); - } - else - { - return StatusCode(StatusCodes.Status500InternalServerError, $"Failed to save fallback configuration for model '{primaryModel}'"); - } - } - catch (Exception ex) - { -_logger.LogError(ex, "Error setting fallback configuration for model {PrimaryModel}".Replace(Environment.NewLine, ""), primaryModel.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, $"Error setting fallback configuration for model '{primaryModel}'"); - } - } - - /// - /// Removes a fallback configuration - /// - /// The primary model - /// Success response - [HttpDelete("fallbacks/{primaryModel}")] - [Authorize(Policy = "MasterKeyPolicy")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task RemoveFallbackConfiguration(string primaryModel) - { - try - { - bool success = await _routerService.RemoveFallbackConfigurationAsync(primaryModel); - if (success) - { - return Ok("Fallback configuration removed successfully"); - } - else - { - return NotFound("Fallback configuration not found"); - } - } - catch (Exception ex) - { -_logger.LogError(ex, "Error removing fallback configuration for model {PrimaryModel}".Replace(Environment.NewLine, ""), primaryModel.Replace(Environment.NewLine, "")); - return StatusCode(StatusCodes.Status500InternalServerError, "Error removing fallback configuration"); - } - } -} diff --git a/ConduitLLM.Admin/Extensions/FallbackConfigurationRepositoryExtensions.cs b/ConduitLLM.Admin/Extensions/FallbackConfigurationRepositoryExtensions.cs deleted file mode 100644 index 9f87f14f1..000000000 --- a/ConduitLLM.Admin/Extensions/FallbackConfigurationRepositoryExtensions.cs +++ /dev/null @@ -1,77 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Extensions -{ - /// - /// Extension methods for the fallback configuration repository - /// - public static class FallbackConfigurationRepositoryExtensions - { - /// - /// Converts a FallbackConfigurationEntity to a FallbackConfiguration model - /// - /// The entity to convert - /// The fallback mappings for this configuration - /// The converted model - public static FallbackConfiguration ToModel( - this FallbackConfigurationEntity entity, - IEnumerable fallbackMappings) - { - var model = new FallbackConfiguration - { - Id = entity.Id, - PrimaryModelDeploymentId = entity.PrimaryModelDeploymentId.ToString(), - FallbackModelDeploymentIds = fallbackMappings - .OrderBy(m => m.Order) - .Select(m => m.ModelDeploymentId.ToString()) - .ToList() - }; - - return model; - } - - /// - /// Saves a FallbackConfiguration model to the repository - /// - /// The repository - /// The configuration to save - /// A task representing the asynchronous operation - public static async Task SaveAsync( - this IFallbackConfigurationRepository repository, - FallbackConfiguration config) - { - // Parse the GUID from the string - if (!Guid.TryParse(config.PrimaryModelDeploymentId, out var primaryModelGuid)) - { - throw new ArgumentException($"Invalid primary model ID: {config.PrimaryModelDeploymentId}"); - } - - // Check if a configuration for this primary model already exists - var allConfigs = await repository.GetAllAsync(); - var existingConfig = allConfigs.FirstOrDefault(c => c.PrimaryModelDeploymentId == primaryModelGuid); - - if (existingConfig == null) - { - // Create new configuration - var entity = new FallbackConfigurationEntity - { - PrimaryModelDeploymentId = primaryModelGuid, - IsActive = true, - Name = $"Fallback for {config.PrimaryModelDeploymentId}" - }; - - await repository.CreateAsync(entity); - } - else - { - // Update existing configuration - await repository.UpdateAsync(existingConfig); - } - - // Note: This is a simplified implementation - in a real application, - // you would also need to handle the fallback mappings - } - } -} diff --git a/ConduitLLM.Admin/Extensions/RepositoryExtensions.cs b/ConduitLLM.Admin/Extensions/RepositoryExtensions.cs index c5063a1c6..0b125d3c3 100644 --- a/ConduitLLM.Admin/Extensions/RepositoryExtensions.cs +++ b/ConduitLLM.Admin/Extensions/RepositoryExtensions.cs @@ -307,10 +307,6 @@ public static ModelCostDto ToDto(this ModelCost modelCost) OutputCostPerMillionTokens = modelCost.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = modelCost.EmbeddingCostPerMillionTokens, ImageCostPerImage = modelCost.ImageCostPerImage, - AudioCostPerMinute = modelCost.AudioCostPerMinute, - AudioCostPerKCharacters = modelCost.AudioCostPerKCharacters, - AudioInputCostPerMinute = modelCost.AudioInputCostPerMinute, - AudioOutputCostPerMinute = modelCost.AudioOutputCostPerMinute, VideoCostPerSecond = modelCost.VideoCostPerSecond, VideoResolutionMultipliers = modelCost.VideoResolutionMultipliers, ImageResolutionMultipliers = modelCost.ImageResolutionMultipliers, @@ -354,10 +350,6 @@ public static ModelCost ToEntity(this CreateModelCostDto dto) OutputCostPerMillionTokens = dto.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = dto.EmbeddingCostPerMillionTokens, ImageCostPerImage = dto.ImageCostPerImage, - AudioCostPerMinute = dto.AudioCostPerMinute, - AudioCostPerKCharacters = dto.AudioCostPerKCharacters, - AudioInputCostPerMinute = dto.AudioInputCostPerMinute, - AudioOutputCostPerMinute = dto.AudioOutputCostPerMinute, VideoCostPerSecond = dto.VideoCostPerSecond, VideoResolutionMultipliers = dto.VideoResolutionMultipliers, ImageResolutionMultipliers = dto.ImageResolutionMultipliers, @@ -399,10 +391,6 @@ public static ModelCost UpdateFrom(this ModelCost entity, UpdateModelCostDto dto entity.OutputCostPerMillionTokens = dto.OutputCostPerMillionTokens; entity.EmbeddingCostPerMillionTokens = dto.EmbeddingCostPerMillionTokens; entity.ImageCostPerImage = dto.ImageCostPerImage; - entity.AudioCostPerMinute = dto.AudioCostPerMinute; - entity.AudioCostPerKCharacters = dto.AudioCostPerKCharacters; - entity.AudioInputCostPerMinute = dto.AudioInputCostPerMinute; - entity.AudioOutputCostPerMinute = dto.AudioOutputCostPerMinute; entity.VideoCostPerSecond = dto.VideoCostPerSecond; entity.VideoResolutionMultipliers = dto.VideoResolutionMultipliers; entity.ImageResolutionMultipliers = dto.ImageResolutionMultipliers; diff --git a/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs index 9075e02e7..346a63990 100644 --- a/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs +++ b/ConduitLLM.Admin/Extensions/ServiceCollectionExtensions.cs @@ -100,7 +100,6 @@ public static IServiceCollection AddAdminServices(this IServiceCollection servic return new AdminModelProviderMappingService(mappingRepository, credentialRepository, modelRepository, publishEndpoint, logger); }); - services.AddScoped(); // Register Analytics services services.AddSingleton(); @@ -143,11 +142,6 @@ public static IServiceCollection AddAdminServices(this IServiceCollection servic services.AddScoped(); services.AddScoped(); - // Register audio-related services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - // Register media management service (requires IMediaLifecycleService to be registered) services.AddScoped(serviceProvider => { diff --git a/ConduitLLM.Admin/Interfaces/IAdminAudioCostService.cs b/ConduitLLM.Admin/Interfaces/IAdminAudioCostService.cs deleted file mode 100644 index 03bb3d644..000000000 --- a/ConduitLLM.Admin/Interfaces/IAdminAudioCostService.cs +++ /dev/null @@ -1,81 +0,0 @@ -using ConduitLLM.Configuration.DTOs.Audio; - -namespace ConduitLLM.Admin.Interfaces -{ - /// - /// Service interface for managing audio cost configurations. - /// - public interface IAdminAudioCostService - { - /// - /// Gets all audio cost configurations. - /// - Task> GetAllAsync(); - - /// - /// Gets an audio cost configuration by ID. - /// - Task GetByIdAsync(int id); - - /// - /// Gets audio costs by provider. - /// - Task> GetByProviderAsync(int providerId); - - /// - /// Gets the current cost for a specific provider and operation. - /// - Task GetCurrentCostAsync(int providerId, string operationType, string? model = null); - - /// - /// Gets cost history for a provider and operation. - /// - Task> GetCostHistoryAsync(int providerId, string operationType, string? model = null); - - /// - /// Creates a new audio cost configuration. - /// - Task CreateAsync(CreateAudioCostDto dto); - - /// - /// Updates an existing audio cost configuration. - /// - Task UpdateAsync(int id, UpdateAudioCostDto dto); - - /// - /// Deletes an audio cost configuration. - /// - Task DeleteAsync(int id); - - /// - /// Imports bulk audio costs from a CSV or JSON file. - /// - Task ImportCostsAsync(string data, string format); - - /// - /// Exports audio costs to CSV or JSON format. - /// - Task ExportCostsAsync(string format, int? providerId = null); - } - - /// - /// Result of bulk cost import operation. - /// - public class BulkImportResult - { - /// - /// Number of costs successfully imported. - /// - public int SuccessCount { get; set; } - - /// - /// Number of costs that failed to import. - /// - public int FailureCount { get; set; } - - /// - /// Error messages for failed imports. - /// - public List Errors { get; set; } = new(); - } -} diff --git a/ConduitLLM.Admin/Interfaces/IAdminAudioProviderService.cs b/ConduitLLM.Admin/Interfaces/IAdminAudioProviderService.cs deleted file mode 100644 index af5a176cf..000000000 --- a/ConduitLLM.Admin/Interfaces/IAdminAudioProviderService.cs +++ /dev/null @@ -1,76 +0,0 @@ -using ConduitLLM.Configuration.DTOs.Audio; - -namespace ConduitLLM.Admin.Interfaces -{ - /// - /// Service interface for managing audio provider configurations. - /// - public interface IAdminAudioProviderService - { - /// - /// Gets all audio provider configurations. - /// - Task> GetAllAsync(); - - /// - /// Gets an audio provider configuration by ID. - /// - Task GetByIdAsync(int id); - - /// - /// Gets audio provider configurations by provider ID. - /// - Task> GetByProviderAsync(int providerId); - - /// - /// Gets enabled audio provider configurations for a specific operation. - /// - Task> GetEnabledForOperationAsync(string operationType); - - /// - /// Creates a new audio provider configuration. - /// - Task CreateAsync(CreateAudioProviderConfigDto dto); - - /// - /// Updates an existing audio provider configuration. - /// - Task UpdateAsync(int id, UpdateAudioProviderConfigDto dto); - - /// - /// Deletes an audio provider configuration. - /// - Task DeleteAsync(int id); - - /// - /// Tests audio provider connectivity. - /// - Task TestProviderAsync(int id, string operationType); - } - - /// - /// Result of audio provider connectivity test. - /// - public class AudioProviderTestResult - { - /// - /// Whether the test was successful. - /// - public bool Success { get; set; } - - /// - /// Test message or error description. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Response time in milliseconds. - /// - public int? ResponseTimeMs { get; set; } - - /// - /// Provider capabilities detected. - /// - public Dictionary? Capabilities { get; set; } - } -} diff --git a/ConduitLLM.Admin/Interfaces/IAdminAudioUsageService.cs b/ConduitLLM.Admin/Interfaces/IAdminAudioUsageService.cs deleted file mode 100644 index fe195cd9d..000000000 --- a/ConduitLLM.Admin/Interfaces/IAdminAudioUsageService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; - -namespace ConduitLLM.Admin.Interfaces -{ - /// - /// Service interface for managing audio usage analytics. - /// - public interface IAdminAudioUsageService - { - /// - /// Gets paginated audio usage logs. - /// - Task> GetUsageLogsAsync(AudioUsageQueryDto query); - - /// - /// Gets audio usage summary statistics. - /// - Task GetUsageSummaryAsync(DateTime startDate, DateTime endDate, string? virtualKey = null, int? providerId = null); - - /// - /// Gets audio usage by virtual key. - /// - Task GetUsageByKeyAsync(string virtualKey, DateTime? startDate = null, DateTime? endDate = null); - - /// - /// Gets audio usage by provider. - /// - Task GetUsageByProviderAsync(int providerId, DateTime? startDate = null, DateTime? endDate = null); - - /// - /// Gets real-time session metrics. - /// - Task GetRealtimeSessionMetricsAsync(); - - /// - /// Gets active real-time sessions. - /// - Task> GetActiveSessionsAsync(); - - /// - /// Gets details of a specific real-time session. - /// - Task GetSessionDetailsAsync(string sessionId); - - /// - /// Terminates an active real-time session. - /// - Task TerminateSessionAsync(string sessionId); - - /// - /// Exports usage data to CSV or JSON format. - /// - Task ExportUsageDataAsync(AudioUsageQueryDto query, string format); - - /// - /// Cleans up old usage logs based on retention policy. - /// - Task CleanupOldLogsAsync(int retentionDays); - } -} diff --git a/ConduitLLM.Admin/Interfaces/IAdminRouterService.cs b/ConduitLLM.Admin/Interfaces/IAdminRouterService.cs deleted file mode 100644 index da44ebadc..000000000 --- a/ConduitLLM.Admin/Interfaces/IAdminRouterService.cs +++ /dev/null @@ -1,70 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -namespace ConduitLLM.Admin.Interfaces; - -/// -/// Service interface for managing routing configuration through the Admin API -/// -public interface IAdminRouterService -{ - /// - /// Gets the current router configuration - /// - /// The router configuration - Task GetRouterConfigAsync(); - - /// - /// Updates the router configuration - /// - /// The new router configuration - /// True if the update was successful - Task UpdateRouterConfigAsync(RouterConfig config); - - /// - /// Gets all model deployments - /// - /// List of all model deployments - Task> GetModelDeploymentsAsync(); - - /// - /// Gets a specific model deployment - /// - /// The name of the deployment - /// The model deployment, or null if not found - Task GetModelDeploymentAsync(string deploymentName); - - /// - /// Saves a model deployment (creates or updates) - /// - /// The deployment to save - /// True if the operation was successful - Task SaveModelDeploymentAsync(ModelDeployment deployment); - - /// - /// Deletes a model deployment - /// - /// The name of the deployment to delete - /// True if the deletion was successful - Task DeleteModelDeploymentAsync(string deploymentName); - - /// - /// Gets all fallback configurations - /// - /// Dictionary mapping primary models to their fallback models - Task>> GetFallbackConfigurationsAsync(); - - /// - /// Sets a fallback configuration - /// - /// The primary model - /// The fallback models - /// True if the operation was successful - Task SetFallbackConfigurationAsync(string primaryModel, List fallbackModels); - - /// - /// Removes a fallback configuration - /// - /// The primary model - /// True if the removal was successful - Task RemoveFallbackConfigurationAsync(string primaryModel); -} diff --git a/ConduitLLM.Admin/Models/ModelCapabilities/CapabilitiesDto.cs b/ConduitLLM.Admin/Models/ModelCapabilities/CapabilitiesDto.cs index f0e85da5b..4fbb5a5e6 100644 --- a/ConduitLLM.Admin/Models/ModelCapabilities/CapabilitiesDto.cs +++ b/ConduitLLM.Admin/Models/ModelCapabilities/CapabilitiesDto.cs @@ -69,38 +69,6 @@ public class CapabilitiesDto /// True if streaming is supported; otherwise, false. public bool SupportsStreaming { get; set; } - /// - /// Gets or sets whether the model supports audio transcription (speech-to-text). - /// - /// - /// Audio transcription models can convert spoken audio into text. - /// Examples include Whisper models and other STT (speech-to-text) services. - /// These models typically accept audio files in various formats (mp3, wav, etc.). - /// - /// True if audio transcription is supported; otherwise, false. - public bool SupportsAudioTranscription { get; set; } - - /// - /// Gets or sets whether the model supports text-to-speech synthesis. - /// - /// - /// TTS models can convert text into natural-sounding speech. - /// Examples include OpenAI TTS, ElevenLabs, and other voice synthesis services. - /// These models typically support multiple voices and languages. - /// - /// True if text-to-speech is supported; otherwise, false. - public bool SupportsTextToSpeech { get; set; } - - /// - /// Gets or sets whether the model supports real-time audio interactions. - /// - /// - /// Real-time audio support enables live, bidirectional audio conversations. - /// This is more advanced than simple TTS/STT, supporting interruptions, - /// natural conversation flow, and low-latency responses. Example: OpenAI Realtime API. - /// - /// True if real-time audio is supported; otherwise, false. - public bool SupportsRealtimeAudio { get; set; } /// /// Gets or sets whether the model supports image generation. @@ -178,40 +146,5 @@ public class CapabilitiesDto /// The tokenizer type enum value. public TokenizerType TokenizerType { get; set; } - /// - /// Gets or sets the comma-separated list of supported voice IDs for TTS models. - /// - /// - /// For text-to-speech capable models, this lists the available voice options. - /// Format: "voice1,voice2,voice3" or JSON array as string. - /// Examples: "alloy,echo,fable,onyx,nova,shimmer" for OpenAI TTS. - /// - /// Comma-separated voice IDs or JSON array string, or null if not applicable. - public string? SupportedVoices { get; set; } - - /// - /// Gets or sets the comma-separated list of supported languages. - /// - /// - /// Lists the languages the model can process or generate. - /// Format: ISO 639-1 codes like "en,es,fr,de,zh,ja" or full names. - /// Some models support 100+ languages while others are English-only. - /// Applies to chat, TTS, STT, and other language-processing capabilities. - /// - /// Comma-separated language codes or names, or null if not specified. - public string? SupportedLanguages { get; set; } - - /// - /// Gets or sets the comma-separated list of supported input/output formats. - /// - /// - /// Specifies the file formats or data formats the model can handle. - /// For audio models: "mp3,wav,ogg,flac" - /// For image models: "png,jpg,webp,gif" - /// For video models: "mp4,avi,mov,webm" - /// For chat models: "text,json,markdown" - /// - /// Comma-separated format specifications, or null if not specified. - public string? SupportedFormats { get; set; } } } \ No newline at end of file diff --git a/ConduitLLM.Admin/Models/ModelCapabilities/CreateCapabilitiesDto.cs b/ConduitLLM.Admin/Models/ModelCapabilities/CreateCapabilitiesDto.cs index 86845298f..07b74b38c 100644 --- a/ConduitLLM.Admin/Models/ModelCapabilities/CreateCapabilitiesDto.cs +++ b/ConduitLLM.Admin/Models/ModelCapabilities/CreateCapabilitiesDto.cs @@ -58,35 +58,6 @@ public class CreateCapabilitiesDto /// True if streaming is supported; otherwise, false. public bool SupportsStreaming { get; set; } - /// - /// Gets or sets whether models support audio transcription. - /// - /// - /// Set to true for speech-to-text models like Whisper that convert - /// audio files into text transcripts. - /// - /// True if audio transcription is supported; otherwise, false. - public bool SupportsAudioTranscription { get; set; } - - /// - /// Gets or sets whether models support text-to-speech synthesis. - /// - /// - /// Set to true for models that can generate natural-sounding speech from text. - /// Examples: OpenAI TTS, ElevenLabs models. - /// - /// True if TTS is supported; otherwise, false. - public bool SupportsTextToSpeech { get; set; } - - /// - /// Gets or sets whether models support real-time audio interactions. - /// - /// - /// Set to true for models supporting live, bidirectional audio conversations - /// with low latency. More advanced than simple TTS/STT. - /// - /// True if real-time audio is supported; otherwise, false. - public bool SupportsRealtimeAudio { get; set; } /// /// Gets or sets whether models support image generation. @@ -157,44 +128,5 @@ public class CreateCapabilitiesDto /// The tokenizer type enum value. public TokenizerType TokenizerType { get; set; } - /// - /// Gets or sets the comma-separated list of supported voices for TTS. - /// - /// - /// For TTS-capable models, list available voice options. - /// Format: "voice1,voice2,voice3" - /// Example: "alloy,echo,fable,onyx,nova,shimmer" - /// - /// Leave null if not applicable. - /// - /// Comma-separated voice IDs, or null. - public string? SupportedVoices { get; set; } - - /// - /// Gets or sets the comma-separated list of supported languages. - /// - /// - /// List languages the model can process using ISO 639-1 codes or names. - /// Format: "en,es,fr,de,zh,ja" or "English,Spanish,French" - /// - /// Leave null if not specified or if the model supports all common languages. - /// - /// Comma-separated language codes, or null. - public string? SupportedLanguages { get; set; } - - /// - /// Gets or sets the comma-separated list of supported formats. - /// - /// - /// Specify input/output formats the model can handle: - /// - Audio: "mp3,wav,ogg,flac" - /// - Image: "png,jpg,webp,gif" - /// - Video: "mp4,avi,mov,webm" - /// - Text: "text,json,markdown" - /// - /// Leave null if using default formats for the capability type. - /// - /// Comma-separated format specifications, or null. - public string? SupportedFormats { get; set; } } } \ No newline at end of file diff --git a/ConduitLLM.Admin/Models/ModelCapabilities/UpdateCapabilitiesDto.cs b/ConduitLLM.Admin/Models/ModelCapabilities/UpdateCapabilitiesDto.cs index bef8d6e82..3ea080438 100644 --- a/ConduitLLM.Admin/Models/ModelCapabilities/UpdateCapabilitiesDto.cs +++ b/ConduitLLM.Admin/Models/ModelCapabilities/UpdateCapabilitiesDto.cs @@ -35,20 +35,6 @@ public class UpdateCapabilitiesDto /// public bool? SupportsStreaming { get; set; } - /// - /// Gets or sets the new audio transcription support, or null to keep existing. - /// - public bool? SupportsAudioTranscription { get; set; } - - /// - /// Gets or sets the new TTS support, or null to keep existing. - /// - public bool? SupportsTextToSpeech { get; set; } - - /// - /// Gets or sets the new real-time audio support, or null to keep existing. - /// - public bool? SupportsRealtimeAudio { get; set; } /// /// Gets or sets the new image generation support, or null to keep existing. @@ -80,19 +66,5 @@ public class UpdateCapabilitiesDto /// public TokenizerType? TokenizerType { get; set; } - /// - /// Gets or sets the new supported voices list, or null to keep existing. - /// - public string? SupportedVoices { get; set; } - - /// - /// Gets or sets the new supported languages list, or null to keep existing. - /// - public string? SupportedLanguages { get; set; } - - /// - /// Gets or sets the new supported formats list, or null to keep existing. - /// - public string? SupportedFormats { get; set; } } } \ No newline at end of file diff --git a/ConduitLLM.Admin/Services/AdminAudioCostService.cs b/ConduitLLM.Admin/Services/AdminAudioCostService.cs deleted file mode 100644 index d4a3979e9..000000000 --- a/ConduitLLM.Admin/Services/AdminAudioCostService.cs +++ /dev/null @@ -1,369 +0,0 @@ -using System.Text; -using System.Text.Json; - -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Services -{ - /// - /// Service implementation for managing audio cost configurations. - /// - public class AdminAudioCostService : IAdminAudioCostService - { - private readonly IAudioCostRepository _repository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public AdminAudioCostService( - IAudioCostRepository repository, - ILogger logger) - { - _repository = repository; - _logger = logger; - } - - /// - public async Task> GetAllAsync() - { - var costs = await _repository.GetAllAsync(); - return costs.Select(MapToDto).ToList(); - } - - /// - public async Task GetByIdAsync(int id) - { - var cost = await _repository.GetByIdAsync(id); - return cost != null ? MapToDto(cost) : null; - } - - /// - public async Task> GetByProviderAsync(int providerId) - { - var costs = await _repository.GetByProviderAsync(providerId); - return costs.Select(MapToDto).ToList(); - } - - /// - public async Task GetCurrentCostAsync(int providerId, string operationType, string? model = null) - { - var cost = await _repository.GetCurrentCostAsync(providerId, operationType, model); - return cost != null ? MapToDto(cost) : null; - } - - /// - public async Task> GetCostHistoryAsync(int providerId, string operationType, string? model = null) - { - var costs = await _repository.GetCostHistoryAsync(providerId, operationType, model); - return costs.Select(MapToDto).ToList(); - } - - /// - public async Task CreateAsync(CreateAudioCostDto dto) - { - var cost = new AudioCost - { - ProviderId = dto.ProviderId, - OperationType = dto.OperationType, - Model = dto.Model, - CostUnit = dto.CostUnit, - CostPerUnit = dto.CostPerUnit, - MinimumCharge = dto.MinimumCharge, - AdditionalFactors = dto.AdditionalFactors, - IsActive = dto.IsActive, - EffectiveFrom = dto.EffectiveFrom, - EffectiveTo = dto.EffectiveTo - }; - - var created = await _repository.CreateAsync(cost); - _logger.LogInformation("Created audio cost configuration {Id} for Provider {ProviderId} {Operation}", - created.Id, - created.ProviderId, - created.OperationType.Replace(Environment.NewLine, "")); - - return MapToDto(created); - } - - /// - public async Task UpdateAsync(int id, UpdateAudioCostDto dto) - { - var cost = await _repository.GetByIdAsync(id); - if (cost == null) - { - return null; - } - - // Update properties - cost.ProviderId = dto.ProviderId; - cost.OperationType = dto.OperationType; - cost.Model = dto.Model; - cost.CostUnit = dto.CostUnit; - cost.CostPerUnit = dto.CostPerUnit; - cost.MinimumCharge = dto.MinimumCharge; - cost.AdditionalFactors = dto.AdditionalFactors; - cost.IsActive = dto.IsActive; - cost.EffectiveFrom = dto.EffectiveFrom; - cost.EffectiveTo = dto.EffectiveTo; - - var updated = await _repository.UpdateAsync(cost); - _logger.LogInformation("Updated audio cost configuration {Id}", - id); - - return MapToDto(updated); - } - - /// - public async Task DeleteAsync(int id) - { - var deleted = await _repository.DeleteAsync(id); - if (deleted) - { - _logger.LogInformation("Deleted audio cost configuration {Id}", - id); - } - return deleted; - } - - /// - public async Task ImportCostsAsync(string data, string format) - { - var result = new BulkImportResult - { - SuccessCount = 0, - FailureCount = 0, - Errors = new List() - }; - - try - { - format = format?.ToLowerInvariant() ?? "json"; - var costs = format switch - { - "json" => ParseJsonImport(data), - "csv" => ParseCsvImport(data), - _ => throw new ArgumentException($"Unsupported import format: {format}") - }; - - foreach (var cost in costs) - { - try - { - // Check if cost configuration already exists - var existing = await _repository.GetCurrentCostAsync( - cost.ProviderId, cost.OperationType, cost.Model); - - if (existing != null) - { - // Update existing - existing.CostUnit = cost.CostUnit; - existing.CostPerUnit = cost.CostPerUnit; - existing.EffectiveFrom = cost.EffectiveFrom; - existing.EffectiveTo = cost.EffectiveTo; - existing.UpdatedAt = DateTime.UtcNow; - - await _repository.UpdateAsync(existing); - } - else - { - // Create new - await _repository.CreateAsync(cost); - } - - result.SuccessCount++; - } - catch (Exception ex) - { - result.FailureCount++; - result.Errors.Add($"Failed to import cost for Provider {cost.ProviderId}/{cost.OperationType}: {ex.Message}"); - } - } - - _logger.LogInformation("Imported {Success} costs successfully, {Failed} failed", - result.SuccessCount, - result.FailureCount); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error during bulk import"); - result.Errors.Add($"Import failed: {ex.Message}"); - } - - return result; - } - - /// - public async Task ExportCostsAsync(string format, int? providerId = null) - { - List costs; - if (providerId.HasValue) - { - costs = await _repository.GetByProviderAsync(providerId.Value); - } - else - { - costs = await _repository.GetAllAsync(); - } - - format = format?.ToLowerInvariant() ?? "json"; - - return format switch - { - "json" => GenerateJsonExport(costs), - "csv" => GenerateCsvExport(costs), - _ => throw new ArgumentException($"Unsupported export format: {format}") - }; - } - - private static AudioCostDto MapToDto(AudioCost cost) - { - return new AudioCostDto - { - Id = cost.Id, - ProviderId = cost.ProviderId, - ProviderName = cost.Provider?.ProviderName, - OperationType = cost.OperationType, - Model = cost.Model, - CostUnit = cost.CostUnit, - CostPerUnit = cost.CostPerUnit, - MinimumCharge = cost.MinimumCharge, - AdditionalFactors = cost.AdditionalFactors, - IsActive = cost.IsActive, - EffectiveFrom = cost.EffectiveFrom, - EffectiveTo = cost.EffectiveTo, - CreatedAt = cost.CreatedAt, - UpdatedAt = cost.UpdatedAt - }; - } - - private List ParseJsonImport(string jsonData) - { - try - { - var importData = JsonSerializer.Deserialize>(jsonData); - if (importData == null) return new List(); - - var costs = new List(); - foreach (var d in importData) - { - costs.Add(new AudioCost - { - ProviderId = d.ProviderId, - OperationType = d.OperationType, - Model = d.Model ?? "default", - CostUnit = d.CostUnit, - CostPerUnit = d.CostPerUnit, - MinimumCharge = d.MinimumCharge, - IsActive = d.IsActive ?? true, - EffectiveFrom = d.EffectiveFrom ?? DateTime.UtcNow, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }); - } - return costs; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to parse JSON import data"); - throw new ArgumentException("Invalid JSON format", ex); - } - } - - private List ParseCsvImport(string csvData) - { - var costs = new List(); - var lines = csvData.Split('\n', StringSplitOptions.RemoveEmptyEntries); - - if (lines.Length < 2) - { - throw new ArgumentException("CSV data must contain header and at least one data row"); - } - - // Skip header - for (int i = 1; i < lines.Length; i++) - { - var parts = lines[i].Split(','); - if (parts.Length < 5) - { - _logger.LogWarning("Skipping invalid CSV line: {Line}", - lines[i].Replace(Environment.NewLine, "")); - continue; - } - - try - { - var providerIdString = parts[0].Trim(); - if (!int.TryParse(providerIdString, out var providerId)) - { - _logger.LogWarning("Invalid provider ID in CSV: {ProviderId}", providerIdString); - continue; - } - - costs.Add(new AudioCost - { - ProviderId = providerId, - OperationType = parts[1].Trim(), - Model = parts.Length > 2 ? parts[2].Trim() : "default", - CostUnit = parts[3].Trim(), - CostPerUnit = decimal.Parse(parts[4].Trim()), - MinimumCharge = parts.Length > 5 ? decimal.Parse(parts[5].Trim()) : null, - IsActive = true, - EffectiveFrom = DateTime.UtcNow, - CreatedAt = DateTime.UtcNow, - UpdatedAt = DateTime.UtcNow - }); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to parse CSV line: {Line}", - lines[i].Replace(Environment.NewLine, "")); - throw new ArgumentException($"Invalid CSV data at line {i + 1}", ex); - } - } - - return costs; - } - - private string GenerateJsonExport(List costs) - { - var exportData = costs.Select(c => new AudioCostImportDto - { - ProviderId = c.ProviderId, - ProviderName = c.Provider?.ProviderName, - OperationType = c.OperationType, - Model = c.Model, - CostUnit = c.CostUnit, - CostPerUnit = c.CostPerUnit, - MinimumCharge = c.MinimumCharge, - IsActive = c.IsActive, - EffectiveFrom = c.EffectiveFrom - }); - - return JsonSerializer.Serialize(exportData, new JsonSerializerOptions - { - WriteIndented = true - }); - } - - private string GenerateCsvExport(List costs) - { - var csv = new StringBuilder(); - csv.AppendLine("ProviderId,OperationType,Model,CostUnit,CostPerUnit,MinimumCharge"); - - foreach (var cost in costs.OrderBy(c => c.ProviderId).ThenBy(c => c.OperationType)) - { - csv.AppendLine($"{cost.ProviderId},{cost.OperationType},{cost.Model}," + - $"{cost.CostUnit},{cost.CostPerUnit},{cost.MinimumCharge ?? 0}"); - } - - return csv.ToString(); - } - } - -} diff --git a/ConduitLLM.Admin/Services/AdminAudioProviderService.cs b/ConduitLLM.Admin/Services/AdminAudioProviderService.cs deleted file mode 100644 index 31f3fd212..000000000 --- a/ConduitLLM.Admin/Services/AdminAudioProviderService.cs +++ /dev/null @@ -1,305 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Services -{ - /// - /// Service implementation for managing audio provider configurations. - /// - public class AdminAudioProviderService : IAdminAudioProviderService - { - private readonly IAudioProviderConfigRepository _repository; - private readonly IProviderRepository _credentialRepository; - private readonly ILLMClientFactory _clientFactory; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public AdminAudioProviderService( - IAudioProviderConfigRepository repository, - IProviderRepository credentialRepository, - ILLMClientFactory clientFactory, - ILogger logger) - { - _repository = repository; - _credentialRepository = credentialRepository; - _clientFactory = clientFactory; - _logger = logger; - } - - /// - public async Task> GetAllAsync() - { - var configs = await _repository.GetAllAsync(); - return configs.Select(MapToDto).ToList(); - } - - /// - public async Task GetByIdAsync(int id) - { - var config = await _repository.GetByIdAsync(id); - return config != null ? MapToDto(config) : null; - } - - /// - public async Task> GetByProviderAsync(int providerId) - { - var config = await _repository.GetByProviderIdAsync(providerId); - return config != null ? new List { MapToDto(config) } : new List(); - } - - /// - public async Task> GetEnabledForOperationAsync(string operationType) - { - var configs = await _repository.GetEnabledForOperationAsync(operationType); - return configs.Select(MapToDto).ToList(); - } - - /// - public async Task CreateAsync(CreateAudioProviderConfigDto dto) - { - // Validate that the provider credential exists - var credential = await _credentialRepository.GetByIdAsync(dto.ProviderId); - if (credential == null) - { - throw new ArgumentException($"Provider credential with ID {dto.ProviderId} not found"); - } - - // Check if configuration already exists for this credential - if (await _repository.ExistsForProviderAsync(dto.ProviderId)) - { - throw new ArgumentException($"Audio configuration already exists for provider credential {dto.ProviderId}"); - } - - var config = new AudioProviderConfig - { - ProviderId = dto.ProviderId, - TranscriptionEnabled = dto.TranscriptionEnabled, - DefaultTranscriptionModel = dto.DefaultTranscriptionModel, - TextToSpeechEnabled = dto.TextToSpeechEnabled, - DefaultTTSModel = dto.DefaultTTSModel, - DefaultTTSVoice = dto.DefaultTTSVoice, - RealtimeEnabled = dto.RealtimeEnabled, - DefaultRealtimeModel = dto.DefaultRealtimeModel, - RealtimeEndpoint = dto.RealtimeEndpoint, - CustomSettings = dto.CustomSettings, - RoutingPriority = dto.RoutingPriority - }; - - var created = await _repository.CreateAsync(config); - _logger.LogInformation("Created audio provider configuration {Id} for provider ID {ProviderId}", - created.Id, dto.ProviderId); - - return MapToDto(created); - } - - /// - public async Task UpdateAsync(int id, UpdateAudioProviderConfigDto dto) - { - var config = await _repository.GetByIdAsync(id); - if (config == null) - { - return null; - } - - // Update properties - config.TranscriptionEnabled = dto.TranscriptionEnabled; - config.DefaultTranscriptionModel = dto.DefaultTranscriptionModel; - config.TextToSpeechEnabled = dto.TextToSpeechEnabled; - config.DefaultTTSModel = dto.DefaultTTSModel; - config.DefaultTTSVoice = dto.DefaultTTSVoice; - config.RealtimeEnabled = dto.RealtimeEnabled; - config.DefaultRealtimeModel = dto.DefaultRealtimeModel; - config.RealtimeEndpoint = dto.RealtimeEndpoint; - config.CustomSettings = dto.CustomSettings; - config.RoutingPriority = dto.RoutingPriority; - - var updated = await _repository.UpdateAsync(config); - _logger.LogInformation("Updated audio provider configuration {Id}", - id); - - return MapToDto(updated); - } - - /// - public async Task DeleteAsync(int id) - { - var deleted = await _repository.DeleteAsync(id); - if (deleted) - { - _logger.LogInformation("Deleted audio provider configuration {Id}", - id); - } - return deleted; - } - - /// - public async Task TestProviderAsync(int id, string operationType) - { - var config = await _repository.GetByIdAsync(id); - if (config == null) - { - throw new KeyNotFoundException($"Audio provider configuration {id} not found"); - } - - var result = new AudioProviderTestResult - { - Capabilities = new Dictionary() - }; - - try - { - var stopwatch = Stopwatch.StartNew(); - - // Use the actual provider ID from the config - var providerId = config.ProviderId; - - // Create a client for the provider - var client = _clientFactory.GetClientByProviderId(providerId); - - // Test based on operation type - switch (operationType.ToLower()) - { - case "transcription": - if (client is IAudioTranscriptionClient transcriptionClient) - { - try - { - var supported = await transcriptionClient.SupportsTranscriptionAsync(); - result.Capabilities["transcription"] = supported; - if (supported) - { - var formats = await transcriptionClient.GetSupportedFormatsAsync(); - result.Success = true; - result.Message = $"Provider supports transcription with {formats.Count} audio formats"; - } - else - { - result.Success = false; - result.Message = "Provider reports transcription is not supported"; - } - } - catch (Exception ex) - { - result.Success = false; - result.Message = $"Failed to test transcription: {ex.Message}"; - } - } - else - { - result.Success = false; - result.Message = "Provider does not support transcription"; - } - break; - - case "tts": - case "texttospeech": - if (client is ITextToSpeechClient ttsClient) - { - try - { - var voices = await ttsClient.ListVoicesAsync(); - result.Capabilities["tts"] = voices.Count() > 0; - result.Success = true; - result.Message = $"Provider supports {voices.Count} TTS voices"; - } - catch - { - result.Success = false; - result.Message = "Failed to retrieve TTS voices"; - } - } - else - { - result.Success = false; - result.Message = "Provider does not support text-to-speech"; - } - break; - - case "realtime": - if (client is IRealtimeAudioClient realtimeClient) - { - try - { - var capabilities = await realtimeClient.GetCapabilitiesAsync(); - result.Capabilities["realtime"] = true; - result.Capabilities["interruptions"] = capabilities.SupportsInterruptions; - result.Capabilities["functions"] = capabilities.SupportsFunctionCalling; - result.Success = true; - result.Message = "Provider supports real-time audio"; - } - catch - { - result.Success = false; - result.Message = "Failed to retrieve real-time capabilities"; - } - } - else - { - result.Success = false; - result.Message = "Provider does not support real-time audio"; - } - break; - - default: - result.Success = false; - result.Message = $"Unknown operation type: {operationType}"; - break; - } - - stopwatch.Stop(); - result.ResponseTimeMs = (int)stopwatch.ElapsedMilliseconds; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error testing audio provider {Id} for operation {Operation}", - id, - operationType.Replace(Environment.NewLine, "")); - result.Success = false; - result.Message = $"Test failed: {ex.Message}"; - } - - return result; - } - - private static AudioProviderConfigDto MapToDto(AudioProviderConfig config) - { - return new AudioProviderConfigDto - { - Id = config.Id, - ProviderId = config.ProviderId, - ProviderType = config.Provider?.ProviderType, - TranscriptionEnabled = config.TranscriptionEnabled, - DefaultTranscriptionModel = config.DefaultTranscriptionModel, - TextToSpeechEnabled = config.TextToSpeechEnabled, - DefaultTTSModel = config.DefaultTTSModel, - DefaultTTSVoice = config.DefaultTTSVoice, - RealtimeEnabled = config.RealtimeEnabled, - DefaultRealtimeModel = config.DefaultRealtimeModel, - RealtimeEndpoint = config.RealtimeEndpoint, - CustomSettings = config.CustomSettings, - RoutingPriority = config.RoutingPriority, - CreatedAt = config.CreatedAt, - UpdatedAt = config.UpdatedAt - }; - } - - private static string? GetDefaultModelForOperation(AudioProviderConfig config, string operationType) - { - return operationType.ToLower() switch - { - "transcription" => config.DefaultTranscriptionModel, - "tts" or "texttospeech" => config.DefaultTTSModel, - "realtime" => config.DefaultRealtimeModel, - _ => null - }; - } - } -} diff --git a/ConduitLLM.Admin/Services/AdminAudioUsageService.cs b/ConduitLLM.Admin/Services/AdminAudioUsageService.cs deleted file mode 100644 index f6066ce72..000000000 --- a/ConduitLLM.Admin/Services/AdminAudioUsageService.cs +++ /dev/null @@ -1,454 +0,0 @@ -using System.Globalization; - -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using CsvHelper; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Services -{ - /// - /// Service implementation for managing audio usage analytics. - /// - public class AdminAudioUsageService : IAdminAudioUsageService - { - private readonly IAudioUsageLogRepository _repository; - private readonly IVirtualKeyRepository _virtualKeyRepository; - private readonly ILogger _logger; - private readonly IServiceProvider _serviceProvider; - private readonly ConduitLLM.Core.Interfaces.ICostCalculationService _costCalculationService; - - /// - /// Initializes a new instance of the class. - /// - public AdminAudioUsageService( - IAudioUsageLogRepository repository, - IVirtualKeyRepository virtualKeyRepository, - ILogger logger, - IServiceProvider serviceProvider, - ConduitLLM.Core.Interfaces.ICostCalculationService costCalculationService) - { - _repository = repository; - _virtualKeyRepository = virtualKeyRepository; - _logger = logger; - _serviceProvider = serviceProvider; - _costCalculationService = costCalculationService; - } - - /// - public async Task> GetUsageLogsAsync(AudioUsageQueryDto query) - { - var pagedResult = await _repository.GetPagedAsync(query); - - return new PagedResult - { - Items = pagedResult.Items.Select(MapToDto).ToList(), - TotalCount = pagedResult.TotalCount, - Page = pagedResult.Page, - PageSize = pagedResult.PageSize, - TotalPages = pagedResult.TotalPages - }; - } - - /// - public async Task GetUsageSummaryAsync(DateTime startDate, DateTime endDate, string? virtualKey = null, int? providerId = null) - { - return await _repository.GetUsageSummaryAsync(startDate, endDate, virtualKey, providerId); - } - - /// - public async Task GetUsageByKeyAsync(string virtualKey, DateTime? startDate = null, DateTime? endDate = null) - { - var logs = await _repository.GetByVirtualKeyAsync(virtualKey, startDate, endDate); - var key = await _virtualKeyRepository.GetByKeyHashAsync(virtualKey); - - var effectiveStartDate = startDate ?? DateTime.UtcNow.AddDays(-30); - var effectiveEndDate = endDate ?? DateTime.UtcNow; - - var operationBreakdown = await _repository.GetOperationBreakdownAsync(effectiveStartDate, effectiveEndDate, virtualKey); - var providerBreakdown = await _repository.GetProviderBreakdownAsync(effectiveStartDate, effectiveEndDate, virtualKey); - - return new AudioKeyUsageDto - { - VirtualKey = virtualKey, - KeyName = key?.KeyName ?? string.Empty, - TotalOperations = logs.Count(), - TotalCost = logs.Sum(l => l.Cost), - TotalDurationSeconds = logs.Where(l => l.DurationSeconds.HasValue).Sum(l => l.DurationSeconds!.Value), - LastUsed = logs.OrderByDescending(l => l.Timestamp).FirstOrDefault()?.Timestamp, - SuccessRate = logs.Count() > 0 ? (logs.Count(l => l.StatusCode == null || (l.StatusCode >= 200 && l.StatusCode < 300)) / (double)logs.Count()) * 100 : 100 - }; - } - - /// - public async Task GetUsageByProviderAsync(int providerId, DateTime? startDate = null, DateTime? endDate = null) - { - var logs = await _repository.GetByProviderAsync(providerId, startDate, endDate); - - var successCount = logs.Count(l => l.StatusCode == null || (l.StatusCode >= 200 && l.StatusCode < 300)); - var totalDuration = logs.Where(l => l.DurationSeconds.HasValue).Sum(l => l.DurationSeconds!.Value); - var avgResponseTime = logs.Count() > 0 ? (totalDuration / logs.Count()) * 1000 : 0; // Convert to ms - - // Count operations by type - var transcriptionCount = logs.Count(l => l.OperationType?.ToLower() == "transcription"); - var ttsCount = logs.Count(l => l.OperationType?.ToLower() == "tts" || l.OperationType?.ToLower() == "text-to-speech"); - var realtimeCount = logs.Count(l => l.OperationType?.ToLower() == "realtime"); - - // Find most used model - var mostUsedModel = logs - .Where(l => !string.IsNullOrEmpty(l.Model)) - .GroupBy(l => l.Model) - .OrderByDescending(g => g.Count()) - .FirstOrDefault()?.Key; - - // Get provider name from first log or use provider ID - var providerName = logs.FirstOrDefault()?.Provider?.ProviderName ?? $"Provider {providerId}"; - - return new AudioProviderUsageDto - { - ProviderId = providerId, - ProviderName = providerName, - TotalOperations = logs.Count, - TranscriptionCount = transcriptionCount, - TextToSpeechCount = ttsCount, - RealtimeSessionCount = realtimeCount, - TotalCost = logs.Sum(l => l.Cost), - AverageResponseTime = avgResponseTime, - SuccessRate = logs.Count() > 0 ? (successCount / (double)logs.Count) * 100 : 0, - MostUsedModel = mostUsedModel - }; - } - - /// - public async Task GetRealtimeSessionMetricsAsync() - { - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - - if (sessionStore == null) - { - _logger.LogWarning("Real-time session store not available"); - return new RealtimeSessionMetricsDto - { - ActiveSessions = 0, - SessionsByProvider = new Dictionary(), - AverageSessionDuration = 0, - TotalSessionTimeToday = 0, - TotalCostToday = 0, - PeakConcurrentSessions = 0, - SuccessRate = 100, - AverageTurnsPerSession = 0 - }; - } - - var sessions = await sessionStore.GetActiveSessionsAsync(); - var todaySessions = sessions.Where(s => s.CreatedAt.Date == DateTime.UtcNow.Date).ToList(); - - // Calculate metrics - var sessionsByProvider = sessions - .GroupBy(s => s.Provider) - .ToDictionary(g => g.Key, g => g.Count()); - - var averageDuration = sessions.Count() > 0 - ? sessions.Average(s => s.Statistics.Duration.TotalMinutes) - : 0; - - var totalSessionTimeToday = todaySessions - .Sum(s => s.Statistics.Duration.TotalMinutes); - - var successfulSessions = sessions.Count(s => s.Statistics.ErrorCount == 0); - var successRate = sessions.Count() > 0 - ? (successfulSessions / (double)sessions.Count) * 100 - : 100; - - var averageTurns = sessions.Count() > 0 - ? sessions.Average(s => s.Statistics.TurnCount) - : 0; - - // Calculate cost using actual model costs from database - var totalCostToday = await CalculateTotalSessionsCostAsync(todaySessions); - - return new RealtimeSessionMetricsDto - { - ActiveSessions = sessions.Count, - SessionsByProvider = sessionsByProvider, - AverageSessionDuration = averageDuration, - TotalSessionTimeToday = totalSessionTimeToday, - TotalCostToday = (decimal)totalCostToday, - PeakConcurrentSessions = sessions.Count, // Would need historical tracking - SuccessRate = successRate, - AverageTurnsPerSession = averageTurns - }; - } - - /// - public async Task> GetActiveSessionsAsync() - { - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - - if (sessionStore == null) - { - _logger.LogWarning("Real-time session store not available"); - return new List(); - } - - var sessions = await sessionStore.GetActiveSessionsAsync(); - - var mappedSessions = new List(); - foreach (var session in sessions) - { - mappedSessions.Add(await MapSessionToDtoAsync(session)); - } - return mappedSessions; - } - - /// - public async Task GetSessionDetailsAsync(string sessionId) - { - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - - if (sessionStore == null) - { - _logger.LogWarning("Real-time session store not available"); - return null; - } - - var session = await sessionStore.GetSessionAsync(sessionId); - - return session != null ? await MapSessionToDtoAsync(session) : null; - } - - /// - public async Task TerminateSessionAsync(string sessionId) - { - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - - if (sessionStore == null) - { - _logger.LogWarning("Real-time session store not available"); - return false; - } - - var session = await sessionStore.GetSessionAsync(sessionId); - if (session == null) - { - _logger.LogWarning("Session not found for termination {SessionId}", sessionId.Replace(Environment.NewLine, "")); - return false; - } - - // Update session state to closed - session.State = SessionState.Closed; - session.Statistics.Duration = DateTime.UtcNow - session.CreatedAt; - - await sessionStore.UpdateSessionAsync(session); - - // Remove from active sessions - var removed = await sessionStore.RemoveSessionAsync(sessionId); - - if (removed) - { - _logger.LogInformation("Successfully terminated session {SessionId}", sessionId.Replace(Environment.NewLine, "")); - } - - return removed; - } - - /// - public async Task ExportUsageDataAsync(AudioUsageQueryDto query, string format) - { - // Get all logs without pagination for export - query.Page = 1; - query.PageSize = int.MaxValue; - var result = await _repository.GetPagedAsync(query); - var logs = result.Items; - - format = format?.ToLowerInvariant() ?? "csv"; - - return format switch - { - "csv" => await GenerateCsvExport(logs), - "json" => await GenerateJsonExport(logs), - _ => throw new ArgumentException("Unsupported export format", nameof(format)) - }; - } - - /// - public async Task CleanupOldLogsAsync(int retentionDays) - { - var cutoffDate = DateTime.UtcNow.AddDays(-retentionDays); - var deletedCount = await _repository.DeleteOldLogsAsync(cutoffDate); - - _logger.LogInformation("Cleaned up {Count} audio usage logs older than {Date}", - deletedCount, cutoffDate); - - return deletedCount; - } - - private static AudioUsageDto MapToDto(Configuration.Entities.AudioUsageLog log) - { - return new AudioUsageDto - { - Id = log.Id, - VirtualKey = log.VirtualKey, - ProviderId = log.ProviderId, - OperationType = log.OperationType, - Model = log.Model, - RequestId = log.RequestId, - SessionId = log.SessionId, - DurationSeconds = log.DurationSeconds, - CharacterCount = log.CharacterCount, - InputTokens = log.InputTokens, - OutputTokens = log.OutputTokens, - Cost = log.Cost, - Language = log.Language, - Voice = log.Voice, - StatusCode = log.StatusCode, - ErrorMessage = log.ErrorMessage, - IpAddress = log.IpAddress, - UserAgent = log.UserAgent, - Timestamp = log.Timestamp - }; - } - - private async Task MapSessionToDtoAsync(RealtimeSession session) - { - // Try to get ProviderId from metadata - var providerId = 0; - if (session.Metadata?.TryGetValue("ProviderId", out var idValue) == true && idValue != null) - { - int.TryParse(idValue.ToString(), out providerId); - } - - return new RealtimeSessionDto - { - SessionId = session.Id, - VirtualKey = session.Metadata?.GetValueOrDefault("VirtualKey")?.ToString() ?? "unknown", - ProviderId = providerId, - ProviderName = session.Provider, - State = session.State.ToString(), - CreatedAt = session.CreatedAt, - DurationSeconds = session.Statistics.Duration.TotalSeconds, - TurnCount = session.Statistics.TurnCount, - InputTokens = session.Statistics.InputTokens ?? 0, - OutputTokens = session.Statistics.OutputTokens ?? 0, - EstimatedCost = (decimal)await CalculateSessionCostAsync(session), - IpAddress = session.Metadata?.GetValueOrDefault("IpAddress")?.ToString(), - UserAgent = session.Metadata?.GetValueOrDefault("UserAgent")?.ToString(), - Model = session.Config?.Model, - Voice = session.Config?.Voice, - Language = session.Config?.Language - }; - } - - private async Task CalculateSessionCostAsync(RealtimeSession session) - { - // TODO: ICostCalculationService needs to be enhanced to support separate input/output audio durations - // For now, we'll use total audio duration and log a warning about the limitation - var totalAudioSeconds = (decimal)(session.Statistics.InputAudioDuration.TotalSeconds + - session.Statistics.OutputAudioDuration.TotalSeconds); - - if (string.IsNullOrEmpty(session.Config?.Model)) - { - _logger.LogWarning("No model specified for realtime session {SessionId}, cannot calculate cost", - session.Id); - return 0; - } - - var usage = new ConduitLLM.Core.Models.Usage - { - AudioDurationSeconds = totalAudioSeconds - }; - - try - { - var cost = await _costCalculationService.CalculateCostAsync(session.Config.Model, usage); - return (double)cost; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to calculate cost for session {SessionId} with model {Model}", - session.Id, session.Config.Model); - return 0; - } - } - - private async Task CalculateTotalSessionsCostAsync(IEnumerable sessions) - { - var totalCost = 0.0; - foreach (var session in sessions) - { - totalCost += await CalculateSessionCostAsync(session); - } - return totalCost; - } - - private async Task GenerateCsvExport(List logs) - { - using var stringWriter = new StringWriter(); - using var csv = new CsvWriter(stringWriter, CultureInfo.InvariantCulture); - - // Write header - csv.WriteField("Timestamp"); - csv.WriteField("VirtualKey"); - csv.WriteField("ProviderId"); - csv.WriteField("Operation"); - csv.WriteField("Model"); - csv.WriteField("Duration"); - csv.WriteField("Cost"); - csv.WriteField("Status"); - csv.WriteField("Language"); - csv.WriteField("Voice"); - await csv.NextRecordAsync(); - - // Write data - foreach (var log in logs.OrderBy(l => l.Timestamp)) - { - csv.WriteField(log.Timestamp.ToString("yyyy-MM-dd HH:mm:ss")); - csv.WriteField(log.VirtualKey); - csv.WriteField(log.ProviderId); - csv.WriteField(log.OperationType); - csv.WriteField(log.Model); - csv.WriteField(log.DurationSeconds); - csv.WriteField(log.Cost.ToString("F4")); - csv.WriteField(log.StatusCode); - csv.WriteField(log.Language ?? "N/A"); - csv.WriteField(log.Voice ?? "N/A"); - await csv.NextRecordAsync(); - } - - await csv.FlushAsync(); - return stringWriter.ToString(); - } - - private async Task GenerateJsonExport(List logs) - { - var exportData = logs.OrderBy(l => l.Timestamp).Select(l => new - { - timestamp = l.Timestamp, - virtualKey = l.VirtualKey, - providerId = l.ProviderId, - providerName = l.Provider?.ProviderName, - operation = l.OperationType, - model = l.Model, - duration = l.DurationSeconds, - cost = l.Cost, - status = l.StatusCode, - language = l.Language, - voice = l.Voice, - error = l.ErrorMessage - }); - - return await Task.FromResult(System.Text.Json.JsonSerializer.Serialize(exportData, new System.Text.Json.JsonSerializerOptions - { - WriteIndented = true - })); - } - } -} diff --git a/ConduitLLM.Admin/Services/AdminModelCostService.ImportExport.cs b/ConduitLLM.Admin/Services/AdminModelCostService.ImportExport.cs index 4203bfe21..289797774 100644 --- a/ConduitLLM.Admin/Services/AdminModelCostService.ImportExport.cs +++ b/ConduitLLM.Admin/Services/AdminModelCostService.ImportExport.cs @@ -49,10 +49,6 @@ public async Task ImportModelCostsAsync(IEnumerable mod OutputCostPerMillionTokens = modelCost.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = modelCost.EmbeddingCostPerMillionTokens, ImageCostPerImage = modelCost.ImageCostPerImage, - AudioCostPerMinute = modelCost.AudioCostPerMinute, - AudioCostPerKCharacters = modelCost.AudioCostPerKCharacters, - AudioInputCostPerMinute = modelCost.AudioInputCostPerMinute, - AudioOutputCostPerMinute = modelCost.AudioOutputCostPerMinute, VideoCostPerSecond = modelCost.VideoCostPerSecond, VideoResolutionMultipliers = modelCost.VideoResolutionMultipliers, ImageResolutionMultipliers = modelCost.ImageResolutionMultipliers, @@ -183,10 +179,6 @@ public async Task ImportModelCostsAsync(string data, string fo OutputCostPerMillionTokens = modelCost.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = modelCost.EmbeddingCostPerMillionTokens, ImageCostPerImage = modelCost.ImageCostPerImage, - AudioCostPerMinute = modelCost.AudioCostPerMinute, - AudioCostPerKCharacters = modelCost.AudioCostPerKCharacters, - AudioInputCostPerMinute = modelCost.AudioInputCostPerMinute, - AudioOutputCostPerMinute = modelCost.AudioOutputCostPerMinute, VideoCostPerSecond = modelCost.VideoCostPerSecond, VideoResolutionMultipliers = modelCost.VideoResolutionMultipliers, ImageResolutionMultipliers = modelCost.ImageResolutionMultipliers, diff --git a/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs b/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs index 158670eca..6948a2eba 100644 --- a/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs +++ b/ConduitLLM.Admin/Services/AdminModelCostService.Parsers.cs @@ -23,10 +23,6 @@ private string GenerateJsonExport(List modelCosts) OutputCostPerMillionTokens = mc.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = mc.EmbeddingCostPerMillionTokens, ImageCostPerImage = mc.ImageCostPerImage, - AudioCostPerMinute = mc.AudioCostPerMinute, - AudioCostPerKCharacters = mc.AudioCostPerKCharacters, - AudioInputCostPerMinute = mc.AudioInputCostPerMinute, - AudioOutputCostPerMinute = mc.AudioOutputCostPerMinute, VideoCostPerSecond = mc.VideoCostPerSecond, VideoResolutionMultipliers = mc.VideoResolutionMultipliers, ImageResolutionMultipliers = mc.ImageResolutionMultipliers, @@ -46,7 +42,7 @@ private string GenerateJsonExport(List modelCosts) private string GenerateCsvExport(List modelCosts) { var csv = new StringBuilder(); - csv.AppendLine("Cost Name,Pricing Model,Pricing Configuration,Input Cost (per million tokens),Output Cost (per million tokens),Embedding Cost (per million tokens),Image Cost (per image),Audio Cost (per minute),Audio Cost (per 1K chars),Audio Input Cost (per minute),Audio Output Cost (per minute),Video Cost (per second),Video Resolution Multipliers,Image Resolution Multipliers,Batch Processing Multiplier,Supports Batch Processing,Search Unit Cost (per 1K units),Inference Step Cost,Default Inference Steps"); + csv.AppendLine("Cost Name,Pricing Model,Pricing Configuration,Input Cost (per million tokens),Output Cost (per million tokens),Embedding Cost (per million tokens),Image Cost (per image),Video Cost (per second),Video Resolution Multipliers,Image Resolution Multipliers,Batch Processing Multiplier,Supports Batch Processing,Search Unit Cost (per 1K units),Inference Step Cost,Default Inference Steps"); foreach (var modelCost in modelCosts.OrderBy(mc => mc.CostName)) { @@ -57,10 +53,6 @@ private string GenerateCsvExport(List modelCosts) $"{modelCost.OutputCostPerMillionTokens:F6}," + $"{(modelCost.EmbeddingCostPerMillionTokens.HasValue ? modelCost.EmbeddingCostPerMillionTokens.Value.ToString("F6") : "")}," + $"{(modelCost.ImageCostPerImage?.ToString("F4") ?? "")}," + - $"{(modelCost.AudioCostPerMinute?.ToString("F4") ?? "")}," + - $"{(modelCost.AudioCostPerKCharacters?.ToString("F4") ?? "")}," + - $"{(modelCost.AudioInputCostPerMinute?.ToString("F4") ?? "")}," + - $"{(modelCost.AudioOutputCostPerMinute?.ToString("F4") ?? "")}," + $"{(modelCost.VideoCostPerSecond?.ToString("F4") ?? "")}," + $"{EscapeCsvValue(modelCost.VideoResolutionMultipliers ?? "")}," + $"{EscapeCsvValue(modelCost.ImageResolutionMultipliers ?? "")}," + @@ -90,10 +82,6 @@ private List ParseJsonImport(string jsonData) OutputCostPerMillionTokens = d.OutputCostPerMillionTokens, EmbeddingCostPerMillionTokens = d.EmbeddingCostPerMillionTokens, ImageCostPerImage = d.ImageCostPerImage, - AudioCostPerMinute = d.AudioCostPerMinute, - AudioCostPerKCharacters = d.AudioCostPerKCharacters, - AudioInputCostPerMinute = d.AudioInputCostPerMinute, - AudioOutputCostPerMinute = d.AudioOutputCostPerMinute, VideoCostPerSecond = d.VideoCostPerSecond, VideoResolutionMultipliers = d.VideoResolutionMultipliers, ImageResolutionMultipliers = d.ImageResolutionMultipliers, @@ -142,18 +130,14 @@ private List ParseCsvImport(string csvData) OutputCostPerMillionTokens = parts.Length > 4 && decimal.TryParse(parts[4], out var outputCost) ? outputCost : 0, EmbeddingCostPerMillionTokens = parts.Length > 5 && decimal.TryParse(parts[5], out var embeddingCost) ? embeddingCost : null, ImageCostPerImage = parts.Length > 6 && decimal.TryParse(parts[6], out var imageCost) ? imageCost : null, - AudioCostPerMinute = parts.Length > 7 && decimal.TryParse(parts[7], out var audioCost) ? audioCost : null, - AudioCostPerKCharacters = parts.Length > 8 && decimal.TryParse(parts[8], out var audioKCharCost) ? audioKCharCost : null, - AudioInputCostPerMinute = parts.Length > 9 && decimal.TryParse(parts[9], out var audioInputCost) ? audioInputCost : null, - AudioOutputCostPerMinute = parts.Length > 10 && decimal.TryParse(parts[10], out var audioOutputCost) ? audioOutputCost : null, - VideoCostPerSecond = parts.Length > 11 && decimal.TryParse(parts[11], out var videoCost) ? videoCost : null, - VideoResolutionMultipliers = parts.Length > 12 ? UnescapeCsvValue(parts[12]) : null, - ImageResolutionMultipliers = parts.Length > 13 ? UnescapeCsvValue(parts[13]) : null, - BatchProcessingMultiplier = parts.Length > 14 && decimal.TryParse(parts[14], out var batchMultiplier) ? batchMultiplier : null, - SupportsBatchProcessing = parts.Length > 15 && (parts[15].Trim().ToLower() == "yes" || parts[15].Trim().ToLower() == "true"), - CostPerSearchUnit = parts.Length > 16 && decimal.TryParse(parts[16], out var searchUnitCost) ? searchUnitCost : null, - CostPerInferenceStep = parts.Length > 17 && decimal.TryParse(parts[17], out var inferenceStepCost) ? inferenceStepCost : null, - DefaultInferenceSteps = parts.Length > 18 && int.TryParse(parts[18], out var defaultSteps) ? defaultSteps : null + VideoCostPerSecond = parts.Length > 7 && decimal.TryParse(parts[7], out var videoCost) ? videoCost : null, + VideoResolutionMultipliers = parts.Length > 8 ? UnescapeCsvValue(parts[8]) : null, + ImageResolutionMultipliers = parts.Length > 9 ? UnescapeCsvValue(parts[9]) : null, + BatchProcessingMultiplier = parts.Length > 10 && decimal.TryParse(parts[10], out var batchMultiplier) ? batchMultiplier : null, + SupportsBatchProcessing = parts.Length > 11 && (parts[11].Trim().ToLower() == "yes" || parts[11].Trim().ToLower() == "true"), + CostPerSearchUnit = parts.Length > 12 && decimal.TryParse(parts[12], out var searchUnitCost) ? searchUnitCost : null, + CostPerInferenceStep = parts.Length > 13 && decimal.TryParse(parts[13], out var inferenceStepCost) ? inferenceStepCost : null, + DefaultInferenceSteps = parts.Length > 14 && int.TryParse(parts[14], out var defaultSteps) ? defaultSteps : null }; modelCosts.Add(modelCost); diff --git a/ConduitLLM.Admin/Services/AdminRouterService.cs b/ConduitLLM.Admin/Services/AdminRouterService.cs deleted file mode 100644 index a7b377b3b..000000000 --- a/ConduitLLM.Admin/Services/AdminRouterService.cs +++ /dev/null @@ -1,201 +0,0 @@ -using ConduitLLM.Admin.Extensions; -using ConduitLLM.Admin.Interfaces; -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Admin.Services; - -/// -/// Service for managing router configuration through the Admin API -/// -public class AdminRouterService : IAdminRouterService -{ - private readonly IRouterConfigRepository _routerConfigRepository; - private readonly IModelDeploymentRepository _modelDeploymentRepository; - private readonly IFallbackConfigurationRepository _fallbackConfigRepository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the AdminRouterService class - /// - /// The router configuration repository - /// The model deployment repository - /// The fallback configuration repository - /// The logger - public AdminRouterService( - IRouterConfigRepository routerConfigRepository, - IModelDeploymentRepository modelDeploymentRepository, - IFallbackConfigurationRepository fallbackConfigRepository, - ILogger logger) - { - _routerConfigRepository = routerConfigRepository ?? throw new ArgumentNullException(nameof(routerConfigRepository)); - _modelDeploymentRepository = modelDeploymentRepository ?? throw new ArgumentNullException(nameof(modelDeploymentRepository)); - _fallbackConfigRepository = fallbackConfigRepository ?? throw new ArgumentNullException(nameof(fallbackConfigRepository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public Task GetRouterConfigAsync() - { - _logger.LogInformation("Getting router configuration"); - // For now, we'll just return an empty config since the implementation is incomplete - return Task.FromResult(new RouterConfig()); - } - - /// - public Task UpdateRouterConfigAsync(RouterConfig config) - { - try - { - _logger.LogInformation("Updating router configuration"); - - if (config == null) - { - _logger.LogWarning("Router configuration is null"); - return Task.FromResult(false); - } - - // Implementation would normally call _routerConfigRepository.SaveConfigAsync(config) - // but we'll leave this as a stub for now - return Task.FromResult(true); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating router configuration"); - return Task.FromResult(false); - } - } - - /// - public Task> GetModelDeploymentsAsync() - { - _logger.LogInformation("Getting all model deployments"); - // Return empty list for now - return Task.FromResult(new List()); - } - - /// - public Task GetModelDeploymentAsync(string deploymentName) - { -_logger.LogInformation("Getting model deployment: {DeploymentName}", deploymentName.Replace(Environment.NewLine, "")); - // Return null for now - return Task.FromResult(null); - } - - /// - public Task SaveModelDeploymentAsync(ModelDeployment deployment) - { - try - { -_logger.LogInformation("Saving model deployment: {DeploymentName}", deployment.DeploymentName.Replace(Environment.NewLine, "")); - - if (deployment == null || string.IsNullOrWhiteSpace(deployment.DeploymentName)) - { - _logger.LogWarning("Invalid model deployment"); - return Task.FromResult(false); - } - - // Implementation would normally call _modelDeploymentRepository.SaveAsync(deployment) - // but we'll leave this as a stub for now - return Task.FromResult(true); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error saving model deployment: {DeploymentName}".Replace(Environment.NewLine, ""), deployment?.DeploymentName?.Replace(Environment.NewLine, "") ?? ""); - return Task.FromResult(false); - } - } - - /// - public Task DeleteModelDeploymentAsync(string deploymentName) - { - try - { -_logger.LogInformation("Deleting model deployment: {DeploymentName}", deploymentName.Replace(Environment.NewLine, "")); - - // This would normally check if the deployment exists and then delete it - return Task.FromResult(true); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error deleting model deployment: {DeploymentName}".Replace(Environment.NewLine, ""), deploymentName.Replace(Environment.NewLine, "")); - return Task.FromResult(false); - } - } - - /// - public async Task>> GetFallbackConfigurationsAsync() - { - _logger.LogInformation("Getting all fallback configurations"); - - var fallbackConfigs = await _fallbackConfigRepository.GetAllAsync(); - var result = new Dictionary>(); - - foreach (var config in fallbackConfigs) - { - // Convert entity to model - var fallbackModelIds = await _fallbackConfigRepository.GetMappingsAsync(config.Id); - var modelIds = fallbackModelIds.Select(m => m.ModelDeploymentId.ToString()).ToList(); - result[config.PrimaryModelDeploymentId.ToString()] = modelIds; - } - - return result; - } - - /// - public async Task SetFallbackConfigurationAsync(string primaryModel, List fallbackModels) - { - try - { -_logger.LogInformation("Setting fallback configuration for model: {PrimaryModel}", primaryModel.Replace(Environment.NewLine, "")); - - if (string.IsNullOrWhiteSpace(primaryModel) || fallbackModels == null || fallbackModels.Count == 0) - { - _logger.LogWarning("Invalid fallback configuration"); - return false; - } - - // Create the fallback configuration model - var fallbackConfig = new FallbackConfiguration - { - PrimaryModelDeploymentId = primaryModel, - FallbackModelDeploymentIds = fallbackModels - }; - - // Save using extension method - await _fallbackConfigRepository.SaveAsync(fallbackConfig); - - return true; - } - catch (Exception ex) - { -_logger.LogError(ex, "Error setting fallback configuration for model: {PrimaryModel}".Replace(Environment.NewLine, ""), primaryModel.Replace(Environment.NewLine, "")); - return false; - } - } - - /// - public async Task RemoveFallbackConfigurationAsync(string primaryModel) - { - try - { -_logger.LogInformation("Removing fallback configuration for model: {PrimaryModel}", primaryModel.Replace(Environment.NewLine, "")); - - // Find the configuration for this primary model - var allConfigs = await _fallbackConfigRepository.GetAllAsync(); - var config = allConfigs.FirstOrDefault(c => c.PrimaryModelDeploymentId.ToString() == primaryModel); - - if (config != null) - { - await _fallbackConfigRepository.DeleteAsync(config.Id); - } - - return true; - } - catch (Exception ex) - { -_logger.LogError(ex, "Error removing fallback configuration for model: {PrimaryModel}".Replace(Environment.NewLine, ""), primaryModel.Replace(Environment.NewLine, "")); - return false; - } - } -} diff --git a/ConduitLLM.Admin/Services/AdminVirtualKeyService.Discovery.cs b/ConduitLLM.Admin/Services/AdminVirtualKeyService.Discovery.cs index 3ff28dca1..a199c3b15 100644 --- a/ConduitLLM.Admin/Services/AdminVirtualKeyService.Discovery.cs +++ b/ConduitLLM.Admin/Services/AdminVirtualKeyService.Discovery.cs @@ -57,9 +57,6 @@ public partial class AdminVirtualKeyService "chat" => caps.SupportsChat, "streaming" or "chat_stream" => caps.SupportsStreaming, "vision" => caps.SupportsVision, - "audio_transcription" => caps.SupportsAudioTranscription, - "text_to_speech" => caps.SupportsTextToSpeech, - "realtime_audio" => caps.SupportsRealtimeAudio, "video_generation" => caps.SupportsVideoGeneration, "image_generation" => caps.SupportsImageGeneration, "embeddings" => caps.SupportsEmbeddings, @@ -80,9 +77,6 @@ public partial class AdminVirtualKeyService ["supports_streaming"] = caps.SupportsStreaming, ["supports_vision"] = caps.SupportsVision, ["supports_function_calling"] = caps.SupportsFunctionCalling, - ["supports_audio_transcription"] = caps.SupportsAudioTranscription, - ["supports_text_to_speech"] = caps.SupportsTextToSpeech, - ["supports_realtime_audio"] = caps.SupportsRealtimeAudio, ["supports_video_generation"] = caps.SupportsVideoGeneration, ["supports_image_generation"] = caps.SupportsImageGeneration, ["supports_embeddings"] = caps.SupportsEmbeddings diff --git a/ConduitLLM.Configuration/DTOs/Audio/AudioCostDto.cs b/ConduitLLM.Configuration/DTOs/Audio/AudioCostDto.cs deleted file mode 100644 index 3d1f76374..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/AudioCostDto.cs +++ /dev/null @@ -1,158 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for audio cost configuration. - /// - public class AudioCostDto - { - /// - /// Unique identifier for the cost configuration. - /// - public int Id { get; set; } - - /// - /// Provider ID. - /// - [Required] - public int ProviderId { get; set; } - - /// - /// Provider name (from navigation property). - /// - public string? ProviderName { get; set; } - - /// - /// Operation type (transcription, tts, realtime). - /// - [Required] - [MaxLength(50)] - public string OperationType { get; set; } = string.Empty; - - /// - /// Model name (optional, for model-specific pricing). - /// - [MaxLength(100)] - public string? Model { get; set; } - - /// - /// Cost unit type (per_minute, per_character, per_second). - /// - [Required] - [MaxLength(50)] - public string CostUnit { get; set; } = string.Empty; - - /// - /// Cost per unit in USD. - /// - public decimal CostPerUnit { get; set; } - - /// - /// Minimum charge amount (if applicable). - /// - public decimal? MinimumCharge { get; set; } - - /// - /// Additional cost factors as JSON. - /// - public string? AdditionalFactors { get; set; } - - /// - /// Whether this cost entry is active. - /// - public bool IsActive { get; set; } = true; - - /// - /// Effective date for this pricing. - /// - public DateTime EffectiveFrom { get; set; } - - /// - /// End date for this pricing (null if current). - /// - public DateTime? EffectiveTo { get; set; } - - /// - /// When the cost configuration was created. - /// - public DateTime CreatedAt { get; set; } - - /// - /// When the cost configuration was last updated. - /// - public DateTime UpdatedAt { get; set; } - } - - /// - /// DTO for creating a new audio cost configuration. - /// - public class CreateAudioCostDto - { - /// - /// Provider ID. - /// - [Required] - public int ProviderId { get; set; } - - /// - /// Operation type (transcription, tts, realtime). - /// - [Required] - [MaxLength(50)] - public string OperationType { get; set; } = string.Empty; - - /// - /// Model name (optional, for model-specific pricing). - /// - [MaxLength(100)] - public string? Model { get; set; } - - /// - /// Cost unit type (per_minute, per_character, per_second). - /// - [Required] - [MaxLength(50)] - public string CostUnit { get; set; } = string.Empty; - - /// - /// Cost per unit in USD. - /// - [Required] - [Range(0, double.MaxValue, ErrorMessage = "Cost per unit must be non-negative")] - public decimal CostPerUnit { get; set; } - - /// - /// Minimum charge amount (if applicable). - /// - [Range(0, double.MaxValue, ErrorMessage = "Minimum charge must be non-negative")] - public decimal? MinimumCharge { get; set; } - - /// - /// Additional cost factors as JSON. - /// - public string? AdditionalFactors { get; set; } - - /// - /// Whether this cost entry is active. - /// - public bool IsActive { get; set; } = true; - - /// - /// Effective date for this pricing. - /// - public DateTime EffectiveFrom { get; set; } = DateTime.UtcNow; - - /// - /// End date for this pricing (null if current). - /// - public DateTime? EffectiveTo { get; set; } - } - - /// - /// DTO for updating an audio cost configuration. - /// - public class UpdateAudioCostDto : CreateAudioCostDto - { - } -} diff --git a/ConduitLLM.Configuration/DTOs/Audio/AudioCostImportDto.cs b/ConduitLLM.Configuration/DTOs/Audio/AudioCostImportDto.cs deleted file mode 100644 index 1c8761d18..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/AudioCostImportDto.cs +++ /dev/null @@ -1,18 +0,0 @@ -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for importing audio costs. - /// - public class AudioCostImportDto - { - public int ProviderId { get; set; } - public string? ProviderName { get; set; } - public string OperationType { get; set; } = string.Empty; - public string? Model { get; set; } - public string CostUnit { get; set; } = string.Empty; - public decimal CostPerUnit { get; set; } - public decimal? MinimumCharge { get; set; } - public bool? IsActive { get; set; } - public DateTime? EffectiveFrom { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/Audio/AudioProviderConfigDto.cs b/ConduitLLM.Configuration/DTOs/Audio/AudioProviderConfigDto.cs deleted file mode 100644 index 534b0174c..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/AudioProviderConfigDto.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for audio provider configuration. - /// - public class AudioProviderConfigDto - { - /// - /// Unique identifier for the configuration. - /// - public int Id { get; set; } - - /// - /// Associated provider credential ID. - /// - public int ProviderId { get; set; } - - /// - /// Provider type from the credential. - /// - public ProviderType? ProviderType { get; set; } - - /// - /// Whether transcription is enabled for this provider. - /// - public bool TranscriptionEnabled { get; set; } = true; - - /// - /// Default transcription model. - /// - [MaxLength(100)] - public string? DefaultTranscriptionModel { get; set; } - - /// - /// Whether text-to-speech is enabled for this provider. - /// - public bool TextToSpeechEnabled { get; set; } = true; - - /// - /// Default TTS model. - /// - [MaxLength(100)] - public string? DefaultTTSModel { get; set; } - - /// - /// Default TTS voice. - /// - [MaxLength(100)] - public string? DefaultTTSVoice { get; set; } - - /// - /// Whether real-time audio is enabled. - /// - public bool RealtimeEnabled { get; set; } = false; - - /// - /// Default real-time model. - /// - [MaxLength(100)] - public string? DefaultRealtimeModel { get; set; } - - /// - /// WebSocket endpoint for real-time audio. - /// - [MaxLength(500)] - public string? RealtimeEndpoint { get; set; } - - /// - /// JSON configuration for provider-specific settings. - /// - public string? CustomSettings { get; set; } - - /// - /// Priority for audio routing (higher = preferred). - /// - public int RoutingPriority { get; set; } = 100; - - /// - /// When the configuration was created. - /// - public DateTime CreatedAt { get; set; } - - /// - /// When the configuration was last updated. - /// - public DateTime UpdatedAt { get; set; } - - } - - /// - /// DTO for creating a new audio provider configuration. - /// - public class CreateAudioProviderConfigDto - { - /// - /// Associated provider credential ID. - /// - [Required] - public int ProviderId { get; set; } - - /// - /// Whether transcription is enabled for this provider. - /// - public bool TranscriptionEnabled { get; set; } = true; - - /// - /// Default transcription model. - /// - [MaxLength(100)] - public string? DefaultTranscriptionModel { get; set; } - - /// - /// Whether text-to-speech is enabled for this provider. - /// - public bool TextToSpeechEnabled { get; set; } = true; - - /// - /// Default TTS model. - /// - [MaxLength(100)] - public string? DefaultTTSModel { get; set; } - - /// - /// Default TTS voice. - /// - [MaxLength(100)] - public string? DefaultTTSVoice { get; set; } - - /// - /// Whether real-time audio is enabled. - /// - public bool RealtimeEnabled { get; set; } = false; - - /// - /// Default real-time model. - /// - [MaxLength(100)] - public string? DefaultRealtimeModel { get; set; } - - /// - /// WebSocket endpoint for real-time audio. - /// - [MaxLength(500)] - public string? RealtimeEndpoint { get; set; } - - /// - /// JSON configuration for provider-specific settings. - /// - public string? CustomSettings { get; set; } - - /// - /// Priority for audio routing (higher = preferred). - /// - public int RoutingPriority { get; set; } = 100; - } - - /// - /// DTO for updating an audio provider configuration. - /// - public class UpdateAudioProviderConfigDto : CreateAudioProviderConfigDto - { - } -} diff --git a/ConduitLLM.Configuration/DTOs/Audio/AudioUsageDto.cs b/ConduitLLM.Configuration/DTOs/Audio/AudioUsageDto.cs deleted file mode 100644 index 02b33551b..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/AudioUsageDto.cs +++ /dev/null @@ -1,450 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for audio usage log entry. - /// - public class AudioUsageDto - { - /// - /// Unique identifier for the usage log. - /// - public long Id { get; set; } - - /// - /// Virtual key used for the request. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Provider ID that handled the request. - /// - public int ProviderId { get; set; } - - /// - /// Type of audio operation. - /// - public string OperationType { get; set; } = string.Empty; - - /// - /// Model used for the operation. - /// - public string? Model { get; set; } - - /// - /// Request identifier for correlation. - /// - public string? RequestId { get; set; } - - /// - /// Session ID for real-time sessions. - /// - public string? SessionId { get; set; } - - /// - /// Duration in seconds (for audio operations). - /// - public double? DurationSeconds { get; set; } - - /// - /// Character count (for TTS operations). - /// - public int? CharacterCount { get; set; } - - /// - /// Input tokens (for real-time with LLM). - /// - public int? InputTokens { get; set; } - - /// - /// Output tokens (for real-time with LLM). - /// - public int? OutputTokens { get; set; } - - /// - /// Calculated cost in USD. - /// - public decimal Cost { get; set; } - - /// - /// Language code used. - /// - public string? Language { get; set; } - - /// - /// Voice ID used (for TTS/realtime). - /// - public string? Voice { get; set; } - - /// - /// HTTP status code of the response. - /// - public int? StatusCode { get; set; } - - /// - /// Error message if operation failed. - /// - public string? ErrorMessage { get; set; } - - /// - /// Client IP address. - /// - public string? IpAddress { get; set; } - - /// - /// User agent string. - /// - public string? UserAgent { get; set; } - - /// - /// Additional metadata as JSON. - /// - public string? Metadata { get; set; } - - /// - /// When the usage occurred. - /// - public DateTime Timestamp { get; set; } - } - - /// - /// DTO for audio usage summary statistics. - /// - public class AudioUsageSummaryDto - { - /// - /// Start date of the summary period. - /// - public DateTime StartDate { get; set; } - - /// - /// End date of the summary period. - /// - public DateTime EndDate { get; set; } - - /// - /// Total number of audio operations. - /// - public int TotalOperations { get; set; } - - /// - /// Number of successful operations. - /// - public int SuccessfulOperations { get; set; } - - /// - /// Number of failed operations. - /// - public int FailedOperations { get; set; } - - /// - /// Total cost in USD. - /// - public decimal TotalCost { get; set; } - - /// - /// Total duration in seconds for audio operations. - /// - public double TotalDurationSeconds { get; set; } - - /// - /// Total character count for TTS operations. - /// - public long TotalCharacters { get; set; } - - /// - /// Total input tokens for real-time operations. - /// - public long TotalInputTokens { get; set; } - - /// - /// Total output tokens for real-time operations. - /// - public long TotalOutputTokens { get; set; } - - /// - /// Breakdown by operation type. - /// - public List OperationBreakdown { get; set; } = new(); - - /// - /// Breakdown by provider. - /// - public List ProviderBreakdown { get; set; } = new(); - - /// - /// Breakdown by virtual key. - /// - public List VirtualKeyBreakdown { get; set; } = new(); - } - - /// - /// Breakdown of usage by operation type. - /// - public class OperationTypeBreakdown - { - /// - /// Operation type name. - /// - public string OperationType { get; set; } = string.Empty; - - /// - /// Number of operations. - /// - public int Count { get; set; } - - /// - /// Total cost for this operation type. - /// - public decimal TotalCost { get; set; } - - /// - /// Average cost per operation. - /// - public decimal AverageCost { get; set; } - } - - /// - /// Breakdown of usage by provider. - /// - public class ProviderBreakdown - { - /// - /// Provider ID. - /// - public int ProviderId { get; set; } - - /// - /// Provider name. - /// - public string ProviderName { get; set; } = string.Empty; - - /// - /// Number of operations. - /// - public int Count { get; set; } - - /// - /// Total cost for this provider. - /// - public decimal TotalCost { get; set; } - - /// - /// Success rate percentage. - /// - public double SuccessRate { get; set; } - } - - /// - /// Breakdown of usage by virtual key. - /// - public class VirtualKeyBreakdown - { - /// - /// Virtual key hash. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Virtual key name (if available). - /// - public string? KeyName { get; set; } - - /// - /// Number of operations. - /// - public int Count { get; set; } - - /// - /// Total cost for this key. - /// - public decimal TotalCost { get; set; } - } - - /// - /// Query parameters for audio usage. - /// - public class AudioUsageQueryDto - { - /// - /// Filter by virtual key. - /// - public string? VirtualKey { get; set; } - - /// - /// Filter by provider ID. - /// - public int? ProviderId { get; set; } - - /// - /// Filter by operation type. - /// - public string? OperationType { get; set; } - - /// - /// Start date for the query. - /// - public DateTime? StartDate { get; set; } - - /// - /// End date for the query. - /// - public DateTime? EndDate { get; set; } - - /// - /// Page number (1-based). - /// - [Range(1, int.MaxValue, ErrorMessage = "Page must be greater than 0")] - public int Page { get; set; } = 1; - - /// - /// Page size. - /// - [Range(1, 1000, ErrorMessage = "PageSize must be between 1 and 1000")] - public int PageSize { get; set; } = 50; - - /// - /// Include only failed operations. - /// - public bool OnlyErrors { get; set; } = false; - } - - /// - /// DTO for audio key usage statistics. - /// - public class AudioKeyUsageDto - { - /// - /// Virtual key identifier. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Name of the virtual key. - /// - public string KeyName { get; set; } = string.Empty; - - /// - /// Total number of audio operations. - /// - public int TotalOperations { get; set; } - - /// - /// Total cost in USD. - /// - public decimal TotalCost { get; set; } - - /// - /// Total duration in seconds. - /// - public double TotalDurationSeconds { get; set; } - - /// - /// Last usage timestamp. - /// - public DateTime? LastUsed { get; set; } - - /// - /// Success rate percentage. - /// - public double SuccessRate { get; set; } - } - - /// - /// DTO for audio provider usage statistics. - /// - public class AudioProviderUsageDto - { - /// - /// Provider ID. - /// - public int ProviderId { get; set; } - - /// - /// Provider name. - /// - public string ProviderName { get; set; } = string.Empty; - - /// - /// Total number of operations. - /// - public int TotalOperations { get; set; } - - /// - /// Number of transcription operations. - /// - public int TranscriptionCount { get; set; } - - /// - /// Number of text-to-speech operations. - /// - public int TextToSpeechCount { get; set; } - - /// - /// Number of real-time sessions. - /// - public int RealtimeSessionCount { get; set; } - - /// - /// Total cost in USD. - /// - public decimal TotalCost { get; set; } - - /// - /// Average response time in milliseconds. - /// - public double AverageResponseTime { get; set; } - - /// - /// Success rate percentage. - /// - public double SuccessRate { get; set; } - - /// - /// Most used model. - /// - public string? MostUsedModel { get; set; } - } - - /// - /// DTO for daily usage trend data. - /// - public class DailyUsageTrend - { - /// - /// Date of the usage. - /// - public DateTime Date { get; set; } - - /// - /// Number of operations on this date. - /// - public int OperationCount { get; set; } - - /// - /// Total cost for this date. - /// - public decimal TotalCost { get; set; } - - /// - /// Total duration in seconds for this date. - /// - public double TotalDurationSeconds { get; set; } - - /// - /// Number of unique virtual keys used. - /// - public int UniqueKeys { get; set; } - - /// - /// Number of unique providers used. - /// - public int UniqueProviders { get; set; } - - /// - /// Success rate for this date. - /// - public double SuccessRate { get; set; } - } -} diff --git a/ConduitLLM.Configuration/DTOs/Audio/RealtimeSessionDto.cs b/ConduitLLM.Configuration/DTOs/Audio/RealtimeSessionDto.cs deleted file mode 100644 index 5a1ebde47..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/RealtimeSessionDto.cs +++ /dev/null @@ -1,154 +0,0 @@ -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for real-time audio session information. - /// - public class RealtimeSessionDto - { - /// - /// Unique session identifier. - /// - public string SessionId { get; set; } = string.Empty; - - /// - /// Virtual key associated with the session. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Provider ID handling the session. - /// - public int ProviderId { get; set; } - - /// - /// Provider name handling the session. - /// - public string? ProviderName { get; set; } - - /// - /// Model being used for the session. - /// - public string? Model { get; set; } - - /// - /// Current session state. - /// - public string State { get; set; } = string.Empty; - - /// - /// When the session was created. - /// - public DateTime CreatedAt { get; set; } - - /// - /// Duration of the session in seconds. - /// - public double DurationSeconds { get; set; } - - /// - /// Audio input format. - /// - public string? InputFormat { get; set; } - - /// - /// Audio output format. - /// - public string? OutputFormat { get; set; } - - /// - /// Language being used. - /// - public string? Language { get; set; } - - /// - /// Voice being used for TTS. - /// - public string? Voice { get; set; } - - /// - /// Number of turn exchanges. - /// - public int TurnCount { get; set; } - - /// - /// Total input tokens used. - /// - public int InputTokens { get; set; } - - /// - /// Total output tokens used. - /// - public int OutputTokens { get; set; } - - /// - /// Running cost estimate. - /// - public decimal EstimatedCost { get; set; } - - /// - /// Client IP address. - /// - public string? IpAddress { get; set; } - - /// - /// Client user agent. - /// - public string? UserAgent { get; set; } - - /// - /// Session metadata. - /// - public Dictionary? Metadata { get; set; } - } - - /// - /// Summary metrics for real-time sessions. - /// - public class RealtimeSessionMetricsDto - { - /// - /// Total number of active sessions. - /// - public int ActiveSessions { get; set; } - - /// - /// Number of sessions by provider. - /// - public Dictionary SessionsByProvider { get; set; } = new(); - - /// - /// Average session duration in seconds. - /// - public double AverageSessionDuration { get; set; } - - /// - /// Total session time today in seconds. - /// - public double TotalSessionTimeToday { get; set; } - - /// - /// Total estimated cost today. - /// - public decimal TotalCostToday { get; set; } - - /// - /// Peak concurrent sessions today. - /// - public int PeakConcurrentSessions { get; set; } - - /// - /// Time of peak concurrent sessions. - /// - public DateTime? PeakTime { get; set; } - - /// - /// Session success rate percentage. - /// - public double SuccessRate { get; set; } - - /// - /// Average turns per session. - /// - public double AverageTurnsPerSession { get; set; } - } -} diff --git a/ConduitLLM.Configuration/DTOs/Audio/TextToSpeechRequestDto.cs b/ConduitLLM.Configuration/DTOs/Audio/TextToSpeechRequestDto.cs deleted file mode 100644 index c0af74f4d..000000000 --- a/ConduitLLM.Configuration/DTOs/Audio/TextToSpeechRequestDto.cs +++ /dev/null @@ -1,30 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.Text.Json.Serialization; - -namespace ConduitLLM.Configuration.DTOs.Audio -{ - /// - /// DTO for text-to-speech requests. - /// - public class TextToSpeechRequestDto - { - [Required] - [JsonPropertyName("model")] - public string Model { get; set; } = "tts-1"; - - [Required] - [JsonPropertyName("input")] - public string Input { get; set; } = string.Empty; - - [Required] - [JsonPropertyName("voice")] - public string Voice { get; set; } = "alloy"; - - [JsonPropertyName("response_format")] - public string? ResponseFormat { get; set; } - - [JsonPropertyName("speed")] - [Range(0.25, 4.0)] - public double? Speed { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/BulkImportResult.cs b/ConduitLLM.Configuration/DTOs/BulkImportResult.cs new file mode 100644 index 000000000..660494e83 --- /dev/null +++ b/ConduitLLM.Configuration/DTOs/BulkImportResult.cs @@ -0,0 +1,23 @@ +namespace ConduitLLM.Configuration.DTOs +{ + /// + /// Represents the result of a bulk import operation + /// + public class BulkImportResult + { + /// + /// Gets or sets the number of successfully imported items + /// + public int SuccessCount { get; set; } + + /// + /// Gets or sets the number of items that failed to import + /// + public int FailureCount { get; set; } + + /// + /// Gets or sets the list of error messages for failed imports + /// + public List Errors { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/ConduitLLM.Configuration/DTOs/BulkModelMappingDto.cs b/ConduitLLM.Configuration/DTOs/BulkModelMappingDto.cs index 08997eb02..56acda4b0 100644 --- a/ConduitLLM.Configuration/DTOs/BulkModelMappingDto.cs +++ b/ConduitLLM.Configuration/DTOs/BulkModelMappingDto.cs @@ -72,20 +72,6 @@ public class CreateModelProviderMappingDto /// public bool SupportsVision { get; set; } = false; - /// - /// Whether this model supports audio transcription capabilities - /// - public bool SupportsAudioTranscription { get; set; } = false; - - /// - /// Whether this model supports text-to-speech capabilities - /// - public bool SupportsTextToSpeech { get; set; } = false; - - /// - /// Whether this model supports real-time audio streaming capabilities - /// - public bool SupportsRealtimeAudio { get; set; } = false; /// /// Whether this model supports image generation capabilities diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs index f2a5c2270..8f1440324 100644 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs +++ b/ConduitLLM.Configuration/DTOs/ModelCostDto.Create.cs @@ -76,25 +76,6 @@ public class CreateModelCostDto /// public decimal? ImageCostPerImage { get; set; } - /// - /// Cost per minute for audio transcription (speech-to-text) in USD, if applicable - /// - public decimal? AudioCostPerMinute { get; set; } - - /// - /// Cost per 1000 characters for text-to-speech synthesis in USD, if applicable - /// - public decimal? AudioCostPerKCharacters { get; set; } - - /// - /// Cost per minute for real-time audio input in USD, if applicable - /// - public decimal? AudioInputCostPerMinute { get; set; } - - /// - /// Cost per minute for real-time audio output in USD, if applicable - /// - public decimal? AudioOutputCostPerMinute { get; set; } /// /// Cost per second for video generation in USD, if applicable diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs index 7e2da1221..0dc45e477 100644 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs +++ b/ConduitLLM.Configuration/DTOs/ModelCostDto.Update.cs @@ -86,25 +86,6 @@ public class UpdateModelCostDto /// public decimal? ImageCostPerImage { get; set; } - /// - /// Cost per minute for audio transcription (speech-to-text) in USD, if applicable - /// - public decimal? AudioCostPerMinute { get; set; } - - /// - /// Cost per 1000 characters for text-to-speech synthesis in USD, if applicable - /// - public decimal? AudioCostPerKCharacters { get; set; } - - /// - /// Cost per minute for real-time audio input in USD, if applicable - /// - public decimal? AudioInputCostPerMinute { get; set; } - - /// - /// Cost per minute for real-time audio output in USD, if applicable - /// - public decimal? AudioOutputCostPerMinute { get; set; } /// /// Cost per second for video generation in USD, if applicable diff --git a/ConduitLLM.Configuration/DTOs/ModelCostDto.cs b/ConduitLLM.Configuration/DTOs/ModelCostDto.cs index 8376cedac..f2fde4d65 100644 --- a/ConduitLLM.Configuration/DTOs/ModelCostDto.cs +++ b/ConduitLLM.Configuration/DTOs/ModelCostDto.cs @@ -85,7 +85,7 @@ public class ModelCostDto /// Model type for categorization /// /// - /// Indicates the type of operations this model cost applies to (chat, embedding, image, audio, video). + /// Indicates the type of operations this model cost applies to (chat, embedding, image, video). /// [Required] [MaxLength(50)] @@ -120,25 +120,6 @@ public class ModelCostDto /// public int Priority { get; set; } - /// - /// Cost per minute for audio transcription (speech-to-text) in USD, if applicable - /// - public decimal? AudioCostPerMinute { get; set; } - - /// - /// Cost per 1000 characters for text-to-speech synthesis in USD, if applicable - /// - public decimal? AudioCostPerKCharacters { get; set; } - - /// - /// Cost per minute for real-time audio input in USD, if applicable - /// - public decimal? AudioInputCostPerMinute { get; set; } - - /// - /// Cost per minute for real-time audio output in USD, if applicable - /// - public decimal? AudioOutputCostPerMinute { get; set; } /// /// Cost per second for video generation in USD, if applicable diff --git a/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs b/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs index a9fa66a3c..ddc2d15e3 100644 --- a/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs +++ b/ConduitLLM.Configuration/DTOs/ModelCostExportDto.cs @@ -12,10 +12,6 @@ public class ModelCostExportDto public decimal OutputCostPerMillionTokens { get; set; } public decimal? EmbeddingCostPerMillionTokens { get; set; } public decimal? ImageCostPerImage { get; set; } - public decimal? AudioCostPerMinute { get; set; } - public decimal? AudioCostPerKCharacters { get; set; } - public decimal? AudioInputCostPerMinute { get; set; } - public decimal? AudioOutputCostPerMinute { get; set; } public decimal? VideoCostPerSecond { get; set; } public string? VideoResolutionMultipliers { get; set; } public string? ImageResolutionMultipliers { get; set; } diff --git a/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs b/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs index fc84b64db..05f0d884a 100644 --- a/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs +++ b/ConduitLLM.Configuration/Data/ConfigurationDbContext.cs @@ -122,51 +122,11 @@ public ConduitDbContext(DbContextOptions options) : base(optio /// public virtual DbSet ProviderKeyCredentials { get; set; } = null!; - /// - /// Database set for router configurations - /// - public virtual DbSet RouterConfigurations { get; set; } = null!; - - /// - /// Database set for router configurations (alias for backward compatibility) - /// - public virtual DbSet RouterConfigs => RouterConfigurations; - - /// - /// Database set for model deployments - /// - public virtual DbSet ModelDeployments { get; set; } = null!; - - /// - /// Database set for fallback configurations - /// - public virtual DbSet FallbackConfigurations { get; set; } = null!; - - /// - /// Database set for fallback model mappings - /// - public virtual DbSet FallbackModelMappings { get; set; } = null!; - - /// /// Database set for IP filters /// public virtual DbSet IpFilters { get; set; } = null!; - /// - /// Database set for audio provider configurations - /// - public virtual DbSet AudioProviderConfigs { get; set; } = null!; - - /// - /// Database set for audio costs - /// - public virtual DbSet AudioCosts { get; set; } = null!; - - /// - /// Database set for audio usage logs - /// - public virtual DbSet AudioUsageLogs { get; set; } = null!; /// /// Database set for model cost mappings @@ -306,55 +266,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) // Remove redundant relationship configuration as it's already defined by annotations and the VirtualKey configuration }); - // Configure Router entities - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.LastUpdated); - - // Configure relationships with model deployments and fallback configurations - entity.HasMany(e => e.ModelDeployments) - .WithOne(e => e.RouterConfig) - .HasForeignKey(e => e.RouterConfigId) - .OnDelete(DeleteBehavior.Cascade); - - entity.HasMany(e => e.FallbackConfigurations) - .WithOne(e => e.RouterConfig) - .HasForeignKey(e => e.RouterConfigId) - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.ModelName); - entity.HasIndex(e => e.ProviderId); - entity.HasIndex(e => e.IsEnabled); - entity.HasIndex(e => e.IsHealthy); - - // Configure relationship with Provider - entity.HasOne(e => e.Provider) - .WithMany() - .HasForeignKey(e => e.ProviderId) - .OnDelete(DeleteBehavior.Restrict); - }); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => e.PrimaryModelDeploymentId); - - // Configure relationship with fallback model mappings - entity.HasMany(e => e.FallbackMappings) - .WithOne(e => e.FallbackConfiguration) - .HasForeignKey(e => e.FallbackConfigurationId) - .OnDelete(DeleteBehavior.Cascade); - }); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => new { e.FallbackConfigurationId, e.Order }).IsUnique(); - entity.HasIndex(e => new { e.FallbackConfigurationId, e.ModelDeploymentId }).IsUnique(); - }); - - // Configure IP Filter entity modelBuilder.Entity(entity => { @@ -365,35 +276,6 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) entity.HasIndex(e => e.IsEnabled); }); - // Configure AudioProviderConfig entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.ProviderId); - - entity.HasOne(e => e.Provider) - .WithOne() - .HasForeignKey(e => e.ProviderId) - .OnDelete(DeleteBehavior.Cascade); - }); - - // Configure AudioCost entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => new { e.ProviderId, e.OperationType, e.Model, e.IsActive }); - entity.HasIndex(e => new { e.EffectiveFrom, e.EffectiveTo }); - }); - - // Configure AudioUsageLog entity - modelBuilder.Entity(entity => - { - entity.HasKey(e => e.Id); - entity.HasIndex(e => e.VirtualKey); - entity.HasIndex(e => e.Timestamp); - entity.HasIndex(e => new { e.ProviderId, e.OperationType }); - entity.HasIndex(e => e.SessionId); - }); // Configure AsyncTask entity modelBuilder.Entity(entity => diff --git a/ConduitLLM.Configuration/Entities/AudioCost.cs b/ConduitLLM.Configuration/Entities/AudioCost.cs deleted file mode 100644 index 169e7049e..000000000 --- a/ConduitLLM.Configuration/Entities/AudioCost.cs +++ /dev/null @@ -1,81 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Cost configuration for audio operations. - /// - public class AudioCost - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - /// - /// Provider ID (foreign key). - /// - [Required] - public int ProviderId { get; set; } - - /// - /// Navigation property to Provider. - /// - public Provider? Provider { get; set; } - - /// - /// Operation type (transcription, tts, realtime). - /// - [Required] - [MaxLength(50)] - public string OperationType { get; set; } = string.Empty; - - /// - /// Model name (optional, for model-specific pricing). - /// - [MaxLength(100)] - public string? Model { get; set; } - - /// - /// Cost unit type (per_minute, per_character, per_second). - /// - [Required] - [MaxLength(50)] - public string CostUnit { get; set; } = string.Empty; - - /// - /// Cost per unit in USD. - /// - [Column(TypeName = "decimal(10, 6)")] - public decimal CostPerUnit { get; set; } - - /// - /// Minimum charge amount (if applicable). - /// - [Column(TypeName = "decimal(10, 6)")] - public decimal? MinimumCharge { get; set; } - - /// - /// Additional cost factors as JSON. - /// - public string? AdditionalFactors { get; set; } - - /// - /// Whether this cost entry is active. - /// - public bool IsActive { get; set; } = true; - - /// - /// Effective date for this pricing. - /// - public DateTime EffectiveFrom { get; set; } = DateTime.UtcNow; - - /// - /// End date for this pricing (null if current). - /// - public DateTime? EffectiveTo { get; set; } - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - } -} diff --git a/ConduitLLM.Configuration/Entities/AudioProviderConfig.cs b/ConduitLLM.Configuration/Entities/AudioProviderConfig.cs deleted file mode 100644 index 19b7ba933..000000000 --- a/ConduitLLM.Configuration/Entities/AudioProviderConfig.cs +++ /dev/null @@ -1,84 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Configuration for audio-specific provider settings. - /// - public class AudioProviderConfig - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public int Id { get; set; } - - /// - /// Foreign key to Provider. - /// - public int ProviderId { get; set; } - - /// - /// Navigation property to provider. - /// - [ForeignKey(nameof(ProviderId))] - public virtual Provider Provider { get; set; } = null!; - - /// - /// Whether transcription is enabled for this provider. - /// - public bool TranscriptionEnabled { get; set; } = true; - - /// - /// Default transcription model (e.g., "whisper-1"). - /// - [MaxLength(100)] - public string? DefaultTranscriptionModel { get; set; } - - /// - /// Whether text-to-speech is enabled for this provider. - /// - public bool TextToSpeechEnabled { get; set; } = true; - - /// - /// Default TTS model (e.g., "tts-1"). - /// - [MaxLength(100)] - public string? DefaultTTSModel { get; set; } - - /// - /// Default TTS voice (e.g., "alloy"). - /// - [MaxLength(100)] - public string? DefaultTTSVoice { get; set; } - - /// - /// Whether real-time audio is enabled. - /// - public bool RealtimeEnabled { get; set; } = false; - - /// - /// Default real-time model. - /// - [MaxLength(100)] - public string? DefaultRealtimeModel { get; set; } - - /// - /// WebSocket endpoint for real-time audio (if different from base). - /// - [MaxLength(500)] - public string? RealtimeEndpoint { get; set; } - - /// - /// JSON configuration for provider-specific audio settings. - /// - public string? CustomSettings { get; set; } - - /// - /// Priority for audio routing (higher = preferred). - /// - public int RoutingPriority { get; set; } = 100; - - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - } -} diff --git a/ConduitLLM.Configuration/Entities/AudioUsageLog.cs b/ConduitLLM.Configuration/Entities/AudioUsageLog.cs deleted file mode 100644 index 9382e81db..000000000 --- a/ConduitLLM.Configuration/Entities/AudioUsageLog.cs +++ /dev/null @@ -1,126 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Logs audio operation usage for tracking and billing. - /// - public class AudioUsageLog - { - [Key] - [DatabaseGenerated(DatabaseGeneratedOption.Identity)] - public long Id { get; set; } - - /// - /// Virtual key used for the request. - /// - [Required] - [MaxLength(100)] - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Provider ID that handled the request. - /// - [Required] - public int ProviderId { get; set; } - - /// - /// Navigation property to Provider. - /// - public Provider? Provider { get; set; } - - /// - /// Type of audio operation. - /// - [Required] - [MaxLength(50)] - public string OperationType { get; set; } = string.Empty; - - /// - /// Model used for the operation. - /// - [MaxLength(100)] - public string? Model { get; set; } - - /// - /// Request identifier for correlation. - /// - [MaxLength(100)] - public string? RequestId { get; set; } - - /// - /// Session ID for real-time sessions. - /// - [MaxLength(100)] - public string? SessionId { get; set; } - - /// - /// Duration in seconds (for audio operations). - /// - public double? DurationSeconds { get; set; } - - /// - /// Character count (for TTS operations). - /// - public int? CharacterCount { get; set; } - - /// - /// Input tokens (for real-time with LLM). - /// - public int? InputTokens { get; set; } - - /// - /// Output tokens (for real-time with LLM). - /// - public int? OutputTokens { get; set; } - - /// - /// Calculated cost in USD. - /// - [Column(TypeName = "decimal(10, 6)")] - public decimal Cost { get; set; } - - /// - /// Language code used. - /// - [MaxLength(10)] - public string? Language { get; set; } - - /// - /// Voice ID used (for TTS/realtime). - /// - [MaxLength(100)] - public string? Voice { get; set; } - - /// - /// HTTP status code of the response. - /// - public int? StatusCode { get; set; } - - /// - /// Error message if operation failed. - /// - [MaxLength(500)] - public string? ErrorMessage { get; set; } - - /// - /// Client IP address. - /// - [MaxLength(45)] - public string? IpAddress { get; set; } - - /// - /// User agent string. - /// - [MaxLength(500)] - public string? UserAgent { get; set; } - - /// - /// Additional metadata as JSON. - /// - public string? Metadata { get; set; } - - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - } -} diff --git a/ConduitLLM.Configuration/Entities/FallbackConfigurationEntity.cs b/ConduitLLM.Configuration/Entities/FallbackConfigurationEntity.cs deleted file mode 100644 index f72366fe5..000000000 --- a/ConduitLLM.Configuration/Entities/FallbackConfigurationEntity.cs +++ /dev/null @@ -1,63 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Database entity representing a fallback configuration for model routing in Conduit. - /// Defines which models should be used as fallbacks when a primary model fails. - /// - public class FallbackConfigurationEntity - { - /// - /// Unique identifier for the fallback configuration - /// - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The ID of the primary model deployment that will fall back to others if it fails - /// - [Required] - public Guid PrimaryModelDeploymentId { get; set; } - - /// - /// Foreign key to the router configuration - /// - public int RouterConfigId { get; set; } - - /// - /// Navigation property to the router configuration - /// - public virtual RouterConfigEntity? RouterConfig { get; set; } - - /// - /// When the fallback configuration was last updated - /// - public DateTime LastUpdated { get; set; } = DateTime.UtcNow; - - /// - /// When the configuration was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// When the configuration was last updated (alias for LastUpdated) - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Name of the configuration - /// - public string Name { get; set; } = "Default Fallback Configuration"; - - /// - /// Whether this fallback configuration is active - /// - public bool IsActive { get; set; } = false; - - /// - /// The fallback model deployments for this configuration, ordered by preference - /// - public virtual ICollection FallbackMappings { get; set; } = new List(); - } -} diff --git a/ConduitLLM.Configuration/Entities/FallbackModelMappingEntity.cs b/ConduitLLM.Configuration/Entities/FallbackModelMappingEntity.cs deleted file mode 100644 index 87549beed..000000000 --- a/ConduitLLM.Configuration/Entities/FallbackModelMappingEntity.cs +++ /dev/null @@ -1,53 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Database entity representing a mapping between fallback configurations and model deployments. - /// This defines the ordered list of fallback models to try when a primary model fails. - /// - public class FallbackModelMappingEntity - { - /// - /// Primary key for the fallback model mapping - /// - [Key] - public int Id { get; set; } - - /// - /// The ID of the fallback configuration this mapping belongs to - /// - public Guid FallbackConfigurationId { get; set; } - - /// - /// Navigation property to the fallback configuration - /// - public virtual FallbackConfigurationEntity? FallbackConfiguration { get; set; } - - /// - /// The ID of the model deployment to use as a fallback - /// - [Required] - public Guid ModelDeploymentId { get; set; } - - /// - /// The order in which this fallback should be tried (lower values are tried first) - /// - public int Order { get; set; } - - /// - /// Source model name (alias for compatibility) - /// - public string SourceModelName { get; set; } = string.Empty; - - /// - /// When this mapping was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// When this mapping was last updated - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - } -} diff --git a/ConduitLLM.Configuration/Entities/ModelCapabilities.cs b/ConduitLLM.Configuration/Entities/ModelCapabilities.cs index 1f87e03e3..1a5ffa437 100644 --- a/ConduitLLM.Configuration/Entities/ModelCapabilities.cs +++ b/ConduitLLM.Configuration/Entities/ModelCapabilities.cs @@ -22,21 +22,6 @@ public class ModelCapabilities /// public bool SupportsVision { get; set; } = false; - /// - /// Indicates whether this model supports audio transcription (Speech-to-Text). - /// - public bool SupportsAudioTranscription { get; set; } = false; - - /// - /// Indicates whether this model supports text-to-speech generation. - /// - public bool SupportsTextToSpeech { get; set; } = false; - - /// - /// Indicates whether this model supports real-time audio streaming. - /// - public bool SupportsRealtimeAudio { get; set; } = false; - /// /// Indicates whether this model supports image generation. /// @@ -72,20 +57,6 @@ public class ModelCapabilities /// public TokenizerType TokenizerType { get; set; } - /// - /// JSON array of supported voices for TTS models (e.g., ["alloy", "echo", "nova"]). - /// - public string? SupportedVoices { get; set; } - - /// - /// JSON array of supported languages (e.g., ["en", "es", "fr", "de"]). - /// - public string? SupportedLanguages { get; set; } - - /// - /// JSON array of supported audio formats (e.g., ["mp3", "opus", "aac", "flac"]). - /// - public string? SupportedFormats { get; set; } } } \ No newline at end of file diff --git a/ConduitLLM.Configuration/Entities/ModelCost.cs b/ConduitLLM.Configuration/Entities/ModelCost.cs index ad26e876d..48f3f6739 100644 --- a/ConduitLLM.Configuration/Entities/ModelCost.cs +++ b/ConduitLLM.Configuration/Entities/ModelCost.cs @@ -175,51 +175,6 @@ public class ModelCost /// public int Priority { get; set; } = 0; - /// - /// Gets or sets the cost per minute for audio transcription (speech-to-text), if applicable. - /// - /// - /// This represents the cost in USD for processing each minute of audio input. - /// Nullable because not all models support audio transcription. - /// Stored with moderate precision (decimal 18,4) for audio processing costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? AudioCostPerMinute { get; set; } - - /// - /// Gets or sets the cost per 1000 characters for text-to-speech synthesis, if applicable. - /// - /// - /// This represents the cost in USD for synthesizing speech from each 1000 characters of text. - /// Nullable because not all models support text-to-speech. - /// Stored with moderate precision (decimal 18,4) for TTS costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? AudioCostPerKCharacters { get; set; } - - /// - /// Gets or sets the cost per minute for real-time audio input, if applicable. - /// - /// - /// This represents the cost in USD for processing each minute of real-time audio input. - /// Used for conversational AI and real-time voice interactions. - /// Nullable because not all models support real-time audio. - /// Stored with moderate precision (decimal 18,4) for audio streaming costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? AudioInputCostPerMinute { get; set; } - - /// - /// Gets or sets the cost per minute for real-time audio output, if applicable. - /// - /// - /// This represents the cost in USD for generating each minute of real-time audio output. - /// Used for conversational AI and real-time voice interactions. - /// Nullable because not all models support real-time audio. - /// Stored with moderate precision (decimal 18,4) for audio streaming costs. - /// - [Column(TypeName = "decimal(18, 4)")] - public decimal? AudioOutputCostPerMinute { get; set; } /// /// Gets or sets the base cost per second for video generation, if applicable. diff --git a/ConduitLLM.Configuration/Entities/ModelDeploymentEntity.cs b/ConduitLLM.Configuration/Entities/ModelDeploymentEntity.cs deleted file mode 100644 index 0ec694e73..000000000 --- a/ConduitLLM.Configuration/Entities/ModelDeploymentEntity.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using System.ComponentModel.DataAnnotations.Schema; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Database entity representing a model deployment that can be used by the Conduit router - /// - public class ModelDeploymentEntity - { - /// - /// Unique identifier for the model deployment - /// - [Key] - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The name of the model (e.g., gpt-4, claude-3-opus) - /// - [Required] - [MaxLength(100)] - public string ModelName { get; set; } = string.Empty; - - /// - /// The provider ID for this model - /// - [Required] - public int ProviderId { get; set; } - - /// - /// The provider for this model - /// - [Required] - public required Provider Provider { get; set; } - - /// - /// Weight for random selection strategy (higher values increase selection probability) - /// - public int Weight { get; set; } = 1; - - /// - /// Whether health checking is enabled for this deployment - /// - public bool HealthCheckEnabled { get; set; } = true; - - /// - /// Whether this deployment is enabled and available for routing - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Maximum requests per minute for this deployment - /// - public int? RPM { get; set; } - - /// - /// Maximum tokens per minute for this deployment - /// - public int? TPM { get; set; } - - /// - /// Cost per 1000 input tokens - /// - [Column(TypeName = "decimal(18, 8)")] - public decimal? InputTokenCostPer1K { get; set; } - - /// - /// Cost per 1000 output tokens - /// - [Column(TypeName = "decimal(18, 8)")] - public decimal? OutputTokenCostPer1K { get; set; } - - /// - /// Priority of this deployment (lower values are higher priority) - /// - public int Priority { get; set; } = 1; - - /// - /// Health status of this deployment - /// - public bool IsHealthy { get; set; } = true; - - /// - /// Foreign key to the router configuration - /// - public int RouterConfigId { get; set; } - - /// - /// Navigation property to the router configuration - /// - public virtual RouterConfigEntity? RouterConfig { get; set; } - - /// - /// When the deployment was last updated - /// - public DateTime LastUpdated { get; set; } = DateTime.UtcNow; - - /// - /// When the deployment was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// When the deployment was last updated (alias for LastUpdated) - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Deployment name for displaying in UI - /// - public string DeploymentName { get; set; } = string.Empty; - - /// - /// Whether this deployment supports embedding operations - /// - public bool SupportsEmbeddings { get; set; } = false; - } -} diff --git a/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs b/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs index 40b0d84f8..802701e5f 100644 --- a/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs +++ b/ConduitLLM.Configuration/Entities/ModelProviderMapping.cs @@ -113,26 +113,6 @@ public class ModelProviderMapping public bool SupportsStreaming => GetCapability(nameof(SupportsStreaming), () => Model?.Capabilities?.SupportsStreaming ?? false); - /// - /// Gets whether this model supports audio transcription, checking overrides first. - /// - [NotMapped] - public bool SupportsAudioTranscription => GetCapability(nameof(SupportsAudioTranscription), - () => Model?.Capabilities?.SupportsAudioTranscription ?? false); - - /// - /// Gets whether this model supports text-to-speech, checking overrides first. - /// - [NotMapped] - public bool SupportsTextToSpeech => GetCapability(nameof(SupportsTextToSpeech), - () => Model?.Capabilities?.SupportsTextToSpeech ?? false); - - /// - /// Gets whether this model supports realtime audio, checking overrides first. - /// - [NotMapped] - public bool SupportsRealtimeAudio => GetCapability(nameof(SupportsRealtimeAudio), - () => Model?.Capabilities?.SupportsRealtimeAudio ?? false); /// /// Gets whether this model supports image generation, checking overrides first. @@ -161,23 +141,6 @@ public class ModelProviderMapping [NotMapped] public TokenizerType? TokenizerType => Model?.Capabilities?.TokenizerType; - /// - /// Gets supported voices from Model.Capabilities. - /// - [NotMapped] - public string? SupportedVoices => Model?.Capabilities?.SupportedVoices; - - /// - /// Gets supported languages from Model.Capabilities. - /// - [NotMapped] - public string? SupportedLanguages => Model?.Capabilities?.SupportedLanguages; - - /// - /// Gets supported formats from Model.Capabilities. - /// - [NotMapped] - public string? SupportedFormats => Model?.Capabilities?.SupportedFormats; /// /// Indicates whether this is the default model for its provider and capability type. diff --git a/ConduitLLM.Configuration/Entities/RouterConfigEntity.cs b/ConduitLLM.Configuration/Entities/RouterConfigEntity.cs deleted file mode 100644 index 9cfd9e0d0..000000000 --- a/ConduitLLM.Configuration/Entities/RouterConfigEntity.cs +++ /dev/null @@ -1,78 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Configuration.Entities -{ - /// - /// Database entity representing router configuration for LLM routing - /// - public class RouterConfigEntity - { - /// - /// Primary key for the router configuration - /// - [Key] - public int Id { get; set; } - - /// - /// Default routing strategy to use when not explicitly specified - /// - [Required] - [MaxLength(50)] - public string DefaultRoutingStrategy { get; set; } = "simple"; - - /// - /// Maximum number of retries for a failed request - /// - public int MaxRetries { get; set; } = 3; - - /// - /// Base delay in milliseconds between retries (for exponential backoff) - /// - public int RetryBaseDelayMs { get; set; } = 500; - - /// - /// Maximum delay in milliseconds between retries - /// - public int RetryMaxDelayMs { get; set; } = 10000; - - /// - /// Whether fallbacks are enabled - /// - public bool FallbacksEnabled { get; set; } = false; - - /// - /// When the configuration was last updated - /// - public DateTime LastUpdated { get; set; } = DateTime.UtcNow; - - /// - /// Whether this configuration is active - /// - public bool IsActive { get; set; } = false; - - /// - /// Name of the configuration (for display purposes) - /// - public string Name { get; set; } = "Default Configuration"; - - /// - /// When this configuration was last updated (alias for LastUpdated) - /// - public DateTime UpdatedAt { get; set; } = DateTime.UtcNow; - - /// - /// When this configuration was created - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Model deployments associated with this configuration - /// - public virtual ICollection ModelDeployments { get; set; } = new List(); - - /// - /// Fallback configurations associated with this configuration - /// - public virtual ICollection FallbackConfigurations { get; set; } = new List(); - } -} diff --git a/ConduitLLM.Configuration/Enums/PricingModel.cs b/ConduitLLM.Configuration/Enums/PricingModel.cs index 46460d740..af6d52948 100644 --- a/ConduitLLM.Configuration/Enums/PricingModel.cs +++ b/ConduitLLM.Configuration/Enums/PricingModel.cs @@ -43,14 +43,16 @@ public enum PricingModel /// /// Audio pricing per minute of input/output. - /// Used by transcription and real-time audio models. + /// OBSOLETE - Audio functionality removed /// + [Obsolete("Audio functionality has been removed from the system")] PerMinuteAudio = 6, /// /// Audio pricing per thousand characters. - /// Used by text-to-speech models. + /// OBSOLETE - Audio functionality removed /// + [Obsolete("Audio functionality has been removed from the system")] PerThousandCharacters = 7 } } \ No newline at end of file diff --git a/ConduitLLM.Configuration/Enums/ProviderType.cs b/ConduitLLM.Configuration/Enums/ProviderType.cs index 98c2dc9ab..8cfbc7baa 100644 --- a/ConduitLLM.Configuration/Enums/ProviderType.cs +++ b/ConduitLLM.Configuration/Enums/ProviderType.cs @@ -39,13 +39,15 @@ public enum ProviderType MiniMax = 6, /// - /// Ultravox + /// Ultravox (OBSOLETE - Audio functionality removed) /// + [Obsolete("Audio functionality has been removed from the system")] Ultravox = 7, /// - /// ElevenLabs (audio) + /// ElevenLabs (OBSOLETE - Audio functionality removed) /// + [Obsolete("Audio functionality has been removed from the system")] ElevenLabs = 8, /// diff --git a/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs index a066735cb..33800876c 100644 --- a/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs +++ b/ConduitLLM.Configuration/Extensions/ServiceCollectionExtensions.cs @@ -46,17 +46,8 @@ public static IServiceCollection AddRepositories(this IServiceCollection service // Register new repositories services.AddScoped(); services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); services.AddScoped(); - // Register audio-related repositories - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - // Register async task repository services.AddScoped(); diff --git a/ConduitLLM.Configuration/Interfaces/IAudioCostRepository.cs b/ConduitLLM.Configuration/Interfaces/IAudioCostRepository.cs deleted file mode 100644 index 8019bdc80..000000000 --- a/ConduitLLM.Configuration/Interfaces/IAudioCostRepository.cs +++ /dev/null @@ -1,60 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for audio cost configurations. - /// - public interface IAudioCostRepository - { - /// - /// Gets all audio cost configurations. - /// - Task> GetAllAsync(); - - /// - /// Gets an audio cost configuration by ID. - /// - Task GetByIdAsync(int id); - - /// - /// Gets audio costs by provider. - /// - Task> GetByProviderAsync(int providerId); - - /// - /// Gets the current cost for a specific provider, operation, and model. - /// - Task GetCurrentCostAsync(int providerId, string operationType, string? model = null); - - /// - /// Gets costs effective at a specific date. - /// - Task> GetEffectiveAtDateAsync(DateTime date); - - /// - /// Creates a new audio cost configuration. - /// - Task CreateAsync(AudioCost cost); - - /// - /// Updates an existing audio cost configuration. - /// - Task UpdateAsync(AudioCost cost); - - /// - /// Deletes an audio cost configuration. - /// - Task DeleteAsync(int id); - - /// - /// Deactivates all costs for a provider and operation type. - /// - Task DeactivatePreviousCostsAsync(int providerId, string operationType, string? model = null); - - /// - /// Gets cost history for a provider and operation. - /// - Task> GetCostHistoryAsync(int providerId, string operationType, string? model = null); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IAudioProviderConfigRepository.cs b/ConduitLLM.Configuration/Interfaces/IAudioProviderConfigRepository.cs deleted file mode 100644 index ef83d577a..000000000 --- a/ConduitLLM.Configuration/Interfaces/IAudioProviderConfigRepository.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for audio provider configurations. - /// - public interface IAudioProviderConfigRepository - { - /// - /// Gets all audio provider configurations. - /// - Task> GetAllAsync(); - - /// - /// Gets an audio provider configuration by ID. - /// - Task GetByIdAsync(int id); - - /// - /// Gets audio provider configuration by provider credential ID. - /// - Task GetByProviderIdAsync(int ProviderId); - - /// - /// Gets audio provider configurations by provider type. - /// - Task> GetByProviderTypeAsync(ProviderType providerType); - - /// - /// Gets enabled audio provider configurations for a specific operation type. - /// - Task> GetEnabledForOperationAsync(string operationType); - - /// - /// Creates a new audio provider configuration. - /// - Task CreateAsync(AudioProviderConfig config); - - /// - /// Updates an existing audio provider configuration. - /// - Task UpdateAsync(AudioProviderConfig config); - - /// - /// Deletes an audio provider configuration. - /// - Task DeleteAsync(int id); - - /// - /// Checks if a provider credential already has audio configuration. - /// - Task ExistsForProviderAsync(int ProviderId); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IAudioUsageLogRepository.cs b/ConduitLLM.Configuration/Interfaces/IAudioUsageLogRepository.cs deleted file mode 100644 index a39670259..000000000 --- a/ConduitLLM.Configuration/Interfaces/IAudioUsageLogRepository.cs +++ /dev/null @@ -1,67 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for audio usage logging. - /// - public interface IAudioUsageLogRepository - { - /// - /// Creates a new audio usage log entry. - /// - Task CreateAsync(AudioUsageLog log); - - /// - /// Gets audio usage logs with pagination. - /// - Task> GetPagedAsync(AudioUsageQueryDto query); - - /// - /// Gets usage summary statistics. - /// - Task GetUsageSummaryAsync(DateTime startDate, DateTime endDate, string? virtualKey = null, int? providerId = null); - - /// - /// Gets usage by virtual key. - /// - Task> GetByVirtualKeyAsync(string virtualKey, DateTime? startDate = null, DateTime? endDate = null); - - /// - /// Gets usage by provider. - /// - Task> GetByProviderAsync(int providerId, DateTime? startDate = null, DateTime? endDate = null); - - /// - /// Gets usage by session ID. - /// - Task> GetBySessionIdAsync(string sessionId); - - /// - /// Gets total cost for a virtual key within a date range. - /// - Task GetTotalCostAsync(string virtualKey, DateTime startDate, DateTime endDate); - - /// - /// Gets operation type breakdown for analytics. - /// - Task> GetOperationBreakdownAsync(DateTime startDate, DateTime endDate, string? virtualKey = null); - - /// - /// Gets provider breakdown for analytics. - /// - Task> GetProviderBreakdownAsync(DateTime startDate, DateTime endDate, string? virtualKey = null); - - /// - /// Gets virtual key breakdown for analytics. - /// - Task> GetVirtualKeyBreakdownAsync(DateTime startDate, DateTime endDate, int? providerId = null); - - /// - /// Deletes old usage logs based on retention policy. - /// - Task DeleteOldLogsAsync(DateTime cutoffDate); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs b/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs index ec1a7e640..c627a5d96 100644 --- a/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs +++ b/ConduitLLM.Configuration/Interfaces/IConfigurationDbContext.cs @@ -77,25 +77,6 @@ public interface IConfigurationDbContext : IDisposable /// DbSet ProviderKeyCredentials { get; } - /// - /// Database set for router configurations - /// - DbSet RouterConfigurations { get; } - - /// - /// Database set for model deployments - /// - DbSet ModelDeployments { get; } - - /// - /// Database set for fallback configurations - /// - DbSet FallbackConfigurations { get; } - - /// - /// Database set for fallback model mappings - /// - DbSet FallbackModelMappings { get; } /// @@ -103,20 +84,6 @@ public interface IConfigurationDbContext : IDisposable /// DbSet IpFilters { get; } - /// - /// Database set for audio provider configurations - /// - DbSet AudioProviderConfigs { get; } - - /// - /// Database set for audio costs - /// - DbSet AudioCosts { get; } - - /// - /// Database set for audio usage logs - /// - DbSet AudioUsageLogs { get; } /// /// Database set for async tasks diff --git a/ConduitLLM.Configuration/Interfaces/IFallbackConfigurationRepository.cs b/ConduitLLM.Configuration/Interfaces/IFallbackConfigurationRepository.cs deleted file mode 100644 index fd08fb4b0..000000000 --- a/ConduitLLM.Configuration/Interfaces/IFallbackConfigurationRepository.cs +++ /dev/null @@ -1,72 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for managing fallback configurations - /// - public interface IFallbackConfigurationRepository - { - /// - /// Gets a fallback configuration by ID - /// - /// The fallback configuration ID - /// Cancellation token - /// The fallback configuration entity or null if not found - Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); - - /// - /// Gets the active fallback configuration - /// - /// Cancellation token - /// The active fallback configuration or null if none found - Task GetActiveConfigAsync(CancellationToken cancellationToken = default); - - /// - /// Gets all fallback configurations - /// - /// Cancellation token - /// A list of all fallback configurations - Task> GetAllAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a new fallback configuration - /// - /// The fallback configuration to create - /// Cancellation token - /// The ID of the created fallback configuration - Task CreateAsync(FallbackConfigurationEntity fallbackConfig, CancellationToken cancellationToken = default); - - /// - /// Updates a fallback configuration - /// - /// The fallback configuration to update - /// Cancellation token - /// True if the update was successful, false otherwise - Task UpdateAsync(FallbackConfigurationEntity fallbackConfig, CancellationToken cancellationToken = default); - - /// - /// Activates a fallback configuration and deactivates all others - /// - /// The ID of the fallback configuration to activate - /// Cancellation token - /// True if the activation was successful, false otherwise - Task ActivateAsync(Guid id, CancellationToken cancellationToken = default); - - /// - /// Deletes a fallback configuration - /// - /// The ID of the fallback configuration to delete - /// Cancellation token - /// True if the deletion was successful, false otherwise - Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); - - /// - /// Gets fallback model mappings for a fallback configuration - /// - /// The fallback configuration ID - /// Cancellation token - /// A list of fallback model mappings for the specified configuration - Task> GetMappingsAsync(Guid fallbackConfigId, CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IFallbackModelMappingRepository.cs b/ConduitLLM.Configuration/Interfaces/IFallbackModelMappingRepository.cs deleted file mode 100644 index 2a17d3faa..000000000 --- a/ConduitLLM.Configuration/Interfaces/IFallbackModelMappingRepository.cs +++ /dev/null @@ -1,71 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for managing fallback model mappings - /// - public interface IFallbackModelMappingRepository - { - /// - /// Gets a fallback model mapping by ID - /// - /// The fallback model mapping ID - /// Cancellation token - /// The fallback model mapping entity or null if not found - Task GetByIdAsync(int id, CancellationToken cancellationToken = default); - - /// - /// Gets a fallback model mapping by source model name within a fallback configuration - /// - /// The fallback configuration ID - /// The source model name - /// Cancellation token - /// The fallback model mapping entity or null if not found - Task GetBySourceModelAsync( - Guid fallbackConfigId, - string sourceModelName, - CancellationToken cancellationToken = default); - - /// - /// Gets all fallback model mappings for a fallback configuration - /// - /// The fallback configuration ID - /// Cancellation token - /// A list of fallback model mappings - Task> GetByFallbackConfigIdAsync( - Guid fallbackConfigId, - CancellationToken cancellationToken = default); - - /// - /// Gets all fallback model mappings - /// - /// Cancellation token - /// A list of all fallback model mappings - Task> GetAllAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a new fallback model mapping - /// - /// The fallback model mapping to create - /// Cancellation token - /// The ID of the created fallback model mapping - Task CreateAsync(FallbackModelMappingEntity fallbackModelMapping, CancellationToken cancellationToken = default); - - /// - /// Updates a fallback model mapping - /// - /// The fallback model mapping to update - /// Cancellation token - /// True if the update was successful, false otherwise - Task UpdateAsync(FallbackModelMappingEntity fallbackModelMapping, CancellationToken cancellationToken = default); - - /// - /// Deletes a fallback model mapping - /// - /// The ID of the fallback model mapping to delete - /// Cancellation token - /// True if the deletion was successful, false otherwise - Task DeleteAsync(int id, CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IModelDeploymentRepository.cs b/ConduitLLM.Configuration/Interfaces/IModelDeploymentRepository.cs deleted file mode 100644 index 9396a6487..000000000 --- a/ConduitLLM.Configuration/Interfaces/IModelDeploymentRepository.cs +++ /dev/null @@ -1,73 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for managing model deployments - /// - public interface IModelDeploymentRepository - { - /// - /// Gets a model deployment by ID - /// - /// The model deployment ID - /// Cancellation token - /// The model deployment entity or null if not found - Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); - - /// - /// Gets a model deployment by deployment name - /// - /// The deployment name - /// Cancellation token - /// The model deployment entity or null if not found - Task GetByDeploymentNameAsync(string deploymentName, CancellationToken cancellationToken = default); - - /// - /// Gets model deployments by provider - /// - /// The provider type - /// Cancellation token - /// A list of model deployments for the specified provider - Task> GetByProviderAsync(ProviderType providerType, CancellationToken cancellationToken = default); - - /// - /// Gets model deployments by model name - /// - /// The model name - /// Cancellation token - /// A list of model deployments for the specified model - Task> GetByModelNameAsync(string modelName, CancellationToken cancellationToken = default); - - /// - /// Gets all model deployments - /// - /// Cancellation token - /// A list of all model deployments - Task> GetAllAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a new model deployment - /// - /// The model deployment to create - /// Cancellation token - /// The ID of the created model deployment - Task CreateAsync(ModelDeploymentEntity modelDeployment, CancellationToken cancellationToken = default); - - /// - /// Updates a model deployment - /// - /// The model deployment to update - /// Cancellation token - /// True if the update was successful, false otherwise - Task UpdateAsync(ModelDeploymentEntity modelDeployment, CancellationToken cancellationToken = default); - - /// - /// Deletes a model deployment - /// - /// The ID of the model deployment to delete - /// Cancellation token - /// True if the deletion was successful, false otherwise - Task DeleteAsync(Guid id, CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Configuration/Interfaces/IRouterConfigRepository.cs b/ConduitLLM.Configuration/Interfaces/IRouterConfigRepository.cs deleted file mode 100644 index 1a46f3824..000000000 --- a/ConduitLLM.Configuration/Interfaces/IRouterConfigRepository.cs +++ /dev/null @@ -1,64 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Configuration.Interfaces -{ - /// - /// Repository interface for managing router configurations - /// - public interface IRouterConfigRepository - { - /// - /// Gets a router configuration by ID - /// - /// The router configuration ID - /// Cancellation token - /// The router configuration entity or null if not found - Task GetByIdAsync(int id, CancellationToken cancellationToken = default); - - /// - /// Gets the active router configuration - /// - /// Cancellation token - /// The active router configuration or null if none found - Task GetActiveConfigAsync(CancellationToken cancellationToken = default); - - /// - /// Gets all router configurations - /// - /// Cancellation token - /// A list of all router configurations - Task> GetAllAsync(CancellationToken cancellationToken = default); - - /// - /// Creates a new router configuration - /// - /// The router configuration to create - /// Cancellation token - /// The ID of the created router configuration - Task CreateAsync(RouterConfigEntity routerConfig, CancellationToken cancellationToken = default); - - /// - /// Updates a router configuration - /// - /// The router configuration to update - /// Cancellation token - /// True if the update was successful, false otherwise - Task UpdateAsync(RouterConfigEntity routerConfig, CancellationToken cancellationToken = default); - - /// - /// Activates a router configuration and deactivates all others - /// - /// The ID of the router configuration to activate - /// Cancellation token - /// True if the activation was successful, false otherwise - Task ActivateAsync(int id, CancellationToken cancellationToken = default); - - /// - /// Deletes a router configuration - /// - /// The ID of the router configuration to delete - /// Cancellation token - /// True if the deletion was successful, false otherwise - Task DeleteAsync(int id, CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.Designer.cs b/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.Designer.cs new file mode 100644 index 000000000..79d3adf04 --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.Designer.cs @@ -0,0 +1,2265 @@ +// +using System; +using ConduitLLM.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + [DbContext(typeof(ConduitDbContext))] + [Migration("20250829050036_RemoveRoutingSystem")] + partial class RemoveRoutingSystem + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("LeaseExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LeasedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("ProgressMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsArchived"); + + b.HasIndex("State"); + + b.HasIndex("Type"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("IsArchived", "ArchivedAt") + .HasDatabaseName("IX_AsyncTasks_Cleanup"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.HasIndex("IsArchived", "CompletedAt", "State") + .HasDatabaseName("IX_AsyncTasks_Archival"); + + b.ToTable("AsyncTasks"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AdditionalFactors") + .HasColumnType("text"); + + b.Property("CostPerUnit") + .HasColumnType("decimal(10, 6)"); + + b.Property("CostUnit") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveFrom") + .HasColumnType("timestamp with time zone"); + + b.Property("EffectiveTo") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MinimumCharge") + .HasColumnType("decimal(10, 6)"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("EffectiveFrom", "EffectiveTo"); + + b.HasIndex("ProviderId", "OperationType", "Model", "IsActive"); + + b.ToTable("AudioCosts"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioProviderConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CustomSettings") + .HasColumnType("text"); + + b.Property("DefaultRealtimeModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTTSModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTTSVoice") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTranscriptionModel") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("RealtimeEnabled") + .HasColumnType("boolean"); + + b.Property("RealtimeEndpoint") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("RoutingPriority") + .HasColumnType("integer"); + + b.Property("TextToSpeechEnabled") + .HasColumnType("boolean"); + + b.Property("TranscriptionEnabled") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId") + .IsUnique(); + + b.ToTable("AudioProviderConfigs"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioUsageLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CharacterCount") + .HasColumnType("integer"); + + b.Property("Cost") + .HasColumnType("decimal(10, 6)"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("ErrorMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InputTokens") + .HasColumnType("integer"); + + b.Property("IpAddress") + .HasMaxLength(45) + .HasColumnType("character varying(45)"); + + b.Property("Language") + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OutputTokens") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("RequestId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("SessionId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserAgent") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("VirtualKey") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Voice") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("SessionId"); + + b.HasIndex("Timestamp"); + + b.HasIndex("VirtualKey"); + + b.HasIndex("ProviderId", "OperationType"); + + b.ToTable("AudioUsageLogs"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.Property("OperationId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CanResume") + .HasColumnType("boolean"); + + b.Property("CancellationReason") + .HasColumnType("text"); + + b.Property("CheckpointData") + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("ErrorDetails") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailedCount") + .HasColumnType("integer"); + + b.Property("ItemsPerSecond") + .HasColumnType("double precision"); + + b.Property("LastProcessedIndex") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResultSummary") + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SuccessCount") + .HasColumnType("integer"); + + b.Property("TotalItems") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("OperationId"); + + b.HasIndex("OperationType"); + + b.HasIndex("StartedAt"); + + b.HasIndex("Status"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "StartedAt"); + + b.HasIndex("OperationType", "Status", "StartedAt"); + + b.ToTable("BatchOperationHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CalculatedCost") + .HasColumnType("decimal(10, 6)"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HttpStatusCode") + .HasColumnType("integer"); + + b.Property("IsEstimated") + .HasColumnType("boolean"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UsageJson") + .HasColumnType("jsonb"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EventType") + .HasDatabaseName("IX_BillingAuditEvents_EventType"); + + b.HasIndex("RequestId") + .HasDatabaseName("IX_BillingAuditEvents_RequestId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_Timestamp"); + + b.HasIndex("VirtualKeyId") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId"); + + b.HasIndex("EventType", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_EventType_Timestamp"); + + b.HasIndex("VirtualKeyId", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId_Timestamp"); + + b.ToTable("BillingAuditEvents", (string)null); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompressionThresholdBytes") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTtlSeconds") + .HasColumnType("integer"); + + b.Property("EnableCompression") + .HasColumnType("boolean"); + + b.Property("EnableDetailedStats") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("EvictionPolicy") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExtendedConfig") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxEntries") + .HasColumnType("bigint"); + + b.Property("MaxMemoryBytes") + .HasColumnType("bigint"); + + b.Property("MaxTtlSeconds") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UseDistributedCache") + .HasColumnType("boolean"); + + b.Property("UseMemoryCache") + .HasColumnType("boolean"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Region") + .IsUnique() + .HasFilter("\"IsActive\" = true"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("Region", "IsActive"); + + b.ToTable("CacheConfigurations"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfigurationAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangeSource") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("NewConfigJson") + .HasColumnType("text"); + + b.Property("OldConfigJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ChangedAt"); + + b.HasIndex("ChangedBy"); + + b.HasIndex("Region"); + + b.HasIndex("Region", "ChangedAt"); + + b.ToTable("CacheConfigurationAudits"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("PrimaryModelDeploymentId") + .HasColumnType("uuid"); + + b.Property("RouterConfigId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("PrimaryModelDeploymentId"); + + b.HasIndex("RouterConfigId"); + + b.ToTable("FallbackConfigurations"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackModelMappingEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("FallbackConfigurationId") + .HasColumnType("uuid"); + + b.Property("ModelDeploymentId") + .HasColumnType("uuid"); + + b.Property("Order") + .HasColumnType("integer"); + + b.Property("SourceModelName") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("FallbackConfigurationId", "ModelDeploymentId") + .IsUnique(); + + b.HasIndex("FallbackConfigurationId", "Order") + .IsUnique(); + + b.ToTable("FallbackModelMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.GlobalSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.IpFilterEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FilterType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IpAddressOrCidr") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("FilterType", "IpAddressOrCidr"); + + b.ToTable("IpFilters"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastAccessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Prompt") + .HasColumnType("text"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicUrl") + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("StorageUrl") + .HasColumnType("text"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("StorageKey") + .IsUnique(); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.ToTable("MediaRecords"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsProTier") + .HasColumnType("boolean"); + + b.Property("MaxFileCount") + .HasColumnType("integer"); + + b.Property("MaxStorageSizeBytes") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NegativeBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("PositiveBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("RecentAccessWindowDays") + .HasColumnType("integer"); + + b.Property("RespectRecentAccess") + .HasColumnType("boolean"); + + b.Property("SoftDeleteGracePeriodDays") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZeroBalanceRetentionDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsDefault") + .IsUnique() + .HasFilter("\"IsDefault\" = true"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaRetentionPolicies"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCapabilitiesId") + .HasColumnType("integer"); + + b.Property("ModelCardUrl") + .HasColumnType("text"); + + b.Property("ModelParameters") + .HasColumnType("text") + .HasColumnName("Parameters"); + + b.Property("ModelSeriesId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ModelCapabilitiesId") + .HasDatabaseName("IX_Model_ModelCapabilitiesId"); + + b.HasIndex("ModelSeriesId") + .HasDatabaseName("IX_Model_ModelSeriesId"); + + b.ToTable("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WebsiteUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_ModelAuthor_Name_Unique"); + + b.ToTable("ModelAuthors"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCapabilities", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("MinTokens") + .HasColumnType("integer"); + + b.Property("SupportedFormats") + .HasColumnType("text"); + + b.Property("SupportedLanguages") + .HasColumnType("text"); + + b.Property("SupportedVoices") + .HasColumnType("text"); + + b.Property("SupportsAudioTranscription") + .HasColumnType("boolean"); + + b.Property("SupportsChat") + .HasColumnType("boolean"); + + b.Property("SupportsEmbeddings") + .HasColumnType("boolean"); + + b.Property("SupportsFunctionCalling") + .HasColumnType("boolean"); + + b.Property("SupportsImageGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsRealtimeAudio") + .HasColumnType("boolean"); + + b.Property("SupportsStreaming") + .HasColumnType("boolean"); + + b.Property("SupportsTextToSpeech") + .HasColumnType("boolean"); + + b.Property("SupportsVideoGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsVision") + .HasColumnType("boolean"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SupportsChat") + .HasDatabaseName("IX_ModelCapabilities_SupportsChat") + .HasFilter("\"SupportsChat\" = true"); + + b.HasIndex("SupportsFunctionCalling") + .HasDatabaseName("IX_ModelCapabilities_SupportsFunctionCalling") + .HasFilter("\"SupportsFunctionCalling\" = true"); + + b.HasIndex("SupportsImageGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsImageGeneration") + .HasFilter("\"SupportsImageGeneration\" = true"); + + b.HasIndex("SupportsVideoGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsVideoGeneration") + .HasFilter("\"SupportsVideoGeneration\" = true"); + + b.HasIndex("SupportsVision") + .HasDatabaseName("IX_ModelCapabilities_SupportsVision") + .HasFilter("\"SupportsVision\" = true"); + + b.HasIndex("SupportsChat", "SupportsFunctionCalling", "SupportsStreaming") + .HasDatabaseName("IX_ModelCapabilities_Chat_Function_Streaming"); + + b.ToTable("ModelCapabilities"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AudioCostPerKCharacters") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioInputCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioOutputCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("BatchProcessingMultiplier") + .HasColumnType("decimal(18, 4)"); + + b.Property("CachedInputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CachedInputWriteCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CostName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CostPerInferenceStep") + .HasColumnType("decimal(18, 8)"); + + b.Property("CostPerSearchUnit") + .HasColumnType("decimal(18, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultInferenceSteps") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EffectiveDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddingCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageCostPerImage") + .HasColumnType("decimal(18, 4)"); + + b.Property("ImageQualityMultipliers") + .HasColumnType("text"); + + b.Property("ImageResolutionMultipliers") + .HasColumnType("text"); + + b.Property("InputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OutputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("PricingConfiguration") + .HasColumnType("text"); + + b.Property("PricingModel") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("SupportsBatchProcessing") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VideoCostPerSecond") + .HasColumnType("decimal(18, 4)"); + + b.Property("VideoResolutionMultipliers") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CostName"); + + b.ToTable("ModelCosts"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCostId") + .HasColumnType("integer"); + + b.Property("ModelProviderMappingId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ModelProviderMappingId"); + + b.HasIndex("ModelCostId", "ModelProviderMappingId") + .IsUnique(); + + b.ToTable("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelDeploymentEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeploymentName") + .IsRequired() + .HasColumnType("text"); + + b.Property("HealthCheckEnabled") + .HasColumnType("boolean"); + + b.Property("InputTokenCostPer1K") + .HasColumnType("decimal(18, 8)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsHealthy") + .HasColumnType("boolean"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OutputTokenCostPer1K") + .HasColumnType("decimal(18, 8)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("RPM") + .HasColumnType("integer"); + + b.Property("RouterConfigId") + .HasColumnType("integer"); + + b.Property("SupportsEmbeddings") + .HasColumnType("boolean"); + + b.Property("TPM") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Weight") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("IsHealthy"); + + b.HasIndex("ModelName"); + + b.HasIndex("ProviderId"); + + b.HasIndex("RouterConfigId"); + + b.ToTable("ModelDeployments"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasDatabaseName("IX_ModelIdentifier_Identifier"); + + b.HasIndex("IsPrimary") + .HasDatabaseName("IX_ModelIdentifier_IsPrimary") + .HasFilter("\"IsPrimary\" = true"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelIdentifier_ModelId"); + + b.HasIndex("Provider", "Identifier") + .IsUnique() + .HasDatabaseName("IX_ModelIdentifier_Provider_Identifier_Unique"); + + b.ToTable("ModelIdentifiers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapabilityOverrides") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCapabilityType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MaxContextTokensOverride") + .HasColumnType("integer"); + + b.Property("ModelAlias") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("ProviderModelId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderVariation") + .HasColumnType("text"); + + b.Property("QualityScore") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CapabilityOverrides") + .HasDatabaseName("IX_ModelProviderMapping_CapabilityOverrides") + .HasFilter("\"CapabilityOverrides\" IS NOT NULL"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelProviderMapping_ModelId"); + + b.HasIndex("ModelAlias", "ProviderId") + .IsUnique(); + + b.HasIndex("ModelId", "QualityScore") + .HasDatabaseName("IX_ModelProviderMapping_ModelId_QualityScore") + .HasFilter("\"QualityScore\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsEnabled") + .HasDatabaseName("IX_ModelProviderMapping_ProviderId_IsEnabled") + .HasFilter("\"IsEnabled\" = true"); + + b.ToTable("ModelProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId") + .HasDatabaseName("IX_ModelSeries_AuthorId"); + + b.HasIndex("TokenizerType") + .HasDatabaseName("IX_ModelSeries_TokenizerType"); + + b.HasIndex("AuthorId", "Name") + .IsUnique() + .HasDatabaseName("IX_ModelSeries_AuthorId_Name_Unique"); + + b.ToTable("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderType"); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("KeyName") + .HasColumnType("text"); + + b.Property("Organization") + .HasColumnType("text"); + + b.Property("ProviderAccountGroup") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId") + .HasDatabaseName("IX_ProviderKeyCredential_ProviderId"); + + b.HasIndex("ProviderId", "ApiKey") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_UniqueApiKeyPerProvider") + .HasFilter("\"ApiKey\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsPrimary") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_OnePrimaryPerProvider") + .HasFilter("\"IsPrimary\" = true"); + + b.ToTable("ProviderKeyCredentials", t => + { + t.HasCheckConstraint("CK_ProviderKeyCredential_AccountGroupRange", "\"ProviderAccountGroup\" >= 0 AND \"ProviderAccountGroup\" <= 32"); + + t.HasCheckConstraint("CK_ProviderKeyCredential_PrimaryMustBeEnabled", "\"IsPrimary\" = false OR \"IsEnabled\" = true"); + }); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientIp") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Cost") + .HasColumnType("decimal(10, 6)"); + + b.Property("InputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OutputTokens") + .HasColumnType("integer"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseTimeMs") + .HasColumnType("double precision"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RouterConfigEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultRoutingStrategy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("FallbacksEnabled") + .HasColumnType("boolean"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("LastUpdated") + .HasColumnType("timestamp with time zone"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("RetryBaseDelayMs") + .HasColumnType("integer"); + + b.Property("RetryMaxDelayMs") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("LastUpdated"); + + b.ToTable("RouterConfigEntity"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedModels") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("RateLimitRpd") + .HasColumnType("integer"); + + b.Property("RateLimitRpm") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("VirtualKeyGroupId"); + + b.ToTable("VirtualKeys"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(19, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalGroupId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LifetimeCreditsAdded") + .HasColumnType("decimal(19, 8)"); + + b.Property("LifetimeSpent") + .HasColumnType("decimal(19, 8)"); + + b.Property("MediaRetentionPolicyId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalGroupId"); + + b.HasIndex("MediaRetentionPolicyId"); + + b.ToTable("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18, 6)"); + + b.Property("BalanceAfter") + .HasColumnType("decimal(18, 6)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InitiatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InitiatedByUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ReferenceType") + .HasColumnType("integer"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ReferenceType"); + + b.HasIndex("TransactionType"); + + b.HasIndex("VirtualKeyGroupId"); + + b.HasIndex("IsDeleted", "CreatedAt"); + + b.HasIndex("VirtualKeyGroupId", "CreatedAt"); + + b.ToTable("VirtualKeyGroupTransactions"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(10, 6)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("VirtualKeySpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioCost", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioProviderConfig", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithOne() + .HasForeignKey("ConduitLLM.Configuration.Entities.AudioProviderConfig", "ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioUsageLog", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.RouterConfigEntity", "RouterConfig") + .WithMany("FallbackConfigurations") + .HasForeignKey("RouterConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("RouterConfig"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackModelMappingEntity", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", "FallbackConfiguration") + .WithMany("FallbackMappings") + .HasForeignKey("FallbackConfigurationId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("FallbackConfiguration"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCapabilities", "Capabilities") + .WithMany() + .HasForeignKey("ModelCapabilitiesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelSeries", "Series") + .WithMany("Models") + .HasForeignKey("ModelSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Capabilities"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCost", "ModelCost") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelCostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelProviderMapping", "ModelProviderMapping") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelProviderMappingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelCost"); + + b.Navigation("ModelProviderMapping"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelDeploymentEntity", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.RouterConfigEntity", "RouterConfig") + .WithMany("ModelDeployments") + .HasForeignKey("RouterConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + + b.Navigation("RouterConfig"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("Identifiers") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("ProviderMappings") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelAuthor", "Author") + .WithMany("ModelSeries") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("Notifications") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany("ProviderKeyCredentials") + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("RequestLogs") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("VirtualKeys") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", "MediaRetentionPolicy") + .WithMany("VirtualKeyGroups") + .HasForeignKey("MediaRetentionPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaRetentionPolicy"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("Transactions") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("SpendHistory") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => + { + b.Navigation("FallbackMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Navigation("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Navigation("Identifiers"); + + b.Navigation("ProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Navigation("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Navigation("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Navigation("ProviderKeyCredentials"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RouterConfigEntity", b => + { + b.Navigation("FallbackConfigurations"); + + b.Navigation("ModelDeployments"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Navigation("Notifications"); + + b.Navigation("RequestLogs"); + + b.Navigation("SpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Navigation("Transactions"); + + b.Navigation("VirtualKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.cs b/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.cs new file mode 100644 index 000000000..2d54e41d5 --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829050036_RemoveRoutingSystem.cs @@ -0,0 +1,213 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + /// + public partial class RemoveRoutingSystem : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Drop foreign key constraints first + // Check if tables exist before dropping constraints + migrationBuilder.Sql(@" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'FallbackModelMappings') THEN + ALTER TABLE ""FallbackModelMappings"" DROP CONSTRAINT IF EXISTS ""FK_FallbackModelMappings_FallbackConfigurations_FallbackConfi~""; + END IF; + END $$; + "); + + migrationBuilder.Sql(@" + DO $$ + BEGIN + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'FallbackConfigurations') THEN + ALTER TABLE ""FallbackConfigurations"" DROP CONSTRAINT IF EXISTS ""FK_FallbackConfigurations_RouterConfigurations_RouterConfigId""; + END IF; + IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_name = 'ModelDeployments') THEN + ALTER TABLE ""ModelDeployments"" DROP CONSTRAINT IF EXISTS ""FK_ModelDeployments_Providers_ProviderId""; + ALTER TABLE ""ModelDeployments"" DROP CONSTRAINT IF EXISTS ""FK_ModelDeployments_RouterConfigurations_RouterConfigId""; + END IF; + END $$; + "); + + // Drop tables in correct order (respecting dependencies) + migrationBuilder.Sql("DROP TABLE IF EXISTS \"FallbackModelMappings\";"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"FallbackConfigurations\";"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"ModelDeployments\";"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"RouterConfigurations\";"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Recreate RouterConfigurations table + migrationBuilder.CreateTable( + name: "RouterConfigurations", + columns: table => new + { + Id = table.Column(type: "integer", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", Npgsql.EntityFrameworkCore.PostgreSQL.Metadata.NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + DefaultRoutingStrategy = table.Column(type: "character varying(50)", maxLength: 50, nullable: false), + MaxRetries = table.Column(type: "integer", nullable: false), + RetryBaseDelayMs = table.Column(type: "integer", nullable: false), + RetryMaxDelayMs = table.Column(type: "integer", nullable: false), + FallbacksEnabled = table.Column(type: "boolean", nullable: false), + LastUpdated = table.Column(type: "timestamp with time zone", nullable: false), + IsActive = table.Column(type: "boolean", nullable: false), + Name = table.Column(type: "text", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_RouterConfigurations", x => x.Id); + }); + + // Recreate ModelDeployments table + migrationBuilder.CreateTable( + name: "ModelDeployments", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + ModelName = table.Column(type: "character varying(100)", maxLength: 100, nullable: false), + ProviderId = table.Column(type: "integer", nullable: false), + Weight = table.Column(type: "integer", nullable: false), + HealthCheckEnabled = table.Column(type: "boolean", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + RPM = table.Column(type: "integer", nullable: true), + TPM = table.Column(type: "integer", nullable: true), + InputTokenCostPer1K = table.Column(type: "numeric(18,8)", nullable: true), + OutputTokenCostPer1K = table.Column(type: "numeric(18,8)", nullable: true), + Priority = table.Column(type: "integer", nullable: false), + IsHealthy = table.Column(type: "boolean", nullable: false), + RouterConfigId = table.Column(type: "integer", nullable: false), + LastUpdated = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + DeploymentName = table.Column(type: "text", nullable: false), + SupportsEmbeddings = table.Column(type: "boolean", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_ModelDeployments", x => x.Id); + table.ForeignKey( + name: "FK_ModelDeployments_Providers_ProviderId", + column: x => x.ProviderId, + principalTable: "Providers", + principalColumn: "Id", + onDelete: ReferentialAction.Restrict); + table.ForeignKey( + name: "FK_ModelDeployments_RouterConfigurations_RouterConfigId", + column: x => x.RouterConfigId, + principalTable: "RouterConfigurations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + // Recreate FallbackConfigurations table + migrationBuilder.CreateTable( + name: "FallbackConfigurations", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + PrimaryModelDeploymentId = table.Column(type: "uuid", nullable: false), + RouterConfigId = table.Column(type: "integer", nullable: false), + IsEnabled = table.Column(type: "boolean", nullable: false), + LastUpdated = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FallbackConfigurations", x => x.Id); + table.ForeignKey( + name: "FK_FallbackConfigurations_RouterConfigurations_RouterConfigId", + column: x => x.RouterConfigId, + principalTable: "RouterConfigurations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + // Recreate FallbackModelMappings table + migrationBuilder.CreateTable( + name: "FallbackModelMappings", + columns: table => new + { + Id = table.Column(type: "uuid", nullable: false), + FallbackConfigurationId = table.Column(type: "uuid", nullable: false), + ModelDeploymentId = table.Column(type: "uuid", nullable: false), + Order = table.Column(type: "integer", nullable: false), + LastUpdated = table.Column(type: "timestamp with time zone", nullable: false), + UpdatedAt = table.Column(type: "timestamp with time zone", nullable: false), + CreatedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_FallbackModelMappings", x => x.Id); + table.ForeignKey( + name: "FK_FallbackModelMappings_FallbackConfigurations_FallbackConfi~", + column: x => x.FallbackConfigurationId, + principalTable: "FallbackConfigurations", + principalColumn: "Id", + onDelete: ReferentialAction.Cascade); + }); + + // Create indexes + migrationBuilder.CreateIndex( + name: "IX_RouterConfigurations_LastUpdated", + table: "RouterConfigurations", + column: "LastUpdated"); + + migrationBuilder.CreateIndex( + name: "IX_ModelDeployments_ModelName", + table: "ModelDeployments", + column: "ModelName"); + + migrationBuilder.CreateIndex( + name: "IX_ModelDeployments_ProviderId", + table: "ModelDeployments", + column: "ProviderId"); + + migrationBuilder.CreateIndex( + name: "IX_ModelDeployments_IsEnabled", + table: "ModelDeployments", + column: "IsEnabled"); + + migrationBuilder.CreateIndex( + name: "IX_ModelDeployments_IsHealthy", + table: "ModelDeployments", + column: "IsHealthy"); + + migrationBuilder.CreateIndex( + name: "IX_ModelDeployments_RouterConfigId", + table: "ModelDeployments", + column: "RouterConfigId"); + + migrationBuilder.CreateIndex( + name: "IX_FallbackConfigurations_PrimaryModelDeploymentId", + table: "FallbackConfigurations", + column: "PrimaryModelDeploymentId"); + + migrationBuilder.CreateIndex( + name: "IX_FallbackConfigurations_RouterConfigId", + table: "FallbackConfigurations", + column: "RouterConfigId"); + + migrationBuilder.CreateIndex( + name: "IX_FallbackModelMappings_FallbackConfigurationId_Order", + table: "FallbackModelMappings", + columns: new[] { "FallbackConfigurationId", "Order" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_FallbackModelMappings_FallbackConfigurationId_ModelDeploym~", + table: "FallbackModelMappings", + columns: new[] { "FallbackConfigurationId", "ModelDeploymentId" }, + unique: true); + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.Designer.cs b/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.Designer.cs new file mode 100644 index 000000000..3fe0444dd --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.Designer.cs @@ -0,0 +1,1773 @@ +// +using System; +using ConduitLLM.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + [DbContext(typeof(ConduitDbContext))] + [Migration("20250829061847_RemoveAudioColumns")] + partial class RemoveAudioColumns + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("LeaseExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LeasedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("ProgressMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsArchived"); + + b.HasIndex("State"); + + b.HasIndex("Type"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("IsArchived", "ArchivedAt") + .HasDatabaseName("IX_AsyncTasks_Cleanup"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.HasIndex("IsArchived", "CompletedAt", "State") + .HasDatabaseName("IX_AsyncTasks_Archival"); + + b.ToTable("AsyncTasks"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.Property("OperationId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CanResume") + .HasColumnType("boolean"); + + b.Property("CancellationReason") + .HasColumnType("text"); + + b.Property("CheckpointData") + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("ErrorDetails") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailedCount") + .HasColumnType("integer"); + + b.Property("ItemsPerSecond") + .HasColumnType("double precision"); + + b.Property("LastProcessedIndex") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResultSummary") + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SuccessCount") + .HasColumnType("integer"); + + b.Property("TotalItems") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("OperationId"); + + b.HasIndex("OperationType"); + + b.HasIndex("StartedAt"); + + b.HasIndex("Status"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "StartedAt"); + + b.HasIndex("OperationType", "Status", "StartedAt"); + + b.ToTable("BatchOperationHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CalculatedCost") + .HasColumnType("decimal(10, 6)"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HttpStatusCode") + .HasColumnType("integer"); + + b.Property("IsEstimated") + .HasColumnType("boolean"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UsageJson") + .HasColumnType("jsonb"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EventType") + .HasDatabaseName("IX_BillingAuditEvents_EventType"); + + b.HasIndex("RequestId") + .HasDatabaseName("IX_BillingAuditEvents_RequestId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_Timestamp"); + + b.HasIndex("VirtualKeyId") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId"); + + b.HasIndex("EventType", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_EventType_Timestamp"); + + b.HasIndex("VirtualKeyId", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId_Timestamp"); + + b.ToTable("BillingAuditEvents", (string)null); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompressionThresholdBytes") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTtlSeconds") + .HasColumnType("integer"); + + b.Property("EnableCompression") + .HasColumnType("boolean"); + + b.Property("EnableDetailedStats") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("EvictionPolicy") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExtendedConfig") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxEntries") + .HasColumnType("bigint"); + + b.Property("MaxMemoryBytes") + .HasColumnType("bigint"); + + b.Property("MaxTtlSeconds") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UseDistributedCache") + .HasColumnType("boolean"); + + b.Property("UseMemoryCache") + .HasColumnType("boolean"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Region") + .IsUnique() + .HasFilter("\"IsActive\" = true"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("Region", "IsActive"); + + b.ToTable("CacheConfigurations"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfigurationAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangeSource") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("NewConfigJson") + .HasColumnType("text"); + + b.Property("OldConfigJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ChangedAt"); + + b.HasIndex("ChangedBy"); + + b.HasIndex("Region"); + + b.HasIndex("Region", "ChangedAt"); + + b.ToTable("CacheConfigurationAudits"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.GlobalSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.IpFilterEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FilterType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IpAddressOrCidr") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("FilterType", "IpAddressOrCidr"); + + b.ToTable("IpFilters"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastAccessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Prompt") + .HasColumnType("text"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicUrl") + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("StorageUrl") + .HasColumnType("text"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("StorageKey") + .IsUnique(); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.ToTable("MediaRecords"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsProTier") + .HasColumnType("boolean"); + + b.Property("MaxFileCount") + .HasColumnType("integer"); + + b.Property("MaxStorageSizeBytes") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NegativeBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("PositiveBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("RecentAccessWindowDays") + .HasColumnType("integer"); + + b.Property("RespectRecentAccess") + .HasColumnType("boolean"); + + b.Property("SoftDeleteGracePeriodDays") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZeroBalanceRetentionDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsDefault") + .IsUnique() + .HasFilter("\"IsDefault\" = true"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaRetentionPolicies"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCapabilitiesId") + .HasColumnType("integer"); + + b.Property("ModelCardUrl") + .HasColumnType("text"); + + b.Property("ModelParameters") + .HasColumnType("text") + .HasColumnName("Parameters"); + + b.Property("ModelSeriesId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ModelCapabilitiesId") + .HasDatabaseName("IX_Model_ModelCapabilitiesId"); + + b.HasIndex("ModelSeriesId") + .HasDatabaseName("IX_Model_ModelSeriesId"); + + b.ToTable("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WebsiteUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_ModelAuthor_Name_Unique"); + + b.ToTable("ModelAuthors"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCapabilities", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("MinTokens") + .HasColumnType("integer"); + + b.Property("SupportedFormats") + .HasColumnType("text"); + + b.Property("SupportedLanguages") + .HasColumnType("text"); + + b.Property("SupportedVoices") + .HasColumnType("text"); + + b.Property("SupportsAudioTranscription") + .HasColumnType("boolean"); + + b.Property("SupportsChat") + .HasColumnType("boolean"); + + b.Property("SupportsEmbeddings") + .HasColumnType("boolean"); + + b.Property("SupportsFunctionCalling") + .HasColumnType("boolean"); + + b.Property("SupportsImageGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsRealtimeAudio") + .HasColumnType("boolean"); + + b.Property("SupportsStreaming") + .HasColumnType("boolean"); + + b.Property("SupportsTextToSpeech") + .HasColumnType("boolean"); + + b.Property("SupportsVideoGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsVision") + .HasColumnType("boolean"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SupportsChat") + .HasDatabaseName("IX_ModelCapabilities_SupportsChat") + .HasFilter("\"SupportsChat\" = true"); + + b.HasIndex("SupportsFunctionCalling") + .HasDatabaseName("IX_ModelCapabilities_SupportsFunctionCalling") + .HasFilter("\"SupportsFunctionCalling\" = true"); + + b.HasIndex("SupportsImageGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsImageGeneration") + .HasFilter("\"SupportsImageGeneration\" = true"); + + b.HasIndex("SupportsVideoGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsVideoGeneration") + .HasFilter("\"SupportsVideoGeneration\" = true"); + + b.HasIndex("SupportsVision") + .HasDatabaseName("IX_ModelCapabilities_SupportsVision") + .HasFilter("\"SupportsVision\" = true"); + + b.HasIndex("SupportsChat", "SupportsFunctionCalling", "SupportsStreaming") + .HasDatabaseName("IX_ModelCapabilities_Chat_Function_Streaming"); + + b.ToTable("ModelCapabilities"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AudioCostPerKCharacters") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioInputCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("AudioOutputCostPerMinute") + .HasColumnType("decimal(18, 4)"); + + b.Property("BatchProcessingMultiplier") + .HasColumnType("decimal(18, 4)"); + + b.Property("CachedInputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CachedInputWriteCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CostName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CostPerInferenceStep") + .HasColumnType("decimal(18, 8)"); + + b.Property("CostPerSearchUnit") + .HasColumnType("decimal(18, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultInferenceSteps") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EffectiveDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddingCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageCostPerImage") + .HasColumnType("decimal(18, 4)"); + + b.Property("ImageQualityMultipliers") + .HasColumnType("text"); + + b.Property("ImageResolutionMultipliers") + .HasColumnType("text"); + + b.Property("InputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OutputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("PricingConfiguration") + .HasColumnType("text"); + + b.Property("PricingModel") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("SupportsBatchProcessing") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VideoCostPerSecond") + .HasColumnType("decimal(18, 4)"); + + b.Property("VideoResolutionMultipliers") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CostName"); + + b.ToTable("ModelCosts"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCostId") + .HasColumnType("integer"); + + b.Property("ModelProviderMappingId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ModelProviderMappingId"); + + b.HasIndex("ModelCostId", "ModelProviderMappingId") + .IsUnique(); + + b.ToTable("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasDatabaseName("IX_ModelIdentifier_Identifier"); + + b.HasIndex("IsPrimary") + .HasDatabaseName("IX_ModelIdentifier_IsPrimary") + .HasFilter("\"IsPrimary\" = true"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelIdentifier_ModelId"); + + b.HasIndex("Provider", "Identifier") + .IsUnique() + .HasDatabaseName("IX_ModelIdentifier_Provider_Identifier_Unique"); + + b.ToTable("ModelIdentifiers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapabilityOverrides") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCapabilityType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MaxContextTokensOverride") + .HasColumnType("integer"); + + b.Property("ModelAlias") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("ProviderModelId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderVariation") + .HasColumnType("text"); + + b.Property("QualityScore") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CapabilityOverrides") + .HasDatabaseName("IX_ModelProviderMapping_CapabilityOverrides") + .HasFilter("\"CapabilityOverrides\" IS NOT NULL"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelProviderMapping_ModelId"); + + b.HasIndex("ModelAlias", "ProviderId") + .IsUnique(); + + b.HasIndex("ModelId", "QualityScore") + .HasDatabaseName("IX_ModelProviderMapping_ModelId_QualityScore") + .HasFilter("\"QualityScore\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsEnabled") + .HasDatabaseName("IX_ModelProviderMapping_ProviderId_IsEnabled") + .HasFilter("\"IsEnabled\" = true"); + + b.ToTable("ModelProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId") + .HasDatabaseName("IX_ModelSeries_AuthorId"); + + b.HasIndex("TokenizerType") + .HasDatabaseName("IX_ModelSeries_TokenizerType"); + + b.HasIndex("AuthorId", "Name") + .IsUnique() + .HasDatabaseName("IX_ModelSeries_AuthorId_Name_Unique"); + + b.ToTable("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderType"); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("KeyName") + .HasColumnType("text"); + + b.Property("Organization") + .HasColumnType("text"); + + b.Property("ProviderAccountGroup") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId") + .HasDatabaseName("IX_ProviderKeyCredential_ProviderId"); + + b.HasIndex("ProviderId", "ApiKey") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_UniqueApiKeyPerProvider") + .HasFilter("\"ApiKey\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsPrimary") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_OnePrimaryPerProvider") + .HasFilter("\"IsPrimary\" = true"); + + b.ToTable("ProviderKeyCredentials", t => + { + t.HasCheckConstraint("CK_ProviderKeyCredential_AccountGroupRange", "\"ProviderAccountGroup\" >= 0 AND \"ProviderAccountGroup\" <= 32"); + + t.HasCheckConstraint("CK_ProviderKeyCredential_PrimaryMustBeEnabled", "\"IsPrimary\" = false OR \"IsEnabled\" = true"); + }); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientIp") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Cost") + .HasColumnType("decimal(10, 6)"); + + b.Property("InputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OutputTokens") + .HasColumnType("integer"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseTimeMs") + .HasColumnType("double precision"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedModels") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("RateLimitRpd") + .HasColumnType("integer"); + + b.Property("RateLimitRpm") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("VirtualKeyGroupId"); + + b.ToTable("VirtualKeys"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(19, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalGroupId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LifetimeCreditsAdded") + .HasColumnType("decimal(19, 8)"); + + b.Property("LifetimeSpent") + .HasColumnType("decimal(19, 8)"); + + b.Property("MediaRetentionPolicyId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalGroupId"); + + b.HasIndex("MediaRetentionPolicyId"); + + b.ToTable("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18, 6)"); + + b.Property("BalanceAfter") + .HasColumnType("decimal(18, 6)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InitiatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InitiatedByUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ReferenceType") + .HasColumnType("integer"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ReferenceType"); + + b.HasIndex("TransactionType"); + + b.HasIndex("VirtualKeyGroupId"); + + b.HasIndex("IsDeleted", "CreatedAt"); + + b.HasIndex("VirtualKeyGroupId", "CreatedAt"); + + b.ToTable("VirtualKeyGroupTransactions"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(10, 6)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("VirtualKeySpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCapabilities", "Capabilities") + .WithMany() + .HasForeignKey("ModelCapabilitiesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelSeries", "Series") + .WithMany("Models") + .HasForeignKey("ModelSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Capabilities"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCost", "ModelCost") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelCostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelProviderMapping", "ModelProviderMapping") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelProviderMappingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelCost"); + + b.Navigation("ModelProviderMapping"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("Identifiers") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("ProviderMappings") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelAuthor", "Author") + .WithMany("ModelSeries") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("Notifications") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany("ProviderKeyCredentials") + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("RequestLogs") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("VirtualKeys") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", "MediaRetentionPolicy") + .WithMany("VirtualKeyGroups") + .HasForeignKey("MediaRetentionPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaRetentionPolicy"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("Transactions") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("SpendHistory") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Navigation("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Navigation("Identifiers"); + + b.Navigation("ProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Navigation("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Navigation("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Navigation("ProviderKeyCredentials"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Navigation("Notifications"); + + b.Navigation("RequestLogs"); + + b.Navigation("SpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Navigation("Transactions"); + + b.Navigation("VirtualKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.cs b/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.cs new file mode 100644 index 000000000..4749dae4f --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829061847_RemoveAudioColumns.cs @@ -0,0 +1,130 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + /// + public partial class RemoveAudioColumns : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // Drop audio-related columns from ModelCapabilities table + migrationBuilder.DropColumn( + name: "SupportsAudioTranscription", + table: "ModelCapabilities"); + + migrationBuilder.DropColumn( + name: "SupportsTextToSpeech", + table: "ModelCapabilities"); + + migrationBuilder.DropColumn( + name: "SupportsRealtimeAudio", + table: "ModelCapabilities"); + + migrationBuilder.DropColumn( + name: "SupportedVoices", + table: "ModelCapabilities"); + + migrationBuilder.DropColumn( + name: "SupportedLanguages", + table: "ModelCapabilities"); + + migrationBuilder.DropColumn( + name: "SupportedFormats", + table: "ModelCapabilities"); + + // Drop audio-related columns from ModelCosts table + migrationBuilder.DropColumn( + name: "AudioCostPerMinute", + table: "ModelCosts"); + + migrationBuilder.DropColumn( + name: "AudioCostPerKCharacters", + table: "ModelCosts"); + + migrationBuilder.DropColumn( + name: "AudioInputCostPerMinute", + table: "ModelCosts"); + + migrationBuilder.DropColumn( + name: "AudioOutputCostPerMinute", + table: "ModelCosts"); + + // Drop audio-related tables that were previously created + migrationBuilder.Sql("DROP TABLE IF EXISTS \"AudioCosts\";"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"AudioProviderConfigs\";"); + migrationBuilder.Sql("DROP TABLE IF EXISTS \"AudioUsageLogs\";"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // Re-add audio columns if rolling back + migrationBuilder.AddColumn( + name: "SupportsAudioTranscription", + table: "ModelCapabilities", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SupportsTextToSpeech", + table: "ModelCapabilities", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SupportsRealtimeAudio", + table: "ModelCapabilities", + type: "boolean", + nullable: false, + defaultValue: false); + + migrationBuilder.AddColumn( + name: "SupportedVoices", + table: "ModelCapabilities", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SupportedLanguages", + table: "ModelCapabilities", + type: "text", + nullable: true); + + migrationBuilder.AddColumn( + name: "SupportedFormats", + table: "ModelCapabilities", + type: "text", + nullable: true); + + // Re-add audio columns to ModelCosts if rolling back + migrationBuilder.AddColumn( + name: "AudioCostPerMinute", + table: "ModelCosts", + type: "decimal(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AudioCostPerKCharacters", + table: "ModelCosts", + type: "decimal(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AudioInputCostPerMinute", + table: "ModelCosts", + type: "decimal(18,4)", + nullable: true); + + migrationBuilder.AddColumn( + name: "AudioOutputCostPerMinute", + table: "ModelCosts", + type: "decimal(18,4)", + nullable: true); + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.Designer.cs b/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.Designer.cs new file mode 100644 index 000000000..330f26653 --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.Designer.cs @@ -0,0 +1,1743 @@ +// +using System; +using ConduitLLM.Configuration; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + [DbContext(typeof(ConduitDbContext))] + [Migration("20250829082745_UpdateSnapshotAfterAudioRemoval")] + partial class UpdateSnapshotAfterAudioRemoval + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "9.0.8") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.Property("Id") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ArchivedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Error") + .HasColumnType("text"); + + b.Property("IsArchived") + .HasColumnType("boolean"); + + b.Property("IsRetryable") + .HasColumnType("boolean"); + + b.Property("LeaseExpiryTime") + .HasColumnType("timestamp with time zone"); + + b.Property("LeasedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("MaxRetries") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("NextRetryAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Payload") + .HasColumnType("text"); + + b.Property("Progress") + .HasColumnType("integer"); + + b.Property("ProgressMessage") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Result") + .HasColumnType("text"); + + b.Property("RetryCount") + .HasColumnType("integer"); + + b.Property("State") + .HasColumnType("integer"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .IsConcurrencyToken() + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("IsArchived"); + + b.HasIndex("State"); + + b.HasIndex("Type"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("IsArchived", "ArchivedAt") + .HasDatabaseName("IX_AsyncTasks_Cleanup"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.HasIndex("IsArchived", "CompletedAt", "State") + .HasDatabaseName("IX_AsyncTasks_Archival"); + + b.ToTable("AsyncTasks"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.Property("OperationId") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("CanResume") + .HasColumnType("boolean"); + + b.Property("CancellationReason") + .HasColumnType("text"); + + b.Property("CheckpointData") + .HasColumnType("text"); + + b.Property("CompletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DurationSeconds") + .HasColumnType("double precision"); + + b.Property("ErrorDetails") + .HasColumnType("text"); + + b.Property("ErrorMessage") + .HasColumnType("text"); + + b.Property("FailedCount") + .HasColumnType("integer"); + + b.Property("ItemsPerSecond") + .HasColumnType("double precision"); + + b.Property("LastProcessedIndex") + .HasColumnType("integer"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("OperationType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResultSummary") + .HasColumnType("text"); + + b.Property("StartedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Status") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("SuccessCount") + .HasColumnType("integer"); + + b.Property("TotalItems") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("OperationId"); + + b.HasIndex("OperationType"); + + b.HasIndex("StartedAt"); + + b.HasIndex("Status"); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "StartedAt"); + + b.HasIndex("OperationType", "Status", "StartedAt"); + + b.ToTable("BatchOperationHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid") + .HasDefaultValueSql("gen_random_uuid()"); + + b.Property("CalculatedCost") + .HasColumnType("decimal(10, 6)"); + + b.Property("EventType") + .HasColumnType("integer"); + + b.Property("FailureReason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("HttpStatusCode") + .HasColumnType("integer"); + + b.Property("IsEstimated") + .HasColumnType("boolean"); + + b.Property("MetadataJson") + .HasColumnType("jsonb"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("RequestId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Timestamp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasDefaultValueSql("CURRENT_TIMESTAMP"); + + b.Property("UsageJson") + .HasColumnType("jsonb"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("EventType") + .HasDatabaseName("IX_BillingAuditEvents_EventType"); + + b.HasIndex("RequestId") + .HasDatabaseName("IX_BillingAuditEvents_RequestId"); + + b.HasIndex("Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_Timestamp"); + + b.HasIndex("VirtualKeyId") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId"); + + b.HasIndex("EventType", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_EventType_Timestamp"); + + b.HasIndex("VirtualKeyId", "Timestamp") + .HasDatabaseName("IX_BillingAuditEvents_VirtualKeyId_Timestamp"); + + b.ToTable("BillingAuditEvents", (string)null); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfiguration", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CompressionThresholdBytes") + .HasColumnType("bigint"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("CreatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("DefaultTtlSeconds") + .HasColumnType("integer"); + + b.Property("EnableCompression") + .HasColumnType("boolean"); + + b.Property("EnableDetailedStats") + .HasColumnType("boolean"); + + b.Property("Enabled") + .HasColumnType("boolean"); + + b.Property("EvictionPolicy") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)"); + + b.Property("ExtendedConfig") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("MaxEntries") + .HasColumnType("bigint"); + + b.Property("MaxMemoryBytes") + .HasColumnType("bigint"); + + b.Property("MaxTtlSeconds") + .HasColumnType("integer"); + + b.Property("Notes") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("UpdatedBy") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UseDistributedCache") + .HasColumnType("boolean"); + + b.Property("UseMemoryCache") + .HasColumnType("boolean"); + + b.Property("Version") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("Id"); + + b.HasIndex("Region") + .IsUnique() + .HasFilter("\"IsActive\" = true"); + + b.HasIndex("UpdatedAt"); + + b.HasIndex("Region", "IsActive"); + + b.ToTable("CacheConfigurations"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.CacheConfigurationAudit", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangeSource") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ChangedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ChangedBy") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ErrorMessage") + .HasMaxLength(1000) + .HasColumnType("character varying(1000)"); + + b.Property("NewConfigJson") + .HasColumnType("text"); + + b.Property("OldConfigJson") + .HasColumnType("text"); + + b.Property("Reason") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Region") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Success") + .HasColumnType("boolean"); + + b.HasKey("Id"); + + b.HasIndex("ChangedAt"); + + b.HasIndex("ChangedBy"); + + b.HasIndex("Region"); + + b.HasIndex("Region", "ChangedAt"); + + b.ToTable("CacheConfigurationAudits"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.GlobalSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Key") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Value") + .IsRequired() + .HasMaxLength(2000) + .HasColumnType("character varying(2000)"); + + b.HasKey("Id"); + + b.HasIndex("Key") + .IsUnique(); + + b.ToTable("GlobalSettings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.IpFilterEntity", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("FilterType") + .IsRequired() + .HasMaxLength(10) + .HasColumnType("character varying(10)"); + + b.Property("IpAddressOrCidr") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("IsEnabled"); + + b.HasIndex("FilterType", "IpAddressOrCidr"); + + b.ToTable("IpFilters"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("AccessCount") + .HasColumnType("integer"); + + b.Property("ContentHash") + .HasMaxLength(64) + .HasColumnType("character varying(64)"); + + b.Property("ContentType") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("LastAccessedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MediaType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Model") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Prompt") + .HasColumnType("text"); + + b.Property("Provider") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("PublicUrl") + .HasColumnType("text"); + + b.Property("SizeBytes") + .HasColumnType("bigint"); + + b.Property("StorageKey") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("StorageUrl") + .HasColumnType("text"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ExpiresAt"); + + b.HasIndex("StorageKey") + .IsUnique(); + + b.HasIndex("VirtualKeyId"); + + b.HasIndex("VirtualKeyId", "CreatedAt"); + + b.ToTable("MediaRecords"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsProTier") + .HasColumnType("boolean"); + + b.Property("MaxFileCount") + .HasColumnType("integer"); + + b.Property("MaxStorageSizeBytes") + .HasColumnType("bigint"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("NegativeBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("PositiveBalanceRetentionDays") + .HasColumnType("integer"); + + b.Property("RecentAccessWindowDays") + .HasColumnType("integer"); + + b.Property("RespectRecentAccess") + .HasColumnType("boolean"); + + b.Property("SoftDeleteGracePeriodDays") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ZeroBalanceRetentionDays") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("IsActive"); + + b.HasIndex("IsDefault") + .IsUnique() + .HasFilter("\"IsDefault\" = true"); + + b.HasIndex("Name") + .IsUnique(); + + b.ToTable("MediaRetentionPolicies"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCapabilitiesId") + .HasColumnType("integer"); + + b.Property("ModelCardUrl") + .HasColumnType("text"); + + b.Property("ModelParameters") + .HasColumnType("text") + .HasColumnName("Parameters"); + + b.Property("ModelSeriesId") + .HasColumnType("integer"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Version") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("ModelCapabilitiesId") + .HasDatabaseName("IX_Model_ModelCapabilitiesId"); + + b.HasIndex("ModelSeriesId") + .HasDatabaseName("IX_Model_ModelSeriesId"); + + b.ToTable("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("WebsiteUrl") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.HasKey("Id"); + + b.HasIndex("Name") + .IsUnique() + .HasDatabaseName("IX_ModelAuthor_Name_Unique"); + + b.ToTable("ModelAuthors"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCapabilities", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("MaxTokens") + .HasColumnType("integer"); + + b.Property("MinTokens") + .HasColumnType("integer"); + + b.Property("SupportsChat") + .HasColumnType("boolean"); + + b.Property("SupportsEmbeddings") + .HasColumnType("boolean"); + + b.Property("SupportsFunctionCalling") + .HasColumnType("boolean"); + + b.Property("SupportsImageGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsStreaming") + .HasColumnType("boolean"); + + b.Property("SupportsVideoGeneration") + .HasColumnType("boolean"); + + b.Property("SupportsVision") + .HasColumnType("boolean"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("SupportsChat") + .HasDatabaseName("IX_ModelCapabilities_SupportsChat") + .HasFilter("\"SupportsChat\" = true"); + + b.HasIndex("SupportsFunctionCalling") + .HasDatabaseName("IX_ModelCapabilities_SupportsFunctionCalling") + .HasFilter("\"SupportsFunctionCalling\" = true"); + + b.HasIndex("SupportsImageGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsImageGeneration") + .HasFilter("\"SupportsImageGeneration\" = true"); + + b.HasIndex("SupportsVideoGeneration") + .HasDatabaseName("IX_ModelCapabilities_SupportsVideoGeneration") + .HasFilter("\"SupportsVideoGeneration\" = true"); + + b.HasIndex("SupportsVision") + .HasDatabaseName("IX_ModelCapabilities_SupportsVision") + .HasFilter("\"SupportsVision\" = true"); + + b.HasIndex("SupportsChat", "SupportsFunctionCalling", "SupportsStreaming") + .HasDatabaseName("IX_ModelCapabilities_Chat_Function_Streaming"); + + b.ToTable("ModelCapabilities"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BatchProcessingMultiplier") + .HasColumnType("decimal(18, 4)"); + + b.Property("CachedInputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CachedInputWriteCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("CostName") + .IsRequired() + .HasMaxLength(255) + .HasColumnType("character varying(255)"); + + b.Property("CostPerInferenceStep") + .HasColumnType("decimal(18, 8)"); + + b.Property("CostPerSearchUnit") + .HasColumnType("decimal(18, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultInferenceSteps") + .HasColumnType("integer"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("EffectiveDate") + .HasColumnType("timestamp with time zone"); + + b.Property("EmbeddingCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("ExpiryDate") + .HasColumnType("timestamp with time zone"); + + b.Property("ImageCostPerImage") + .HasColumnType("decimal(18, 4)"); + + b.Property("ImageQualityMultipliers") + .HasColumnType("text"); + + b.Property("ImageResolutionMultipliers") + .HasColumnType("text"); + + b.Property("InputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("OutputCostPerMillionTokens") + .HasColumnType("decimal(18, 10)"); + + b.Property("PricingConfiguration") + .HasColumnType("text"); + + b.Property("PricingModel") + .HasColumnType("integer"); + + b.Property("Priority") + .HasColumnType("integer"); + + b.Property("SupportsBatchProcessing") + .HasColumnType("boolean"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VideoCostPerSecond") + .HasColumnType("decimal(18, 4)"); + + b.Property("VideoResolutionMultipliers") + .HasColumnType("text"); + + b.HasKey("Id"); + + b.HasIndex("CostName"); + + b.ToTable("ModelCosts"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsActive") + .HasColumnType("boolean"); + + b.Property("ModelCostId") + .HasColumnType("integer"); + + b.Property("ModelProviderMappingId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("ModelProviderMappingId"); + + b.HasIndex("ModelCostId", "ModelProviderMappingId") + .IsUnique(); + + b.ToTable("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Identifier") + .IsRequired() + .HasMaxLength(200) + .HasColumnType("character varying(200)"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("Provider") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.HasKey("Id"); + + b.HasIndex("Identifier") + .HasDatabaseName("IX_ModelIdentifier_Identifier"); + + b.HasIndex("IsPrimary") + .HasDatabaseName("IX_ModelIdentifier_IsPrimary") + .HasFilter("\"IsPrimary\" = true"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelIdentifier_ModelId"); + + b.HasIndex("Provider", "Identifier") + .IsUnique() + .HasDatabaseName("IX_ModelIdentifier_Provider_Identifier_Unique"); + + b.ToTable("ModelIdentifiers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CapabilityOverrides") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DefaultCapabilityType") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("IsDefault") + .HasColumnType("boolean"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("MaxContextTokensOverride") + .HasColumnType("integer"); + + b.Property("ModelAlias") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ModelId") + .HasColumnType("integer"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("ProviderModelId") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderVariation") + .HasColumnType("text"); + + b.Property("QualityScore") + .HasColumnType("numeric"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("CapabilityOverrides") + .HasDatabaseName("IX_ModelProviderMapping_CapabilityOverrides") + .HasFilter("\"CapabilityOverrides\" IS NOT NULL"); + + b.HasIndex("ModelId") + .HasDatabaseName("IX_ModelProviderMapping_ModelId"); + + b.HasIndex("ModelAlias", "ProviderId") + .IsUnique(); + + b.HasIndex("ModelId", "QualityScore") + .HasDatabaseName("IX_ModelProviderMapping_ModelId_QualityScore") + .HasFilter("\"QualityScore\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsEnabled") + .HasDatabaseName("IX_ModelProviderMapping_ProviderId_IsEnabled") + .HasFilter("\"IsEnabled\" = true"); + + b.ToTable("ModelProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("integer"); + + b.Property("Description") + .HasColumnType("text"); + + b.Property("Name") + .IsRequired() + .HasColumnType("text"); + + b.Property("Parameters") + .IsRequired() + .HasColumnType("text"); + + b.Property("TokenizerType") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("AuthorId") + .HasDatabaseName("IX_ModelSeries_AuthorId"); + + b.HasIndex("TokenizerType") + .HasDatabaseName("IX_ModelSeries_TokenizerType"); + + b.HasIndex("AuthorId", "Name") + .IsUnique() + .HasDatabaseName("IX_ModelSeries_AuthorId_Name_Unique"); + + b.ToTable("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsRead") + .HasColumnType("boolean"); + + b.Property("Message") + .IsRequired() + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("Severity") + .HasColumnType("integer"); + + b.Property("Type") + .HasColumnType("integer"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("Notifications"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("ProviderName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ProviderType") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderType"); + + b.ToTable("Providers"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ApiKey") + .HasColumnType("text"); + + b.Property("BaseUrl") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("IsPrimary") + .HasColumnType("boolean"); + + b.Property("KeyName") + .HasColumnType("text"); + + b.Property("Organization") + .HasColumnType("text"); + + b.Property("ProviderAccountGroup") + .HasColumnType("smallint"); + + b.Property("ProviderId") + .HasColumnType("integer"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ProviderId") + .HasDatabaseName("IX_ProviderKeyCredential_ProviderId"); + + b.HasIndex("ProviderId", "ApiKey") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_UniqueApiKeyPerProvider") + .HasFilter("\"ApiKey\" IS NOT NULL"); + + b.HasIndex("ProviderId", "IsPrimary") + .IsUnique() + .HasDatabaseName("IX_ProviderKeyCredential_OnePrimaryPerProvider") + .HasFilter("\"IsPrimary\" = true"); + + b.ToTable("ProviderKeyCredentials", t => + { + t.HasCheckConstraint("CK_ProviderKeyCredential_AccountGroupRange", "\"ProviderAccountGroup\" >= 0 AND \"ProviderAccountGroup\" <= 32"); + + t.HasCheckConstraint("CK_ProviderKeyCredential_PrimaryMustBeEnabled", "\"IsPrimary\" = false OR \"IsEnabled\" = true"); + }); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ClientIp") + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("Cost") + .HasColumnType("decimal(10, 6)"); + + b.Property("InputTokens") + .HasColumnType("integer"); + + b.Property("ModelName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("OutputTokens") + .HasColumnType("integer"); + + b.Property("RequestPath") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("RequestType") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("ResponseTimeMs") + .HasColumnType("double precision"); + + b.Property("StatusCode") + .HasColumnType("integer"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("UserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("RequestLogs"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowedModels") + .HasColumnType("text"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("ExpiresAt") + .HasColumnType("timestamp with time zone"); + + b.Property("IsEnabled") + .HasColumnType("boolean"); + + b.Property("KeyHash") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)"); + + b.Property("KeyName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("Metadata") + .HasColumnType("text"); + + b.Property("RateLimitRpd") + .HasColumnType("integer"); + + b.Property("RateLimitRpm") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("KeyHash") + .IsUnique(); + + b.HasIndex("VirtualKeyGroupId"); + + b.ToTable("VirtualKeys"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("decimal(19, 8)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("ExternalGroupId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("GroupName") + .IsRequired() + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("LifetimeCreditsAdded") + .HasColumnType("decimal(19, 8)"); + + b.Property("LifetimeSpent") + .HasColumnType("decimal(19, 8)"); + + b.Property("MediaRetentionPolicyId") + .HasColumnType("integer"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone"); + + b.HasKey("Id"); + + b.HasIndex("ExternalGroupId"); + + b.HasIndex("MediaRetentionPolicyId"); + + b.ToTable("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(18, 6)"); + + b.Property("BalanceAfter") + .HasColumnType("decimal(18, 6)"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("Description") + .HasMaxLength(500) + .HasColumnType("character varying(500)"); + + b.Property("InitiatedBy") + .IsRequired() + .HasMaxLength(50) + .HasColumnType("character varying(50)"); + + b.Property("InitiatedByUserId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("IsDeleted") + .HasColumnType("boolean"); + + b.Property("ReferenceId") + .HasMaxLength(100) + .HasColumnType("character varying(100)"); + + b.Property("ReferenceType") + .HasColumnType("integer"); + + b.Property("TransactionType") + .HasColumnType("integer"); + + b.Property("VirtualKeyGroupId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("ReferenceType"); + + b.HasIndex("TransactionType"); + + b.HasIndex("VirtualKeyGroupId"); + + b.HasIndex("IsDeleted", "CreatedAt"); + + b.HasIndex("VirtualKeyGroupId", "CreatedAt"); + + b.ToTable("VirtualKeyGroupTransactions"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("decimal(10, 6)"); + + b.Property("Date") + .HasColumnType("timestamp with time zone"); + + b.Property("Timestamp") + .HasColumnType("timestamp with time zone"); + + b.Property("VirtualKeyId") + .HasColumnType("integer"); + + b.HasKey("Id"); + + b.HasIndex("VirtualKeyId"); + + b.ToTable("VirtualKeySpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.AsyncTask", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.BillingAuditEvent", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany() + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCapabilities", "Capabilities") + .WithMany() + .HasForeignKey("ModelCapabilitiesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelSeries", "Series") + .WithMany("Models") + .HasForeignKey("ModelSeriesId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Capabilities"); + + b.Navigation("Series"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCostMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelCost", "ModelCost") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelCostId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.ModelProviderMapping", "ModelProviderMapping") + .WithMany("ModelCostMappings") + .HasForeignKey("ModelProviderMappingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("ModelCost"); + + b.Navigation("ModelProviderMapping"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("Identifiers") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Model"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") + .WithMany("ProviderMappings") + .HasForeignKey("ModelId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany() + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("Model"); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.ModelAuthor", "Author") + .WithMany("ModelSeries") + .HasForeignKey("AuthorId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Author"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Notification", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("Notifications") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ProviderKeyCredential", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") + .WithMany("ProviderKeyCredentials") + .HasForeignKey("ProviderId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Provider"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.RequestLog", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("RequestLogs") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("VirtualKeys") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Restrict) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", "MediaRetentionPolicy") + .WithMany("VirtualKeyGroups") + .HasForeignKey("MediaRetentionPolicyId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("MediaRetentionPolicy"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroupTransaction", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKeyGroup", "VirtualKeyGroup") + .WithMany("Transactions") + .HasForeignKey("VirtualKeyGroupId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKeyGroup"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeySpendHistory", b => + { + b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") + .WithMany("SpendHistory") + .HasForeignKey("VirtualKeyId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("VirtualKey"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => + { + b.Navigation("VirtualKeyGroups"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Model", b => + { + b.Navigation("Identifiers"); + + b.Navigation("ProviderMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelAuthor", b => + { + b.Navigation("ModelSeries"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelCost", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelProviderMapping", b => + { + b.Navigation("ModelCostMappings"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelSeries", b => + { + b.Navigation("Models"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.Provider", b => + { + b.Navigation("ProviderKeyCredentials"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => + { + b.Navigation("Notifications"); + + b.Navigation("RequestLogs"); + + b.Navigation("SpendHistory"); + }); + + modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKeyGroup", b => + { + b.Navigation("Transactions"); + + b.Navigation("VirtualKeys"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.cs b/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.cs new file mode 100644 index 000000000..041590019 --- /dev/null +++ b/ConduitLLM.Configuration/Migrations/20250829082745_UpdateSnapshotAfterAudioRemoval.cs @@ -0,0 +1,24 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace ConduitLLM.Configuration.Migrations +{ + /// + public partial class UpdateSnapshotAfterAudioRemoval : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + // These columns were already dropped in the RemoveAudioColumns migration + // This migration exists only to update the model snapshot + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + // No-op: The columns were removed in RemoveAudioColumns migration + // Reverting this empty migration should not re-add them + } + } +} diff --git a/ConduitLLM.Configuration/Migrations/ConduitDbContextModelSnapshot.cs b/ConduitLLM.Configuration/Migrations/ConduitDbContextModelSnapshot.cs index f8a37f8b5..eaf57a7e4 100644 --- a/ConduitLLM.Configuration/Migrations/ConduitDbContextModelSnapshot.cs +++ b/ConduitLLM.Configuration/Migrations/ConduitDbContextModelSnapshot.cs @@ -119,214 +119,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("AsyncTasks"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioCost", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("AdditionalFactors") - .HasColumnType("text"); - - b.Property("CostPerUnit") - .HasColumnType("decimal(10, 6)"); - - b.Property("CostUnit") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("EffectiveFrom") - .HasColumnType("timestamp with time zone"); - - b.Property("EffectiveTo") - .HasColumnType("timestamp with time zone"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("MinimumCharge") - .HasColumnType("decimal(10, 6)"); - - b.Property("Model") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("OperationType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("ProviderId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("EffectiveFrom", "EffectiveTo"); - - b.HasIndex("ProviderId", "OperationType", "Model", "IsActive"); - - b.ToTable("AudioCosts"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioProviderConfig", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("CustomSettings") - .HasColumnType("text"); - - b.Property("DefaultRealtimeModel") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("DefaultTTSModel") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("DefaultTTSVoice") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("DefaultTranscriptionModel") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("ProviderId") - .HasColumnType("integer"); - - b.Property("RealtimeEnabled") - .HasColumnType("boolean"); - - b.Property("RealtimeEndpoint") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("RoutingPriority") - .HasColumnType("integer"); - - b.Property("TextToSpeechEnabled") - .HasColumnType("boolean"); - - b.Property("TranscriptionEnabled") - .HasColumnType("boolean"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("ProviderId") - .IsUnique(); - - b.ToTable("AudioProviderConfigs"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioUsageLog", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("bigint"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CharacterCount") - .HasColumnType("integer"); - - b.Property("Cost") - .HasColumnType("decimal(10, 6)"); - - b.Property("DurationSeconds") - .HasColumnType("double precision"); - - b.Property("ErrorMessage") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("InputTokens") - .HasColumnType("integer"); - - b.Property("IpAddress") - .HasMaxLength(45) - .HasColumnType("character varying(45)"); - - b.Property("Language") - .HasMaxLength(10) - .HasColumnType("character varying(10)"); - - b.Property("Metadata") - .HasColumnType("text"); - - b.Property("Model") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("OperationType") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("OutputTokens") - .HasColumnType("integer"); - - b.Property("ProviderId") - .HasColumnType("integer"); - - b.Property("RequestId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("SessionId") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("StatusCode") - .HasColumnType("integer"); - - b.Property("Timestamp") - .HasColumnType("timestamp with time zone"); - - b.Property("UserAgent") - .HasMaxLength(500) - .HasColumnType("character varying(500)"); - - b.Property("VirtualKey") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("Voice") - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.HasKey("Id"); - - b.HasIndex("SessionId"); - - b.HasIndex("Timestamp"); - - b.HasIndex("VirtualKey"); - - b.HasIndex("ProviderId", "OperationType"); - - b.ToTable("AudioUsageLogs"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => { b.Property("OperationId") @@ -637,81 +429,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("CacheConfigurationAudits"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("PrimaryModelDeploymentId") - .HasColumnType("uuid"); - - b.Property("RouterConfigId") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("PrimaryModelDeploymentId"); - - b.HasIndex("RouterConfigId"); - - b.ToTable("FallbackConfigurations"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackModelMappingEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("FallbackConfigurationId") - .HasColumnType("uuid"); - - b.Property("ModelDeploymentId") - .HasColumnType("uuid"); - - b.Property("Order") - .HasColumnType("integer"); - - b.Property("SourceModelName") - .IsRequired() - .HasColumnType("text"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("FallbackConfigurationId", "ModelDeploymentId") - .IsUnique(); - - b.HasIndex("FallbackConfigurationId", "Order") - .IsUnique(); - - b.ToTable("FallbackModelMappings"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.GlobalSetting", b => { b.Property("Id") @@ -1033,18 +750,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("MinTokens") .HasColumnType("integer"); - b.Property("SupportedFormats") - .HasColumnType("text"); - - b.Property("SupportedLanguages") - .HasColumnType("text"); - - b.Property("SupportedVoices") - .HasColumnType("text"); - - b.Property("SupportsAudioTranscription") - .HasColumnType("boolean"); - b.Property("SupportsChat") .HasColumnType("boolean"); @@ -1057,15 +762,9 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Property("SupportsImageGeneration") .HasColumnType("boolean"); - b.Property("SupportsRealtimeAudio") - .HasColumnType("boolean"); - b.Property("SupportsStreaming") .HasColumnType("boolean"); - b.Property("SupportsTextToSpeech") - .HasColumnType("boolean"); - b.Property("SupportsVideoGeneration") .HasColumnType("boolean"); @@ -1111,18 +810,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("AudioCostPerKCharacters") - .HasColumnType("decimal(18, 4)"); - - b.Property("AudioCostPerMinute") - .HasColumnType("decimal(18, 4)"); - - b.Property("AudioInputCostPerMinute") - .HasColumnType("decimal(18, 4)"); - - b.Property("AudioOutputCostPerMinute") - .HasColumnType("decimal(18, 4)"); - b.Property("BatchProcessingMultiplier") .HasColumnType("decimal(18, 4)"); @@ -1243,81 +930,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("ModelCostMappings"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelDeploymentEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("uuid"); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DeploymentName") - .IsRequired() - .HasColumnType("text"); - - b.Property("HealthCheckEnabled") - .HasColumnType("boolean"); - - b.Property("InputTokenCostPer1K") - .HasColumnType("decimal(18, 8)"); - - b.Property("IsEnabled") - .HasColumnType("boolean"); - - b.Property("IsHealthy") - .HasColumnType("boolean"); - - b.Property("LastUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("ModelName") - .IsRequired() - .HasMaxLength(100) - .HasColumnType("character varying(100)"); - - b.Property("OutputTokenCostPer1K") - .HasColumnType("decimal(18, 8)"); - - b.Property("Priority") - .HasColumnType("integer"); - - b.Property("ProviderId") - .HasColumnType("integer"); - - b.Property("RPM") - .HasColumnType("integer"); - - b.Property("RouterConfigId") - .HasColumnType("integer"); - - b.Property("SupportsEmbeddings") - .HasColumnType("boolean"); - - b.Property("TPM") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("Weight") - .HasColumnType("integer"); - - b.HasKey("Id"); - - b.HasIndex("IsEnabled"); - - b.HasIndex("IsHealthy"); - - b.HasIndex("ModelName"); - - b.HasIndex("ProviderId"); - - b.HasIndex("RouterConfigId"); - - b.ToTable("ModelDeployments"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => { b.Property("Id") @@ -1667,54 +1279,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.ToTable("RequestLogs"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.RouterConfigEntity", b => - { - b.Property("Id") - .ValueGeneratedOnAdd() - .HasColumnType("integer"); - - NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - - b.Property("CreatedAt") - .HasColumnType("timestamp with time zone"); - - b.Property("DefaultRoutingStrategy") - .IsRequired() - .HasMaxLength(50) - .HasColumnType("character varying(50)"); - - b.Property("FallbacksEnabled") - .HasColumnType("boolean"); - - b.Property("IsActive") - .HasColumnType("boolean"); - - b.Property("LastUpdated") - .HasColumnType("timestamp with time zone"); - - b.Property("MaxRetries") - .HasColumnType("integer"); - - b.Property("Name") - .IsRequired() - .HasColumnType("text"); - - b.Property("RetryBaseDelayMs") - .HasColumnType("integer"); - - b.Property("RetryMaxDelayMs") - .HasColumnType("integer"); - - b.Property("UpdatedAt") - .HasColumnType("timestamp with time zone"); - - b.HasKey("Id"); - - b.HasIndex("LastUpdated"); - - b.ToTable("RouterConfigEntity"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => { b.Property("Id") @@ -1932,39 +1496,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("VirtualKey"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioCost", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioProviderConfig", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") - .WithOne() - .HasForeignKey("ConduitLLM.Configuration.Entities.AudioProviderConfig", "ProviderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Provider"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.AudioUsageLog", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Provider"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.BatchOperationHistory", b => { b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") @@ -1986,28 +1517,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("VirtualKey"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.RouterConfigEntity", "RouterConfig") - .WithMany("FallbackConfigurations") - .HasForeignKey("RouterConfigId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("RouterConfig"); - }); - - modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackModelMappingEntity", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", "FallbackConfiguration") - .WithMany("FallbackMappings") - .HasForeignKey("FallbackConfigurationId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("FallbackConfiguration"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRecord", b => { b.HasOne("ConduitLLM.Configuration.Entities.VirtualKey", "VirtualKey") @@ -2057,25 +1566,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ModelProviderMapping"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelDeploymentEntity", b => - { - b.HasOne("ConduitLLM.Configuration.Entities.Provider", "Provider") - .WithMany() - .HasForeignKey("ProviderId") - .OnDelete(DeleteBehavior.Restrict) - .IsRequired(); - - b.HasOne("ConduitLLM.Configuration.Entities.RouterConfigEntity", "RouterConfig") - .WithMany("ModelDeployments") - .HasForeignKey("RouterConfigId") - .OnDelete(DeleteBehavior.Cascade) - .IsRequired(); - - b.Navigation("Provider"); - - b.Navigation("RouterConfig"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.ModelIdentifier", b => { b.HasOne("ConduitLLM.Configuration.Entities.Model", "Model") @@ -2192,11 +1682,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("VirtualKey"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.FallbackConfigurationEntity", b => - { - b.Navigation("FallbackMappings"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.MediaRetentionPolicy", b => { b.Navigation("VirtualKeyGroups"); @@ -2234,13 +1719,6 @@ protected override void BuildModel(ModelBuilder modelBuilder) b.Navigation("ProviderKeyCredentials"); }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.RouterConfigEntity", b => - { - b.Navigation("FallbackConfigurations"); - - b.Navigation("ModelDeployments"); - }); - modelBuilder.Entity("ConduitLLM.Configuration.Entities.VirtualKey", b => { b.Navigation("Notifications"); diff --git a/ConduitLLM.Configuration/Options/RouterOptions.cs b/ConduitLLM.Configuration/Options/RouterOptions.cs deleted file mode 100644 index f94ff90aa..000000000 --- a/ConduitLLM.Configuration/Options/RouterOptions.cs +++ /dev/null @@ -1,94 +0,0 @@ -namespace ConduitLLM.Configuration.Options -{ - /// - /// Configuration options for the router service - /// - public class RouterOptions - { - /// - /// Section name for configuration - /// - public const string SectionName = "Router"; - - /// - /// Whether the router is enabled - /// - public bool Enabled { get; set; } = false; - - /// - /// The default routing strategy to use - /// - public string DefaultRoutingStrategy { get; set; } = "Simple"; - - /// - /// Maximum number of retries for a failed request - /// - public int MaxRetries { get; set; } = 3; - - /// - /// Base delay in milliseconds between retries (for exponential backoff) - /// - public int RetryBaseDelayMs { get; set; } = 500; - - /// - /// Maximum delay in milliseconds between retries - /// - public int RetryMaxDelayMs { get; set; } = 10000; - - /// - /// List of model deployments available to the router - /// - public List ModelDeployments { get; set; } = new(); - - /// - /// List of fallback configurations in the format "primary_model:fallback_model1,fallback_model2" - /// - public List FallbackRules { get; set; } = new(); - - /// - /// The default audio routing strategy to use (LatencyBased, LanguageOptimized, CostOptimized, QualityBased) - /// - public string DefaultAudioRoutingStrategy { get; set; } = "LatencyBased"; - } - - /// - /// Configuration for a model deployment in the router - /// - public class RouterModelDeployment - { - /// - /// Unique name for this model deployment - /// - public string DeploymentName { get; set; } = string.Empty; - - /// - /// The model alias this deployment refers to - /// - public string ModelAlias { get; set; } = string.Empty; - - /// - /// Maximum requests per minute for this deployment - /// - public int? RPM { get; set; } - - /// - /// Maximum tokens per minute for this deployment - /// - public int? TPM { get; set; } - - /// - /// Cost per 1000 input tokens - /// - public decimal? InputTokenCostPer1K { get; set; } - - /// - /// Cost per 1000 output tokens - /// - public decimal? OutputTokenCostPer1K { get; set; } - - /// - /// Priority of this deployment (lower values are higher priority) - /// - public int Priority { get; set; } = 1; - } -} diff --git a/ConduitLLM.Configuration/ProviderDefaultModels.cs b/ConduitLLM.Configuration/ProviderDefaultModels.cs index dc45b0708..9f0ed449c 100644 --- a/ConduitLLM.Configuration/ProviderDefaultModels.cs +++ b/ConduitLLM.Configuration/ProviderDefaultModels.cs @@ -6,15 +6,6 @@ namespace ConduitLLM.Configuration; /// public class ProviderDefaultModels { - /// - /// Gets or sets the default models for audio operations. - /// - public AudioDefaultModels Audio { get; set; } = new(); - - /// - /// Gets or sets the default models for realtime operations. - /// - public RealtimeDefaultModels Realtime { get; set; } = new(); /// /// Gets or sets provider-specific default models. @@ -22,58 +13,6 @@ public class ProviderDefaultModels public Dictionary ProviderDefaults { get; set; } = new(); } -/// -/// Default models for audio operations across providers. -/// -public class AudioDefaultModels -{ - /// - /// Gets or sets the default model for speech-to-text transcription. - /// - public string? DefaultTranscriptionModel { get; set; } - - /// - /// Gets or sets the default model for text-to-speech generation. - /// - public string? DefaultTextToSpeechModel { get; set; } - - /// - /// Gets or sets provider-specific audio model defaults. - /// - public Dictionary ProviderOverrides { get; set; } = new(); -} - -/// -/// Provider-specific audio model defaults. -/// -public class AudioProviderDefaults -{ - /// - /// Gets or sets the transcription model for this provider. - /// - public string? TranscriptionModel { get; set; } - - /// - /// Gets or sets the text-to-speech model for this provider. - /// - public string? TextToSpeechModel { get; set; } -} - -/// -/// Default models for realtime operations. -/// -public class RealtimeDefaultModels -{ - /// - /// Gets or sets the default model for realtime conversations. - /// - public string? DefaultRealtimeModel { get; set; } - - /// - /// Gets or sets provider-specific realtime model defaults. - /// - public Dictionary ProviderOverrides { get; set; } = new(); -} /// /// Provider-specific default model configurations. diff --git a/ConduitLLM.Configuration/Repositories/AudioCostRepository.cs b/ConduitLLM.Configuration/Repositories/AudioCostRepository.cs deleted file mode 100644 index 98017cc7b..000000000 --- a/ConduitLLM.Configuration/Repositories/AudioCostRepository.cs +++ /dev/null @@ -1,172 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for audio cost configurations. - /// - public class AudioCostRepository : IAudioCostRepository - { - private readonly ConduitDbContext _context; - - /// - /// Initializes a new instance of the class. - /// - public AudioCostRepository(ConduitDbContext context) - { - _context = context; - } - - /// - public async Task> GetAllAsync() - { - return await _context.AudioCosts - .OrderBy(c => c.ProviderId) - .ThenBy(c => c.OperationType) - .ThenBy(c => c.Model) - .ToListAsync(); - } - - /// - public async Task GetByIdAsync(int id) - { - return await _context.AudioCosts.FindAsync(id); - } - - /// - public async Task> GetByProviderAsync(int providerId) - { - return await _context.AudioCosts - .Where(c => c.ProviderId == providerId) - .OrderBy(c => c.OperationType) - .ThenBy(c => c.Model) - .ToListAsync(); - } - - /// - public async Task GetCurrentCostAsync(int providerId, string operationType, string? model = null) - { - var now = DateTime.UtcNow; - var query = _context.AudioCosts - .Where(c => c.ProviderId == providerId && - c.OperationType.ToLower() == operationType.ToLower() && - c.IsActive && - c.EffectiveFrom <= now && - (c.EffectiveTo == null || c.EffectiveTo > now)); - - if (!string.IsNullOrEmpty(model)) - { - query = query.Where(c => c.Model == model); - } - else - { - query = query.Where(c => c.Model == null); - } - - return await query.FirstOrDefaultAsync(); - } - - /// - public async Task> GetEffectiveAtDateAsync(DateTime date) - { - return await _context.AudioCosts - .Where(c => c.IsActive && - c.EffectiveFrom <= date && - (c.EffectiveTo == null || c.EffectiveTo > date)) - .OrderBy(c => c.ProviderId) - .ThenBy(c => c.OperationType) - .ThenBy(c => c.Model) - .ToListAsync(); - } - - /// - public async Task CreateAsync(AudioCost cost) - { - cost.CreatedAt = DateTime.UtcNow; - cost.UpdatedAt = DateTime.UtcNow; - - // Deactivate previous costs if this is replacing an existing one - if (cost.IsActive) - { - await DeactivatePreviousCostsAsync(cost.ProviderId, cost.OperationType, cost.Model); - } - - _context.AudioCosts.Add(cost); - await _context.SaveChangesAsync(); - - return cost; - } - - /// - public async Task UpdateAsync(AudioCost cost) - { - cost.UpdatedAt = DateTime.UtcNow; - - _context.AudioCosts.Update(cost); - await _context.SaveChangesAsync(); - - return cost; - } - - /// - public async Task DeleteAsync(int id) - { - var cost = await _context.AudioCosts.FindAsync(id); - if (cost == null) - return false; - - _context.AudioCosts.Remove(cost); - await _context.SaveChangesAsync(); - - return true; - } - - /// - public async Task DeactivatePreviousCostsAsync(int providerId, string operationType, string? model = null) - { - var costs = await _context.AudioCosts - .Where(c => c.ProviderId == providerId && - c.OperationType.ToLower() == operationType.ToLower() && - c.Model == model && - c.IsActive && - c.EffectiveTo == null) - .ToListAsync(); - - foreach (var cost in costs) - { - cost.EffectiveTo = DateTime.UtcNow; - cost.IsActive = false; - cost.UpdatedAt = DateTime.UtcNow; - } - - if (costs.Count() > 0) - { - await _context.SaveChangesAsync(); - } - } - - /// - public async Task> GetCostHistoryAsync(int providerId, string operationType, string? model = null) - { - var query = _context.AudioCosts - .Where(c => c.ProviderId == providerId && - c.OperationType.ToLower() == operationType.ToLower()); - - if (!string.IsNullOrEmpty(model)) - { - query = query.Where(c => c.Model == model); - } - else - { - query = query.Where(c => c.Model == null); - } - - return await query - .OrderByDescending(c => c.EffectiveFrom) - .ToListAsync(); - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/AudioProviderConfigRepository.cs b/ConduitLLM.Configuration/Repositories/AudioProviderConfigRepository.cs deleted file mode 100644 index 86487cb43..000000000 --- a/ConduitLLM.Configuration/Repositories/AudioProviderConfigRepository.cs +++ /dev/null @@ -1,130 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for audio provider configurations. - /// - public class AudioProviderConfigRepository : IAudioProviderConfigRepository - { - private readonly ConduitDbContext _context; - - /// - /// Initializes a new instance of the class. - /// - public AudioProviderConfigRepository(ConduitDbContext context) - { - _context = context; - } - - /// - public async Task> GetAllAsync() - { - try - { - return await _context.AudioProviderConfigs - .Include(c => c.Provider) - .OrderBy(c => c.Provider != null ? c.Provider.ProviderType : ProviderType.OpenAI) - .ThenByDescending(c => c.RoutingPriority) - .ToListAsync(); - } - catch (Exception) - { - // Return empty list if database tables don't exist or there's a connection issue - return new List(); - } - } - - /// - public async Task GetByIdAsync(int id) - { - return await _context.AudioProviderConfigs - .Include(c => c.Provider) - .FirstOrDefaultAsync(c => c.Id == id); - } - - /// - public async Task GetByProviderIdAsync(int ProviderId) - { - return await _context.AudioProviderConfigs - .Include(c => c.Provider) - .FirstOrDefaultAsync(c => c.ProviderId == ProviderId); - } - - /// - public async Task> GetByProviderTypeAsync(ProviderType providerType) - { - return await _context.AudioProviderConfigs - .Include(c => c.Provider) - .Where(c => c.Provider.ProviderType == providerType) - .OrderByDescending(c => c.RoutingPriority) - .ToListAsync(); - } - - /// - public async Task> GetEnabledForOperationAsync(string operationType) - { - var query = _context.AudioProviderConfigs - .Include(c => c.Provider) - .Where(c => c.Provider.IsEnabled); - - query = operationType.ToLower() switch - { - "transcription" => query.Where(c => c.TranscriptionEnabled), - "tts" or "texttospeech" => query.Where(c => c.TextToSpeechEnabled), - "realtime" => query.Where(c => c.RealtimeEnabled), - _ => query - }; - - return await query - .OrderByDescending(c => c.RoutingPriority) - .ToListAsync(); - } - - /// - public async Task CreateAsync(AudioProviderConfig config) - { - config.CreatedAt = DateTime.UtcNow; - config.UpdatedAt = DateTime.UtcNow; - - _context.AudioProviderConfigs.Add(config); - await _context.SaveChangesAsync(); - - return config; - } - - /// - public async Task UpdateAsync(AudioProviderConfig config) - { - config.UpdatedAt = DateTime.UtcNow; - - _context.AudioProviderConfigs.Update(config); - await _context.SaveChangesAsync(); - - return config; - } - - /// - public async Task DeleteAsync(int id) - { - var config = await _context.AudioProviderConfigs.FindAsync(id); - if (config == null) - return false; - - _context.AudioProviderConfigs.Remove(config); - await _context.SaveChangesAsync(); - - return true; - } - - /// - public async Task ExistsForProviderAsync(int ProviderId) - { - return await _context.AudioProviderConfigs - .AnyAsync(c => c.ProviderId == ProviderId); - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/AudioUsageLogRepository.cs b/ConduitLLM.Configuration/Repositories/AudioUsageLogRepository.cs deleted file mode 100644 index 56ecfa52c..000000000 --- a/ConduitLLM.Configuration/Repositories/AudioUsageLogRepository.cs +++ /dev/null @@ -1,324 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for audio usage logging. - /// - public class AudioUsageLogRepository : IAudioUsageLogRepository - { - private readonly ConduitDbContext _context; - - /// - /// Initializes a new instance of the class. - /// - public AudioUsageLogRepository(ConduitDbContext context) - { - _context = context; - } - - /// - public async Task CreateAsync(AudioUsageLog log) - { - log.Timestamp = DateTime.UtcNow; - - _context.AudioUsageLogs.Add(log); - await _context.SaveChangesAsync(); - - return log; - } - - /// - public async Task> GetPagedAsync(AudioUsageQueryDto query) - { - // Ensure page size is within bounds (even though DTO validates this) - const int maxPageSize = 1000; - if (query.PageSize > maxPageSize) - { - query.PageSize = maxPageSize; - } - - var queryable = _context.AudioUsageLogs.AsQueryable(); - - // Apply filters - if (!string.IsNullOrEmpty(query.VirtualKey)) - queryable = queryable.Where(l => l.VirtualKey == query.VirtualKey); - - if (query.ProviderId.HasValue) - queryable = queryable.Where(l => l.ProviderId == query.ProviderId.Value); - - if (!string.IsNullOrEmpty(query.OperationType)) - queryable = queryable.Where(l => l.OperationType.ToLower() == query.OperationType.ToLower()); - - if (query.StartDate.HasValue) - { - var utcStartDate = query.StartDate.Value.Kind == DateTimeKind.Utc ? query.StartDate.Value : DateTime.SpecifyKind(query.StartDate.Value, DateTimeKind.Utc); - queryable = queryable.Where(l => l.Timestamp >= utcStartDate); - } - - if (query.EndDate.HasValue) - { - var utcEndDate = query.EndDate.Value.Kind == DateTimeKind.Utc ? query.EndDate.Value : DateTime.SpecifyKind(query.EndDate.Value, DateTimeKind.Utc); - queryable = queryable.Where(l => l.Timestamp <= utcEndDate); - } - - if (query.OnlyErrors) - queryable = queryable.Where(l => l.StatusCode == null || l.StatusCode >= 400); - - // Get total count - var totalCount = await queryable.CountAsync(); - - // Apply pagination - var items = await queryable - .OrderByDescending(l => l.Timestamp) - .Skip((query.Page - 1) * query.PageSize) - .Take(query.PageSize) - .ToListAsync(); - - return new PagedResult - { - Items = items, - TotalCount = totalCount, - Page = query.Page, - PageSize = query.PageSize, - TotalPages = (int)Math.Ceiling(totalCount / (double)query.PageSize) - }; - } - - /// - public async Task GetUsageSummaryAsync(DateTime startDate, DateTime endDate, string? virtualKey = null, int? providerId = null) - { - // Ensure dates are in UTC for PostgreSQL - var utcStartDate = startDate.Kind == DateTimeKind.Utc ? startDate : DateTime.SpecifyKind(startDate, DateTimeKind.Utc); - var utcEndDate = endDate.Kind == DateTimeKind.Utc ? endDate : DateTime.SpecifyKind(endDate, DateTimeKind.Utc); - - var query = _context.AudioUsageLogs - .Where(l => l.Timestamp >= utcStartDate && l.Timestamp <= utcEndDate); - - if (!string.IsNullOrEmpty(virtualKey)) - query = query.Where(l => l.VirtualKey == virtualKey); - - if (providerId.HasValue) - { - query = query.Where(l => l.ProviderId == providerId.Value); - } - - var logs = await query.ToListAsync(); - - var summary = new AudioUsageSummaryDto - { - StartDate = utcStartDate, - EndDate = utcEndDate, - TotalOperations = logs.Count, - SuccessfulOperations = logs.Count(l => l.StatusCode == null || (l.StatusCode >= 200 && l.StatusCode < 300)), - FailedOperations = logs.Count(l => l.StatusCode >= 400), - TotalCost = logs.Sum(l => l.Cost), - TotalDurationSeconds = logs.Where(l => l.DurationSeconds.HasValue).Sum(l => l.DurationSeconds!.Value), - TotalCharacters = logs.Where(l => l.CharacterCount.HasValue).Sum(l => (long)l.CharacterCount!.Value), - TotalInputTokens = logs.Where(l => l.InputTokens.HasValue).Sum(l => (long)l.InputTokens!.Value), - TotalOutputTokens = logs.Where(l => l.OutputTokens.HasValue).Sum(l => (long)l.OutputTokens!.Value) - }; - - // Get operation breakdown - summary.OperationBreakdown = await GetOperationBreakdownAsync(utcStartDate, utcEndDate, virtualKey); - - // Get provider breakdown - summary.ProviderBreakdown = await GetProviderBreakdownAsync(utcStartDate, utcEndDate, virtualKey); - - // Get virtual key breakdown (if not filtering by a specific key) - if (string.IsNullOrEmpty(virtualKey)) - { - summary.VirtualKeyBreakdown = await GetVirtualKeyBreakdownAsync(utcStartDate, utcEndDate, providerId); - } - - return summary; - } - - /// - public async Task> GetByVirtualKeyAsync(string virtualKey, DateTime? startDate = null, DateTime? endDate = null) - { - var query = _context.AudioUsageLogs.Where(l => l.VirtualKey == virtualKey); - - if (startDate.HasValue) - { - var utcStartDate = startDate.Value.Kind == DateTimeKind.Utc ? startDate.Value : DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc); - query = query.Where(l => l.Timestamp >= utcStartDate); - } - - if (endDate.HasValue) - { - var utcEndDate = endDate.Value.Kind == DateTimeKind.Utc ? endDate.Value : DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc); - query = query.Where(l => l.Timestamp <= utcEndDate); - } - - return await query.OrderByDescending(l => l.Timestamp).ToListAsync(); - } - - /// - public async Task> GetByProviderAsync(int providerId, DateTime? startDate = null, DateTime? endDate = null) - { - var query = _context.AudioUsageLogs.Where(l => l.ProviderId == providerId); - - if (startDate.HasValue) - { - var utcStartDate = startDate.Value.Kind == DateTimeKind.Utc ? startDate.Value : DateTime.SpecifyKind(startDate.Value, DateTimeKind.Utc); - query = query.Where(l => l.Timestamp >= utcStartDate); - } - - if (endDate.HasValue) - { - var utcEndDate = endDate.Value.Kind == DateTimeKind.Utc ? endDate.Value : DateTime.SpecifyKind(endDate.Value, DateTimeKind.Utc); - query = query.Where(l => l.Timestamp <= utcEndDate); - } - - return await query.OrderByDescending(l => l.Timestamp).ToListAsync(); - } - - /// - public async Task> GetBySessionIdAsync(string sessionId) - { - return await _context.AudioUsageLogs - .Where(l => l.SessionId == sessionId) - .OrderBy(l => l.Timestamp) - .ToListAsync(); - } - - /// - public async Task GetTotalCostAsync(string virtualKey, DateTime startDate, DateTime endDate) - { - // Ensure dates are in UTC for PostgreSQL - var utcStartDate = startDate.Kind == DateTimeKind.Utc ? startDate : DateTime.SpecifyKind(startDate, DateTimeKind.Utc); - var utcEndDate = endDate.Kind == DateTimeKind.Utc ? endDate : DateTime.SpecifyKind(endDate, DateTimeKind.Utc); - - return await _context.AudioUsageLogs - .Where(l => l.VirtualKey == virtualKey && - l.Timestamp >= utcStartDate && - l.Timestamp <= utcEndDate) - .SumAsync(l => l.Cost); - } - - /// - public async Task> GetOperationBreakdownAsync(DateTime startDate, DateTime endDate, string? virtualKey = null) - { - // Ensure dates are in UTC for PostgreSQL - var utcStartDate = startDate.Kind == DateTimeKind.Utc ? startDate : DateTime.SpecifyKind(startDate, DateTimeKind.Utc); - var utcEndDate = endDate.Kind == DateTimeKind.Utc ? endDate : DateTime.SpecifyKind(endDate, DateTimeKind.Utc); - - var query = _context.AudioUsageLogs - .Where(l => l.Timestamp >= utcStartDate && l.Timestamp <= utcEndDate); - - if (!string.IsNullOrEmpty(virtualKey)) - query = query.Where(l => l.VirtualKey == virtualKey); - - var breakdown = await query - .GroupBy(l => l.OperationType) - .Select(g => new OperationTypeBreakdown - { - OperationType = g.Key, - Count = g.Count(), - TotalCost = g.Sum(l => l.Cost), - AverageCost = g.Average(l => l.Cost) - }) - .OrderByDescending(b => b.TotalCost) - .ToListAsync(); - - return breakdown; - } - - /// - public async Task> GetProviderBreakdownAsync(DateTime startDate, DateTime endDate, string? virtualKey = null) - { - // Ensure dates are in UTC for PostgreSQL - var utcStartDate = startDate.Kind == DateTimeKind.Utc ? startDate : DateTime.SpecifyKind(startDate, DateTimeKind.Utc); - var utcEndDate = endDate.Kind == DateTimeKind.Utc ? endDate : DateTime.SpecifyKind(endDate, DateTimeKind.Utc); - - var query = _context.AudioUsageLogs - .Where(l => l.Timestamp >= utcStartDate && l.Timestamp <= utcEndDate); - - if (!string.IsNullOrEmpty(virtualKey)) - query = query.Where(l => l.VirtualKey == virtualKey); - - var breakdown = await query - .Include(l => l.Provider) - .GroupBy(l => new { l.ProviderId, ProviderName = l.Provider!.ProviderName }) - .Select(g => new ProviderBreakdown - { - ProviderId = g.Key.ProviderId, - ProviderName = g.Key.ProviderName, - Count = g.Count(), - TotalCost = g.Sum(l => l.Cost), - SuccessRate = g.Count() > 0 ? (g.Count(l => l.StatusCode == null || (l.StatusCode >= 200 && l.StatusCode < 300)) / (double)g.Count()) * 100 : 0 - }) - .OrderByDescending(b => b.TotalCost) - .ToListAsync(); - - return breakdown; - } - - /// - public async Task> GetVirtualKeyBreakdownAsync(DateTime startDate, DateTime endDate, int? providerId = null) - { - // Ensure dates are in UTC for PostgreSQL - var utcStartDate = startDate.Kind == DateTimeKind.Utc ? startDate : DateTime.SpecifyKind(startDate, DateTimeKind.Utc); - var utcEndDate = endDate.Kind == DateTimeKind.Utc ? endDate : DateTime.SpecifyKind(endDate, DateTimeKind.Utc); - - var query = _context.AudioUsageLogs - .Where(l => l.Timestamp >= utcStartDate && l.Timestamp <= utcEndDate); - - if (providerId.HasValue) - { - query = query.Where(l => l.ProviderId == providerId.Value); - } - - var breakdown = await query - .GroupBy(l => l.VirtualKey) - .Select(g => new VirtualKeyBreakdown - { - VirtualKey = g.Key, - Count = g.Count(), - TotalCost = g.Sum(l => l.Cost) - }) - .OrderByDescending(b => b.TotalCost) - .Take(20) // Top 20 keys by cost - .ToListAsync(); - - // Optionally fetch key names from VirtualKeys table - var keyHashes = breakdown.Select(b => b.VirtualKey).ToList(); - var keyNames = await _context.VirtualKeys - .Where(k => keyHashes.Contains(k.KeyHash)) - .Select(k => new { k.KeyHash, k.KeyName }) - .ToDictionaryAsync(k => k.KeyHash, k => k.KeyName); - - foreach (var item in breakdown) - { - if (keyNames.TryGetValue(item.VirtualKey, out var name)) - { - item.KeyName = name; - } - } - - return breakdown; - } - - /// - public async Task DeleteOldLogsAsync(DateTime cutoffDate) - { - // Ensure date is in UTC for PostgreSQL - var utcCutoffDate = cutoffDate.Kind == DateTimeKind.Utc ? cutoffDate : DateTime.SpecifyKind(cutoffDate, DateTimeKind.Utc); - - var logsToDelete = await _context.AudioUsageLogs - .Where(l => l.Timestamp < utcCutoffDate) - .ToListAsync(); - - _context.AudioUsageLogs.RemoveRange(logsToDelete); - await _context.SaveChangesAsync(); - - return logsToDelete.Count; - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/FallbackConfigurationRepository.cs b/ConduitLLM.Configuration/Repositories/FallbackConfigurationRepository.cs deleted file mode 100644 index 5f0de64b3..000000000 --- a/ConduitLLM.Configuration/Repositories/FallbackConfigurationRepository.cs +++ /dev/null @@ -1,261 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for fallback configurations using Entity Framework Core - /// - public class FallbackConfigurationRepository : IFallbackConfigurationRepository - { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - /// - /// Creates a new instance of the repository - /// - /// The database context factory - /// The logger - public FallbackConfigurationRepository( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackConfigurations - .AsNoTracking() - .FirstOrDefaultAsync(f => f.Id == id, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting fallback configuration with ID {ConfigId}", id); - throw; - } - } - - /// - public async Task GetActiveConfigAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackConfigurations - .AsNoTracking() - .FirstOrDefaultAsync(f => f.IsActive, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting active fallback configuration"); - throw; - } - } - - /// - public async Task> GetAllAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackConfigurations - .AsNoTracking() - .OrderByDescending(f => f.IsActive) - .ThenByDescending(f => f.UpdatedAt) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all fallback configurations"); - throw; - } - } - - /// - public async Task CreateAsync(FallbackConfigurationEntity fallbackConfig, CancellationToken cancellationToken = default) - { - if (fallbackConfig == null) - { - throw new ArgumentNullException(nameof(fallbackConfig)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set timestamps - fallbackConfig.CreatedAt = DateTime.UtcNow; - fallbackConfig.UpdatedAt = DateTime.UtcNow; - - if (fallbackConfig.IsActive) - { - // Deactivate all other configs - var activeConfigs = await dbContext.FallbackConfigurations - .Where(f => f.IsActive) - .ToListAsync(cancellationToken); - - foreach (var config in activeConfigs) - { - config.IsActive = false; - } - } - - dbContext.FallbackConfigurations.Add(fallbackConfig); - await dbContext.SaveChangesAsync(cancellationToken); - return fallbackConfig.Id; - } - catch (DbUpdateException ex) - { - _logger.LogError(ex, "Database error creating fallback configuration '{ConfigName}'", - fallbackConfig.Name.Replace(Environment.NewLine, "")); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating fallback configuration '{ConfigName}'", - fallbackConfig.Name.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task UpdateAsync(FallbackConfigurationEntity fallbackConfig, CancellationToken cancellationToken = default) - { - if (fallbackConfig == null) - { - throw new ArgumentNullException(nameof(fallbackConfig)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set updated timestamp - fallbackConfig.UpdatedAt = DateTime.UtcNow; - - if (fallbackConfig.IsActive) - { - // Deactivate all other configs - var activeConfigs = await dbContext.FallbackConfigurations - .Where(f => f.IsActive && f.Id != fallbackConfig.Id) - .ToListAsync(cancellationToken); - - foreach (var config in activeConfigs) - { - config.IsActive = false; - } - } - - dbContext.FallbackConfigurations.Update(fallbackConfig); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating fallback configuration with ID {ConfigId}", - fallbackConfig.Id); - throw; - } - } - - /// - public async Task ActivateAsync(Guid id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - var fallbackConfig = await dbContext.FallbackConfigurations.FindAsync(new object[] { id }, cancellationToken); - if (fallbackConfig == null) - { - return false; - } - - // Deactivate all configs - var configs = await dbContext.FallbackConfigurations.ToListAsync(cancellationToken); - foreach (var config in configs) - { - config.IsActive = (config.Id == id); - config.UpdatedAt = DateTime.UtcNow; - } - - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error activating fallback configuration with ID {ConfigId}", id); - throw; - } - } - - /// - public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var fallbackConfig = await dbContext.FallbackConfigurations.FindAsync(new object[] { id }, cancellationToken); - - if (fallbackConfig == null) - { - return false; - } - - if (fallbackConfig.IsActive) - { - _logger.LogWarning("Attempting to delete active fallback configuration {ConfigId}", id); - // You might want to prevent this or activate another config - } - - // Check for related mappings - var mappings = await dbContext.FallbackModelMappings - .Where(m => m.FallbackConfigurationId == id) - .ToListAsync(cancellationToken); - - if (mappings.Count() > 0) - { - // Remove related mappings if there are any - dbContext.FallbackModelMappings.RemoveRange(mappings); - } - - dbContext.FallbackConfigurations.Remove(fallbackConfig); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting fallback configuration with ID {ConfigId}", id); - throw; - } - } - - /// - public async Task> GetMappingsAsync(Guid fallbackConfigId, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackModelMappings - .AsNoTracking() - .Where(m => m.FallbackConfigurationId == fallbackConfigId) - .OrderBy(m => m.SourceModelName) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting mappings for fallback configuration {ConfigId}", fallbackConfigId); - throw; - } - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/FallbackModelMappingRepository.cs b/ConduitLLM.Configuration/Repositories/FallbackModelMappingRepository.cs deleted file mode 100644 index 82f2be761..000000000 --- a/ConduitLLM.Configuration/Repositories/FallbackModelMappingRepository.cs +++ /dev/null @@ -1,243 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for fallback model mappings using Entity Framework Core - /// - public class FallbackModelMappingRepository : IFallbackModelMappingRepository - { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - /// - /// Creates a new instance of the repository - /// - /// The database context factory - /// The logger - public FallbackModelMappingRepository( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackModelMappings - .AsNoTracking() - .Include(m => m.FallbackConfiguration) - .FirstOrDefaultAsync(m => m.Id == id, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting fallback model mapping with ID {MappingId}", id); - throw; - } - } - - /// - public async Task GetBySourceModelAsync( - Guid fallbackConfigId, - string sourceModelName, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(sourceModelName)) - { - throw new ArgumentException("Source model name cannot be null or empty", nameof(sourceModelName)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackModelMappings - .AsNoTracking() - .Include(m => m.FallbackConfiguration) - .FirstOrDefaultAsync(m => - m.FallbackConfigurationId == fallbackConfigId && - m.SourceModelName == sourceModelName, - cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting fallback model mapping for source model {SourceModel} in config {ConfigId}", - sourceModelName.Replace(Environment.NewLine, ""), fallbackConfigId); - throw; - } - } - - /// - public async Task> GetByFallbackConfigIdAsync( - Guid fallbackConfigId, - CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackModelMappings - .AsNoTracking() - .Where(m => m.FallbackConfigurationId == fallbackConfigId) - .OrderBy(m => m.SourceModelName) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting fallback model mappings for config {ConfigId}", fallbackConfigId); - throw; - } - } - - /// - public async Task> GetAllAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.FallbackModelMappings - .AsNoTracking() - .Include(m => m.FallbackConfiguration) - .OrderBy(m => m.FallbackConfiguration != null ? m.FallbackConfiguration.Name : string.Empty) - .ThenBy(m => m.SourceModelName) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all fallback model mappings"); - throw; - } - } - - /// - public async Task CreateAsync(FallbackModelMappingEntity fallbackModelMapping, CancellationToken cancellationToken = default) - { - if (fallbackModelMapping == null) - { - throw new ArgumentNullException(nameof(fallbackModelMapping)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set timestamps - fallbackModelMapping.CreatedAt = DateTime.UtcNow; - fallbackModelMapping.UpdatedAt = DateTime.UtcNow; - - // Check if the mapping already exists - var existingMapping = await dbContext.FallbackModelMappings - .FirstOrDefaultAsync(m => - m.FallbackConfigurationId == fallbackModelMapping.FallbackConfigurationId && - m.SourceModelName == fallbackModelMapping.SourceModelName, - cancellationToken); - - if (existingMapping != null) - { - throw new InvalidOperationException( - $"A mapping for source model '{fallbackModelMapping.SourceModelName}' " + - $"already exists in fallback configuration ID {fallbackModelMapping.FallbackConfigurationId}"); - } - - // Verify that the fallback configuration exists - var configExists = await dbContext.FallbackConfigurations - .AnyAsync(f => f.Id == fallbackModelMapping.FallbackConfigurationId, cancellationToken); - - if (!configExists) - { - throw new InvalidOperationException( - $"Fallback configuration with ID {fallbackModelMapping.FallbackConfigurationId} does not exist"); - } - - dbContext.FallbackModelMappings.Add(fallbackModelMapping); - await dbContext.SaveChangesAsync(cancellationToken); - return fallbackModelMapping.Id; - } - catch (DbUpdateException ex) - { - _logger.LogError(ex, "Database error creating fallback model mapping for source model '{SourceModel}'", - fallbackModelMapping.SourceModelName.Replace(Environment.NewLine, "")); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating fallback model mapping for source model '{SourceModel}'", - fallbackModelMapping.SourceModelName.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task UpdateAsync(FallbackModelMappingEntity fallbackModelMapping, CancellationToken cancellationToken = default) - { - if (fallbackModelMapping == null) - { - throw new ArgumentNullException(nameof(fallbackModelMapping)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set updated timestamp - fallbackModelMapping.UpdatedAt = DateTime.UtcNow; - - // Check if we're changing the source model name and if that would create a duplicate - var existingMapping = await dbContext.FallbackModelMappings - .FirstOrDefaultAsync(m => - m.Id != fallbackModelMapping.Id && - m.FallbackConfigurationId == fallbackModelMapping.FallbackConfigurationId && - m.SourceModelName == fallbackModelMapping.SourceModelName, - cancellationToken); - - if (existingMapping != null) - { - throw new InvalidOperationException( - $"Another mapping for source model '{fallbackModelMapping.SourceModelName}' " + - $"already exists in fallback configuration ID {fallbackModelMapping.FallbackConfigurationId}"); - } - - dbContext.FallbackModelMappings.Update(fallbackModelMapping); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating fallback model mapping with ID {MappingId}", - fallbackModelMapping.Id); - throw; - } - } - - /// - public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var fallbackModelMapping = await dbContext.FallbackModelMappings.FindAsync(new object[] { id }, cancellationToken); - - if (fallbackModelMapping == null) - { - return false; - } - - dbContext.FallbackModelMappings.Remove(fallbackModelMapping); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting fallback model mapping with ID {MappingId}", id); - throw; - } - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/ModelDeploymentRepository.cs b/ConduitLLM.Configuration/Repositories/ModelDeploymentRepository.cs deleted file mode 100644 index 1225e34ad..000000000 --- a/ConduitLLM.Configuration/Repositories/ModelDeploymentRepository.cs +++ /dev/null @@ -1,216 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for model deployments using Entity Framework Core - /// - public class ModelDeploymentRepository : IModelDeploymentRepository - { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - /// - /// Creates a new instance of the repository - /// - /// The database context factory - /// The logger - public ModelDeploymentRepository( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ModelDeployments - .AsNoTracking() - .FirstOrDefaultAsync(d => d.Id == id, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model deployment with ID {DeploymentId}", id); - throw; - } - } - - /// - public async Task GetByDeploymentNameAsync(string deploymentName, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(deploymentName)) - { - throw new ArgumentException("Deployment name cannot be null or empty", nameof(deploymentName)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ModelDeployments - .AsNoTracking() - .FirstOrDefaultAsync(d => d.DeploymentName == deploymentName, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model deployment with name {DeploymentName}", deploymentName.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task> GetByProviderAsync(ProviderType providerType, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ModelDeployments - .AsNoTracking() - .Where(d => d.Provider.ProviderType == providerType) - .OrderBy(d => d.ModelName) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model deployments for provider {ProviderType}", providerType); - throw; - } - } - - /// - public async Task> GetByModelNameAsync(string modelName, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(modelName)) - { - throw new ArgumentException("Model name cannot be null or empty", nameof(modelName)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ModelDeployments - .AsNoTracking() - .Where(d => d.ModelName == modelName) - .OrderBy(d => d.Provider.ProviderType) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting model deployments for model {ModelName}", modelName.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task> GetAllAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.ModelDeployments - .AsNoTracking() - .OrderBy(d => d.Provider.ProviderType) - .ThenBy(d => d.ModelName) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all model deployments"); - throw; - } - } - - /// - public async Task CreateAsync(ModelDeploymentEntity modelDeployment, CancellationToken cancellationToken = default) - { - if (modelDeployment == null) - { - throw new ArgumentNullException(nameof(modelDeployment)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set timestamps - modelDeployment.CreatedAt = DateTime.UtcNow; - modelDeployment.UpdatedAt = DateTime.UtcNow; - - dbContext.ModelDeployments.Add(modelDeployment); - await dbContext.SaveChangesAsync(cancellationToken); - return modelDeployment.Id; - } - catch (DbUpdateException ex) - { - _logger.LogError(ex, "Database error creating model deployment '{DeploymentName}'", - modelDeployment.DeploymentName.Replace(Environment.NewLine, "")); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating model deployment '{DeploymentName}'", - modelDeployment.DeploymentName.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task UpdateAsync(ModelDeploymentEntity modelDeployment, CancellationToken cancellationToken = default) - { - if (modelDeployment == null) - { - throw new ArgumentNullException(nameof(modelDeployment)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set updated timestamp - modelDeployment.UpdatedAt = DateTime.UtcNow; - - dbContext.ModelDeployments.Update(modelDeployment); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating model deployment with ID {DeploymentId}", - modelDeployment.Id); - throw; - } - } - - /// - public async Task DeleteAsync(Guid id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var modelDeployment = await dbContext.ModelDeployments.FindAsync(new object[] { id }, cancellationToken); - - if (modelDeployment == null) - { - return false; - } - - dbContext.ModelDeployments.Remove(modelDeployment); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting model deployment with ID {DeploymentId}", id); - throw; - } - } - } -} diff --git a/ConduitLLM.Configuration/Repositories/RouterConfigRepository.cs b/ConduitLLM.Configuration/Repositories/RouterConfigRepository.cs deleted file mode 100644 index 70368f465..000000000 --- a/ConduitLLM.Configuration/Repositories/RouterConfigRepository.cs +++ /dev/null @@ -1,231 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using Microsoft.EntityFrameworkCore; -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Configuration.Repositories -{ - /// - /// Repository implementation for router configurations using Entity Framework Core - /// - public class RouterConfigRepository : IRouterConfigRepository - { - private readonly IDbContextFactory _dbContextFactory; - private readonly ILogger _logger; - - /// - /// Creates a new instance of the repository - /// - /// The database context factory - /// The logger - public RouterConfigRepository( - IDbContextFactory dbContextFactory, - ILogger logger) - { - _dbContextFactory = dbContextFactory ?? throw new ArgumentNullException(nameof(dbContextFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetByIdAsync(int id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.RouterConfigs - .AsNoTracking() - .FirstOrDefaultAsync(r => r.Id == id, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting router configuration with ID {ConfigId}", id); - throw; - } - } - - /// - public async Task GetActiveConfigAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.RouterConfigs - .AsNoTracking() - .FirstOrDefaultAsync(r => r.IsActive, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting active router configuration"); - throw; - } - } - - /// - public async Task> GetAllAsync(CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - return await dbContext.RouterConfigs - .AsNoTracking() - .OrderByDescending(r => r.IsActive) - .ThenByDescending(r => r.UpdatedAt) - .ToListAsync(cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting all router configurations"); - throw; - } - } - - /// - public async Task CreateAsync(RouterConfigEntity routerConfig, CancellationToken cancellationToken = default) - { - if (routerConfig == null) - { - throw new ArgumentNullException(nameof(routerConfig)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set timestamps - routerConfig.CreatedAt = DateTime.UtcNow; - routerConfig.UpdatedAt = DateTime.UtcNow; - - if (routerConfig.IsActive) - { - // Deactivate all other configs - var activeConfigs = await dbContext.RouterConfigs - .Where(r => r.IsActive) - .ToListAsync(cancellationToken); - - foreach (var config in activeConfigs) - { - config.IsActive = false; - } - } - - dbContext.RouterConfigs.Add(routerConfig); - await dbContext.SaveChangesAsync(cancellationToken); - return routerConfig.Id; - } - catch (DbUpdateException ex) - { - _logger.LogError(ex, "Database error creating router configuration '{ConfigName}'", - routerConfig.Name.Replace(Environment.NewLine, "")); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error creating router configuration '{ConfigName}'", - routerConfig.Name.Replace(Environment.NewLine, "")); - throw; - } - } - - /// - public async Task UpdateAsync(RouterConfigEntity routerConfig, CancellationToken cancellationToken = default) - { - if (routerConfig == null) - { - throw new ArgumentNullException(nameof(routerConfig)); - } - - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - // Set updated timestamp - routerConfig.UpdatedAt = DateTime.UtcNow; - - if (routerConfig.IsActive) - { - // Deactivate all other configs - var activeConfigs = await dbContext.RouterConfigs - .Where(r => r.IsActive && r.Id != routerConfig.Id) - .ToListAsync(cancellationToken); - - foreach (var config in activeConfigs) - { - config.IsActive = false; - } - } - - dbContext.RouterConfigs.Update(routerConfig); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error updating router configuration with ID {ConfigId}", - routerConfig.Id); - throw; - } - } - - /// - public async Task ActivateAsync(int id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - - var routerConfig = await dbContext.RouterConfigs.FindAsync(new object[] { id }, cancellationToken); - if (routerConfig == null) - { - return false; - } - - // Deactivate all configs - var configs = await dbContext.RouterConfigs.ToListAsync(cancellationToken); - foreach (var config in configs) - { - config.IsActive = (config.Id == id); - config.UpdatedAt = DateTime.UtcNow; - } - - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error activating router configuration with ID {ConfigId}", id); - throw; - } - } - - /// - public async Task DeleteAsync(int id, CancellationToken cancellationToken = default) - { - try - { - using var dbContext = await _dbContextFactory.CreateDbContextAsync(cancellationToken); - var routerConfig = await dbContext.RouterConfigs.FindAsync(new object[] { id }, cancellationToken); - - if (routerConfig == null) - { - return false; - } - - if (routerConfig.IsActive) - { - _logger.LogWarning("Attempting to delete active router configuration {ConfigId}", id); - // You might want to prevent this or activate another config - } - - dbContext.RouterConfigs.Remove(routerConfig); - int rowsAffected = await dbContext.SaveChangesAsync(cancellationToken); - return rowsAffected > 0; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error deleting router configuration with ID {ConfigId}", id); - throw; - } - } - } -} diff --git a/ConduitLLM.Core/Conduit.cs b/ConduitLLM.Core/Conduit.cs index 6b6fc2f61..75525a96d 100644 --- a/ConduitLLM.Core/Conduit.cs +++ b/ConduitLLM.Core/Conduit.cs @@ -16,7 +16,6 @@ namespace ConduitLLM.Core public class Conduit : IConduit { private readonly ILLMClientFactory _clientFactory; - private readonly ILLMRouter? _router; private readonly IContextManager? _contextManager; private readonly IModelProviderMappingService? _modelProviderMappingService; private readonly IOptions? _contextOptions; @@ -27,7 +26,6 @@ public class Conduit : IConduit /// /// The factory used to obtain provider-specific LLM clients. /// Logger instance. - /// Optional router for load balancing and fallback (if null, direct model calls will be used). /// Optional context manager for handling token limits. /// Optional service to retrieve model mappings. /// Optional configuration for context management. @@ -35,14 +33,12 @@ public class Conduit : IConduit public Conduit( ILLMClientFactory clientFactory, ILogger logger, - ILLMRouter? router = null, IContextManager? contextManager = null, IModelProviderMappingService? modelProviderMappingService = null, IOptions? contextOptions = null) { _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _router = router; _contextManager = contextManager; _modelProviderMappingService = modelProviderMappingService; _contextOptions = contextOptions; @@ -74,33 +70,13 @@ public async Task CreateChatCompletionAsync( // Apply context management if enabled request = await ApplyContextManagementAsync(request); - // If a router is configured and the model uses the 'router:' prefix, use the router - if (_router != null && IsRouterRequest(request.Model)) - { - // Extract the routing strategy if specified in the model name - var (routingStrategy, actualModel) = ExtractRoutingInfoFromModel(request.Model); - - // Set the cleaned model name back in the request if provided - if (!string.IsNullOrEmpty(actualModel)) - { - request.Model = actualModel; - } - - // Use the router for this request - return await _router.CreateChatCompletionAsync(request, routingStrategy, apiKey, cancellationToken) - .ConfigureAwait(false); - } - else - { - // Use direct model access via client factory (original behavior) - // 1. Get the appropriate client from the factory based on the model alias in the request - ILLMClient client = _clientFactory.GetClient(request.Model); + // Get the appropriate client from the factory based on the model alias in the request + ILLMClient client = _clientFactory.GetClient(request.Model); - // 2. Call the client's method, passing the optional apiKey - // Exceptions specific to providers (like communication errors) are expected to bubble up from the client. - // The factory handles ConfigurationException and UnsupportedProviderException. - return await client.CreateChatCompletionAsync(request, apiKey, cancellationToken).ConfigureAwait(false); - } + // Call the client's method, passing the optional apiKey + // Exceptions specific to providers (like communication errors) are expected to bubble up from the client. + // The factory handles ConfigurationException and UnsupportedProviderException. + return await client.CreateChatCompletionAsync(request, apiKey, cancellationToken).ConfigureAwait(false); } /// @@ -129,37 +105,15 @@ public async IAsyncEnumerable StreamChatCompletionAsync( // Apply context management if enabled request = await ApplyContextManagementAsync(request); - // If a router is configured and the model uses the 'router:' prefix, use the router - if (_router != null && IsRouterRequest(request.Model)) - { - // Extract the routing strategy if specified in the model name - var (routingStrategy, actualModel) = ExtractRoutingInfoFromModel(request.Model); - - // Set the cleaned model name back in the request if provided - if (!string.IsNullOrEmpty(actualModel)) - { - request.Model = actualModel; - } + // Get the appropriate client from the factory based on the model alias in the request + ILLMClient client = _clientFactory.GetClient(request.Model); - // Use the router for this streaming request - await foreach (var chunk in _router.StreamChatCompletionAsync(request, routingStrategy, apiKey, cancellationToken)) - { - yield return chunk; - } - } - else + // Call the client's streaming method, passing the optional apiKey + // Exceptions specific to providers (like communication errors) are expected to bubble up from the client. + // The factory handles ConfigurationException and UnsupportedProviderException. + await foreach (var chunk in client.StreamChatCompletionAsync(request, apiKey, cancellationToken)) { - // Use direct model access via client factory (original behavior) - // 1. Get the appropriate client from the factory based on the model alias in the request - ILLMClient client = _clientFactory.GetClient(request.Model); - - // 2. Call the client's streaming method, passing the optional apiKey - // Exceptions specific to providers (like communication errors) are expected to bubble up from the client. - // The factory handles ConfigurationException and UnsupportedProviderException. - await foreach (var chunk in client.StreamChatCompletionAsync(request, apiKey, cancellationToken)) - { - yield return chunk; - } + yield return chunk; } } @@ -273,79 +227,6 @@ public async Task CreateImageAsync( return await client.CreateImageAsync(request, apiKey, cancellationToken).ConfigureAwait(false); } - /// - /// Gets the router instance if one is configured - /// - /// The router instance or null if none is configured - public ILLMRouter? GetRouter() => _router; - - /// - /// Determines if a model request should be handled by the router - /// - /// The model name to check - /// True if this is a router request, false otherwise - private bool IsRouterRequest(string modelName) - { - return modelName.StartsWith("router:", StringComparison.OrdinalIgnoreCase) || - modelName.Equals("router", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Extracts routing information from a model name - /// - /// The model name to parse - /// Tuple containing routing strategy and actual model name (both may be null) - private (string? strategy, string? model) ExtractRoutingInfoFromModel(string modelName) - { - // Default case: just "router" - if (modelName.Equals("router", StringComparison.OrdinalIgnoreCase)) - { - return (null, null); - } - - // Model name format: router:strategy:model or router:strategy or router:model - if (modelName.StartsWith("router:", StringComparison.OrdinalIgnoreCase)) - { - string remaining = modelName.Substring("router:".Length); - - // Split by colon to extract strategy and model (if present) - var parts = remaining.Split(':', 2); - - if (parts.Length == 2) - { - // Format: router:strategy:model - return (parts[0], parts[1]); - } - else - { - // Could be either router:strategy or router:model - // Check if the remaining part is a known strategy - if (IsKnownStrategy(parts[0])) - { - return (parts[0], null); - } - else - { - // Assume it's a model name - return (null, parts[0]); - } - } - } - - // Not a router format - return (null, modelName); - } - - /// - /// Checks if a string is a known routing strategy - /// - private bool IsKnownStrategy(string strategy) - { - // List of supported strategies - return new[] { "simple", "random", "roundrobin", "leastused", "passthrough" } - .Contains(strategy.ToLowerInvariant()); - } - /// /// Gets an LLM client for the specified model. /// diff --git a/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs b/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs index 012be64e9..82ca82614 100644 --- a/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs +++ b/ConduitLLM.Core/Extensions/ServiceCollectionExtensions.cs @@ -2,7 +2,6 @@ using ConduitLLM.Core.Configuration; using ConduitLLM.Core.Interfaces; using ConduitLLM.Core.Options; -using ConduitLLM.Core.Routing; using ConduitLLM.Core.Services; using Microsoft.Extensions.Configuration; @@ -45,65 +44,21 @@ public static IServiceCollection AddConduitContextManagement(this IServiceCollec } /// - /// Adds the ConduitLLM Audio services to the service collection. + /// Adds model capability detection and caching services to the service collection. /// /// The service collection to add services to. /// The configuration instance. /// The service collection for chaining. - public static IServiceCollection AddConduitAudioServices(this IServiceCollection services, IConfiguration configuration) + public static IServiceCollection AddModelCapabilityServices(this IServiceCollection services, IConfiguration configuration) { // Register model capability service if not already registered - use database-backed implementation services.TryAddScoped(); - // Register audio capability detector - services.AddScoped(); - - // Register audio router - services.AddScoped(); - // Register capability detector if not already registered services.TryAddScoped(); - // Register hybrid audio service for STT-LLM-TTS pipeline - services.AddScoped(); - - // Register audio processing service for format conversion, compression, etc. - services.AddScoped(); - - - // Register security services - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - services.AddScoped(); - // Register performance optimization services - services.AddMemoryCache(); // For AudioStreamCache - services.AddSingleton(); - services.AddScoped(); - services.AddScoped(); - - // Register monitoring and observability services - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - services.AddSingleton(); - // MonitoringAudioService requires IAudioTranscriptionClient which is obtained dynamically from providers - // services.AddScoped(); - - // Register configuration options - services.Configure( - configuration.GetSection("ConduitLLM:Audio:ConnectionPool")); - services.Configure( - configuration.GetSection("ConduitLLM:Audio:Cache")); - services.Configure( - configuration.GetSection("ConduitLLM:Audio:Cdn")); - services.Configure( - configuration.GetSection("ConduitLLM:Audio:Metrics")); - services.Configure( - configuration.GetSection("ConduitLLM:Audio:Alerting")); - services.Configure( - configuration.GetSection("ConduitLLM:Audio:Tracing")); + services.AddMemoryCache(); return services; } diff --git a/ConduitLLM.Core/Interfaces/IAudioAlertingService.cs b/ConduitLLM.Core/Interfaces/IAudioAlertingService.cs deleted file mode 100644 index 5ea1d02b4..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioAlertingService.cs +++ /dev/null @@ -1,270 +0,0 @@ -using ConduitLLM.Core.Models; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for audio alerting and monitoring. - /// - public interface IAudioAlertingService - { - /// - /// Registers an alert rule. - /// - /// The alert rule to register. - /// The rule ID. - Task RegisterAlertRuleAsync(AudioAlertRule rule); - - /// - /// Updates an existing alert rule. - /// - /// The rule ID. - /// The updated rule. - Task UpdateAlertRuleAsync(string ruleId, AudioAlertRule rule); - - /// - /// Deletes an alert rule. - /// - /// The rule ID to delete. - Task DeleteAlertRuleAsync(string ruleId); - - /// - /// Gets all active alert rules. - /// - /// List of active alert rules. - Task> GetActiveRulesAsync(); - - /// - /// Evaluates metrics against alert rules. - /// - /// The metrics to evaluate. - /// Cancellation token. - Task EvaluateMetricsAsync( - AudioMetricsSnapshot metrics, - CancellationToken cancellationToken = default); - - /// - /// Gets alert history. - /// - /// Start time for history. - /// End time for history. - /// Optional severity filter. - /// List of triggered alerts. - Task> GetAlertHistoryAsync( - DateTime startTime, - DateTime endTime, - AlertSeverity? severity = null); - - /// - /// Acknowledges an alert. - /// - /// The alert ID to acknowledge. - /// Who acknowledged the alert. - /// Optional notes. - Task AcknowledgeAlertAsync( - string alertId, - string acknowledgedBy, - string? notes = null); - - /// - /// Tests an alert rule. - /// - /// The rule to test. - /// Test results. - Task TestAlertRuleAsync(AudioAlertRule rule); - } - - /// - /// Audio alert rule definition. - /// - public class AudioAlertRule - { - /// - /// Gets or sets the rule ID. - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Gets or sets the rule name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the rule description. - /// - public string? Description { get; set; } - - /// - /// Gets or sets whether the rule is enabled. - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Gets or sets the metric to monitor. - /// - public AudioMetricType MetricType { get; set; } - - /// - /// Gets or sets the condition. - /// - public AlertCondition Condition { get; set; } = new(); - - /// - /// Gets or sets the severity. - /// - public AlertSeverity Severity { get; set; } - - /// - /// Gets or sets the notification channels. - /// - public List NotificationChannels { get; set; } = new(); - - /// - /// Gets or sets the cooldown period. - /// - public TimeSpan CooldownPeriod { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// Gets or sets custom tags. - /// - public Dictionary Tags { get; set; } = new(); - } - - /// - /// Types of audio metrics to monitor. - /// - public enum AudioMetricType - { - /// - /// Error rate across all operations. - /// - ErrorRate, - - /// - /// Average latency. - /// - Latency, - - /// - /// Provider availability. - /// - ProviderAvailability, - - /// - /// Cache hit rate. - /// - CacheHitRate, - - /// - /// Active sessions count. - /// - ActiveSessions, - - /// - /// Request rate. - /// - RequestRate, - - /// - /// Cost per hour. - /// - CostRate, - - /// - /// Connection pool utilization. - /// - ConnectionPoolUtilization, - - /// - /// Audio processing queue length. - /// - QueueLength, - - /// - /// Custom metric. - /// - Custom - } - - - /// - /// Triggered alert instance. - /// - public class TriggeredAlert - { - /// - /// Gets or sets the alert ID. - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Gets or sets the rule that triggered this alert. - /// - public AudioAlertRule Rule { get; set; } = new(); - - /// - /// Gets or sets when the alert was triggered. - /// - public DateTime TriggeredAt { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the metric value that triggered the alert. - /// - public double MetricValue { get; set; } - - /// - /// Gets or sets the alert message. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Gets or sets alert details. - /// - public Dictionary Details { get; set; } = new(); - - /// - /// Gets or sets the alert state. - /// - public AlertState State { get; set; } - - /// - /// Gets or sets who acknowledged the alert. - /// - public string? AcknowledgedBy { get; set; } - - /// - /// Gets or sets when the alert was acknowledged. - /// - public DateTime? AcknowledgedAt { get; set; } - - /// - /// Gets or sets acknowledgment notes. - /// - public string? AcknowledgmentNotes { get; set; } - - /// - /// Gets or sets when the alert was resolved. - /// - public DateTime? ResolvedAt { get; set; } - } - - /// - /// Audio-specific notification test result. - /// - public class AudioNotificationTestResult - { - /// - /// Gets or sets the channel type. - /// - public NotificationChannelType ChannelType { get; set; } - - /// - /// Gets or sets whether the test succeeded. - /// - public bool Success { get; set; } - - /// - /// Gets or sets the error message if failed. - /// - public string? ErrorMessage { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioAuditLogger.cs b/ConduitLLM.Core/Interfaces/IAudioAuditLogger.cs deleted file mode 100644 index b6f2ad6cc..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioAuditLogger.cs +++ /dev/null @@ -1,196 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for audio operation audit logging. - /// - public interface IAudioAuditLogger - { - /// - /// Logs an audio transcription operation. - /// - /// The audit log entry. - /// Cancellation token. - Task LogTranscriptionAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default); - - /// - /// Logs a text-to-speech operation. - /// - /// The audit log entry. - /// Cancellation token. - Task LogTextToSpeechAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default); - - /// - /// Logs a real-time audio session. - /// - /// The audit log entry. - /// Cancellation token. - Task LogRealtimeSessionAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default); - - /// - /// Logs a content filtering event. - /// - /// The filtering audit entry. - /// Cancellation token. - Task LogContentFilteringAsync( - ContentFilterAuditEntry entry, - CancellationToken cancellationToken = default); - - /// - /// Logs a PII detection event. - /// - /// The PII audit entry. - /// Cancellation token. - Task LogPiiDetectionAsync( - PiiAuditEntry entry, - CancellationToken cancellationToken = default); - } - - /// - /// Base audit entry for audio operations. - /// - public class AudioAuditEntry - { - /// - /// Gets or sets the unique ID for this audit entry. - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// Gets or sets the timestamp of the operation. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the virtual key used. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Gets or sets the operation type. - /// - public AudioOperation Operation { get; set; } - - /// - /// Gets or sets the provider used. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets the model used. - /// - public string Model { get; set; } = string.Empty; - - /// - /// Gets or sets whether the operation was successful. - /// - public bool Success { get; set; } - - /// - /// Gets or sets the error message if failed. - /// - public string? ErrorMessage { get; set; } - - /// - /// Gets or sets the duration in milliseconds. - /// - public long DurationMs { get; set; } - - /// - /// Gets or sets the size in bytes. - /// - public long SizeBytes { get; set; } - - /// - /// Gets or sets the language code. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the client IP address. - /// - public string? ClientIp { get; set; } - - /// - /// Gets or sets the user agent. - /// - public string? UserAgent { get; set; } - - /// - /// Gets or sets custom metadata. - /// - public Dictionary Metadata { get; set; } = new(); - } - - /// - /// Audit entry for content filtering events. - /// - public class ContentFilterAuditEntry : AudioAuditEntry - { - /// - /// Gets or sets whether content was blocked. - /// - public bool WasBlocked { get; set; } - - /// - /// Gets or sets whether content was modified. - /// - public bool WasModified { get; set; } - - /// - /// Gets or sets the violation categories detected. - /// - public List ViolationCategories { get; set; } = new(); - - /// - /// Gets or sets the filter confidence score. - /// - public double ConfidenceScore { get; set; } - - /// - /// Gets or sets the original content hash. - /// - public string? ContentHash { get; set; } - } - - /// - /// Audit entry for PII detection events. - /// - public class PiiAuditEntry : AudioAuditEntry - { - /// - /// Gets or sets whether PII was detected. - /// - public bool PiiDetected { get; set; } - - /// - /// Gets or sets the types of PII found. - /// - public List PiiTypes { get; set; } = new(); - - /// - /// Gets or sets the number of PII entities found. - /// - public int EntityCount { get; set; } - - /// - /// Gets or sets whether PII was redacted. - /// - public bool WasRedacted { get; set; } - - /// - /// Gets or sets the redaction method used. - /// - public RedactionMethod? RedactionMethod { get; set; } - - /// - /// Gets or sets the risk score. - /// - public double RiskScore { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioCapabilityDetector.cs b/ConduitLLM.Core/Interfaces/IAudioCapabilityDetector.cs deleted file mode 100644 index 3eb6aa1ac..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioCapabilityDetector.cs +++ /dev/null @@ -1,252 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for detecting and validating audio capabilities across different providers and models. - /// - /// - /// - /// The IAudioCapabilityDetector provides a centralized way to determine which audio features - /// are supported by different providers and models. This is essential for routing decisions - /// and graceful feature degradation in multi-provider environments. - /// - /// - /// Key responsibilities include: - /// - /// - /// Identifying which providers support specific audio operations - /// Validating audio format compatibility - /// Checking voice availability across providers - /// Determining real-time conversation support - /// Validating language support for transcription and synthesis - /// - /// - public interface IAudioCapabilityDetector - { - /// - /// Determines if a provider supports audio transcription (Speech-to-Text). - /// - /// The provider ID from the Provider entity. - /// Optional specific model to check. If null, checks general provider support. - /// True if the provider/model supports transcription, false otherwise. - /// - /// This method helps determine routing for transcription requests. Some providers - /// may support transcription only with specific models (e.g., OpenAI's Whisper models). - /// - bool SupportsTranscription(int providerId, string? model = null); - - /// - /// Determines if a provider supports text-to-speech synthesis. - /// - /// The provider ID from the Provider entity. - /// Optional specific model to check. If null, checks general provider support. - /// True if the provider/model supports TTS, false otherwise. - /// - /// Useful for routing TTS requests to appropriate providers. Some providers specialize - /// in TTS (like ElevenLabs) while others offer it as an additional capability. - /// - bool SupportsTextToSpeech(int providerId, string? model = null); - - /// - /// Determines if a provider supports real-time conversational audio. - /// - /// The provider ID from the Provider entity. - /// Optional specific model to check. If null, checks general provider support. - /// True if the provider/model supports real-time audio, false otherwise. - /// - /// Real-time support is currently limited to specific providers and models. - /// This method helps identify which providers can handle bidirectional audio streaming. - /// - bool SupportsRealtime(int providerId, string? model = null); - - /// - /// Checks if a specific voice is available for a provider. - /// - /// The provider ID from the Provider entity. - /// The voice identifier to check. - /// True if the voice is available, false otherwise. - /// - /// Voice IDs are provider-specific. This method validates that a requested voice - /// exists before attempting to use it for TTS or real-time conversations. - /// - bool SupportsVoice(int providerId, string voiceId); - - /// - /// Gets the audio formats supported by a provider for a specific operation. - /// - /// The provider ID from the Provider entity. - /// The audio operation type (transcription, tts, realtime). - /// An array of supported audio format identifiers. - /// - /// - /// Different providers support different audio formats for input and output. - /// This method returns format identifiers like "mp3", "wav", "flac", "opus", etc. - /// - /// - /// For transcription, these are input formats. For TTS, these are output formats. - /// For real-time, separate input/output format queries may be needed. - /// - /// - AudioFormat[] GetSupportedFormats(int providerId, AudioOperation operation); - - /// - /// Gets the languages supported by a provider for a specific audio operation. - /// - /// The provider ID from the Provider entity. - /// The audio operation type. - /// A collection of ISO 639-1 language codes. - /// - /// Returns standard language codes (e.g., "en", "es", "fr", "zh") that the provider - /// supports for the specified operation. Some providers may support different languages - /// for transcription vs. synthesis. - /// - IEnumerable GetSupportedLanguages(int providerId, AudioOperation operation); - - /// - /// Validates that an audio request can be processed by the specified provider. - /// - /// The audio request to validate. - /// The target provider ID. - /// Detailed error message if validation fails. - /// True if the request is valid for the provider, false otherwise. - /// - /// - /// Performs comprehensive validation including: - /// - /// - /// Audio format compatibility - /// Language support verification - /// Voice availability (for TTS/realtime) - /// File size and duration limits - /// Sample rate compatibility - /// - /// - bool ValidateAudioRequest(AudioRequestBase request, int providerId, out string errorMessage); - - /// - /// Gets a list of all provider IDs that support a specific audio capability. - /// - /// The audio capability to check. - /// A collection of provider IDs that support the capability. - /// - /// Useful for discovering which providers can handle specific audio operations, - /// enabling intelligent routing and fallback strategies. - /// - IEnumerable GetProvidersWithCapability(AudioCapability capability); - - /// - /// Gets detailed capability information for a specific provider. - /// - /// The provider ID from the Provider entity. - /// Comprehensive capability information for the provider. - /// - /// Returns a detailed breakdown of all audio capabilities, supported formats, - /// languages, voices, and limitations for the specified provider. - /// - AudioProviderCapabilities GetProviderCapabilities(int providerId); - - /// - /// Determines the best provider for a specific audio request based on capabilities and requirements. - /// - /// The audio request with requirements. - /// List of available provider IDs to choose from. - /// The recommended provider ID, or null if none meet the requirements. - /// - /// - /// Analyzes the request requirements and matches them against provider capabilities - /// to recommend the most suitable provider. Considers factors like: - /// - /// - /// Format support - /// Language availability - /// Voice selection (for TTS) - /// Quality requirements - /// Cost considerations - /// - /// - int? RecommendProvider(AudioRequestBase request, IEnumerable availableProviderIds); - } - - /// - /// Enumeration of audio operations for capability checking. - /// - public enum AudioOperation - { - /// - /// Speech-to-text transcription. - /// - Transcription, - - /// - /// Text-to-speech synthesis. - /// - TextToSpeech, - - /// - /// Real-time conversational audio. - /// - Realtime, - - /// - /// Audio translation (transcription with translation). - /// - Translation - } - - /// - /// Enumeration of audio capabilities for provider discovery. - /// - public enum AudioCapability - { - /// - /// Basic speech-to-text transcription. - /// - BasicTranscription, - - /// - /// Transcription with word-level timestamps. - /// - TimestampedTranscription, - - /// - /// Basic text-to-speech synthesis. - /// - BasicTTS, - - /// - /// TTS with multiple voice options. - /// - MultiVoiceTTS, - - /// - /// TTS with emotional control. - /// - EmotionalTTS, - - /// - /// Real-time bidirectional audio. - /// - RealtimeConversation, - - /// - /// Voice cloning capabilities. - /// - VoiceCloning, - - /// - /// SSML (Speech Synthesis Markup Language) support. - /// - SSMLSupport, - - /// - /// Streaming audio output. - /// - StreamingAudio, - - /// - /// Function calling in real-time conversations. - /// - RealtimeFunctions - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioCdnService.cs b/ConduitLLM.Core/Interfaces/IAudioCdnService.cs deleted file mode 100644 index c81ee4e53..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioCdnService.cs +++ /dev/null @@ -1,250 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for CDN integration for audio content delivery. - /// - public interface IAudioCdnService - { - /// - /// Uploads audio content to CDN. - /// - /// The audio data to upload. - /// The content type (e.g., "audio/mp3"). - /// Optional metadata. - /// Cancellation token. - /// CDN URL for the uploaded content. - Task UploadAudioAsync( - byte[] audioData, - string contentType, - CdnMetadata? metadata = null, - CancellationToken cancellationToken = default); - - /// - /// Streams audio content to CDN with chunked upload. - /// - /// The audio stream. - /// The content type. - /// Optional metadata. - /// Cancellation token. - /// CDN URL for the uploaded content. - Task StreamUploadAsync( - Stream audioStream, - string contentType, - CdnMetadata? metadata = null, - CancellationToken cancellationToken = default); - - /// - /// Gets a CDN URL for cached content. - /// - /// The content key. - /// URL expiration time. - /// CDN URL or null if not found. - Task GetCdnUrlAsync( - string contentKey, - TimeSpan? expiresIn = null); - - /// - /// Invalidates CDN cache for specific content. - /// - /// The content key to invalidate. - /// Cancellation token. - Task InvalidateCacheAsync( - string contentKey, - CancellationToken cancellationToken = default); - - /// - /// Gets CDN usage statistics. - /// - /// Start date for statistics. - /// End date for statistics. - /// CDN usage statistics. - Task GetUsageStatisticsAsync( - DateTime? startDate = null, - DateTime? endDate = null); - - /// - /// Configures CDN edge locations for optimal delivery. - /// - /// Edge configuration. - /// Cancellation token. - Task ConfigureEdgeLocationsAsync( - CdnEdgeConfiguration config, - CancellationToken cancellationToken = default); - } - - /// - /// Result of CDN upload operation. - /// - public class CdnUploadResult - { - /// - /// Gets or sets the CDN URL. - /// - public string Url { get; set; } = string.Empty; - - /// - /// Gets or sets the content key. - /// - public string ContentKey { get; set; } = string.Empty; - - /// - /// Gets or sets the content hash. - /// - public string ContentHash { get; set; } = string.Empty; - - /// - /// Gets or sets the upload timestamp. - /// - public DateTime UploadedAt { get; set; } - - /// - /// Gets or sets the content size in bytes. - /// - public long SizeBytes { get; set; } - - /// - /// Gets or sets edge locations where content is cached. - /// - public List EdgeLocations { get; set; } = new(); - } - - /// - /// Metadata for CDN content. - /// - public class CdnMetadata - { - /// - /// Gets or sets the content duration in seconds. - /// - public double? DurationSeconds { get; set; } - - /// - /// Gets or sets the audio format. - /// - public string? AudioFormat { get; set; } - - /// - /// Gets or sets the bit rate. - /// - public int? BitRate { get; set; } - - /// - /// Gets or sets the language. - /// - public string? Language { get; set; } - - /// - /// Gets or sets cache control headers. - /// - public string? CacheControl { get; set; } - - /// - /// Gets or sets custom metadata. - /// - public Dictionary CustomMetadata { get; set; } = new(); - } - - /// - /// CDN usage statistics. - /// - public class CdnUsageStatistics - { - /// - /// Gets or sets total bandwidth used in bytes. - /// - public long TotalBandwidthBytes { get; set; } - - /// - /// Gets or sets total number of requests. - /// - public long TotalRequests { get; set; } - - /// - /// Gets or sets cache hit rate. - /// - public double CacheHitRate { get; set; } - - /// - /// Gets or sets average response time in milliseconds. - /// - public double AverageResponseTimeMs { get; set; } - - /// - /// Gets or sets bandwidth by region. - /// - public Dictionary BandwidthByRegion { get; set; } = new(); - - /// - /// Gets or sets requests by content type. - /// - public Dictionary RequestsByContentType { get; set; } = new(); - - /// - /// Gets or sets top content by requests. - /// - public List TopContent { get; set; } = new(); - } - - /// - /// Top content information. - /// - public class TopContent - { - /// - /// Gets or sets the content key. - /// - public string ContentKey { get; set; } = string.Empty; - - /// - /// Gets or sets the number of requests. - /// - public long Requests { get; set; } - - /// - /// Gets or sets the bandwidth used. - /// - public long BandwidthBytes { get; set; } - } - - /// - /// CDN edge location configuration. - /// - public class CdnEdgeConfiguration - { - /// - /// Gets or sets priority regions for content distribution. - /// - public List PriorityRegions { get; set; } = new(); - - /// - /// Gets or sets whether to enable auto-scaling. - /// - public bool EnableAutoScaling { get; set; } - - /// - /// Gets or sets custom routing rules. - /// - public List RoutingRules { get; set; } = new(); - } - - /// - /// CDN routing rule. - /// - public class CdnRoutingRule - { - /// - /// Gets or sets the source region. - /// - public string SourceRegion { get; set; } = string.Empty; - - /// - /// Gets or sets the target edge location. - /// - public string TargetEdgeLocation { get; set; } = string.Empty; - - /// - /// Gets or sets the routing weight (0-100). - /// - public int Weight { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioConnectionPool.cs b/ConduitLLM.Core/Interfaces/IAudioConnectionPool.cs deleted file mode 100644 index f6c086697..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioConnectionPool.cs +++ /dev/null @@ -1,164 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for managing pooled connections to audio providers. - /// - public interface IAudioConnectionPool - { - /// - /// Gets or creates a pooled connection for a provider. - /// - /// The provider name. - /// Cancellation token. - /// A pooled connection. - Task GetConnectionAsync( - string provider, - CancellationToken cancellationToken = default); - - /// - /// Returns a connection to the pool. - /// - /// The connection to return. - Task ReturnConnectionAsync(IAudioProviderConnection connection); - - /// - /// Gets statistics about the connection pool. - /// - /// Optional provider to filter by. - /// Connection pool statistics. - Task GetStatisticsAsync(string? provider = null); - - /// - /// Clears idle connections from the pool. - /// - /// Maximum idle time before clearing. - /// Number of connections cleared. - Task ClearIdleConnectionsAsync(TimeSpan maxIdleTime); - - /// - /// Warms up the connection pool by pre-creating connections. - /// - /// The provider to warm up. - /// Number of connections to create. - /// Cancellation token. - Task WarmupAsync( - string provider, - int connectionCount, - CancellationToken cancellationToken = default); - } - - /// - /// Represents a pooled connection to an audio provider. - /// - public interface IAudioProviderConnection : IDisposable - { - /// - /// Gets the provider name. - /// - string Provider { get; } - - /// - /// Gets the connection ID. - /// - string ConnectionId { get; } - - /// - /// Gets whether the connection is healthy. - /// - bool IsHealthy { get; } - - /// - /// Gets when the connection was created. - /// - DateTime CreatedAt { get; } - - /// - /// Gets when the connection was last used. - /// - DateTime LastUsedAt { get; } - - /// - /// Gets the underlying HTTP client. - /// - HttpClient HttpClient { get; } - - /// - /// Validates the connection is still healthy. - /// - /// Cancellation token. - /// True if healthy, false otherwise. - Task ValidateAsync(CancellationToken cancellationToken = default); - } - - /// - /// Statistics about the connection pool. - /// - public class ConnectionPoolStatistics - { - /// - /// Gets or sets the total connections created. - /// - public int TotalCreated { get; set; } - - /// - /// Gets or sets the active connections. - /// - public int ActiveConnections { get; set; } - - /// - /// Gets or sets the idle connections. - /// - public int IdleConnections { get; set; } - - /// - /// Gets or sets the unhealthy connections. - /// - public int UnhealthyConnections { get; set; } - - /// - /// Gets or sets the total requests served. - /// - public long TotalRequests { get; set; } - - /// - /// Gets or sets the cache hit rate. - /// - public double HitRate { get; set; } - - /// - /// Gets or sets per-provider statistics. - /// - public Dictionary ProviderStats { get; set; } = new(); - } - - /// - /// Statistics for a specific provider's connection pool. - /// - public class ProviderPoolStatistics - { - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets the number of connections. - /// - public int ConnectionCount { get; set; } - - /// - /// Gets or sets the number of active connections. - /// - public int ActiveCount { get; set; } - - /// - /// Gets or sets the average connection age. - /// - public TimeSpan AverageAge { get; set; } - - /// - /// Gets or sets the requests per connection. - /// - public double RequestsPerConnection { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioContentFilter.cs b/ConduitLLM.Core/Interfaces/IAudioContentFilter.cs deleted file mode 100644 index 44353b353..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioContentFilter.cs +++ /dev/null @@ -1,48 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for filtering inappropriate content in audio operations. - /// - public interface IAudioContentFilter - { - /// - /// Filters transcribed text for inappropriate content. - /// - /// The transcribed text to filter. - /// The virtual key for tracking. - /// Cancellation token. - /// The filtered content result. - Task FilterTranscriptionAsync( - string text, - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Filters text before converting to speech. - /// - /// The text to filter before TTS. - /// The virtual key for tracking. - /// Cancellation token. - /// The filtered content result. - Task FilterTextToSpeechAsync( - string text, - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Validates audio content for inappropriate material. - /// - /// The audio data to validate. - /// The audio format. - /// The virtual key for tracking. - /// Cancellation token. - /// True if content is appropriate, false otherwise. - Task ValidateAudioContentAsync( - byte[] audioData, - AudioFormat format, - string virtualKey, - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioEncryptionService.cs b/ConduitLLM.Core/Interfaces/IAudioEncryptionService.cs deleted file mode 100644 index 99b5f836d..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioEncryptionService.cs +++ /dev/null @@ -1,115 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for audio encryption and decryption services. - /// - public interface IAudioEncryptionService - { - /// - /// Encrypts audio data. - /// - /// The audio data to encrypt. - /// Optional metadata to include. - /// Cancellation token. - /// Encrypted audio data. - Task EncryptAudioAsync( - byte[] audioData, - AudioEncryptionMetadata? metadata = null, - CancellationToken cancellationToken = default); - - /// - /// Decrypts audio data. - /// - /// The encrypted audio data. - /// Cancellation token. - /// Decrypted audio data. - Task DecryptAudioAsync( - EncryptedAudioData encryptedData, - CancellationToken cancellationToken = default); - - /// - /// Generates a new encryption key. - /// - /// A new encryption key. - Task GenerateKeyAsync(); - - /// - /// Validates encrypted audio data integrity. - /// - /// The encrypted data to validate. - /// True if data is valid and unmodified. - Task ValidateIntegrityAsync(EncryptedAudioData encryptedData); - } - - /// - /// Represents encrypted audio data. - /// - public class EncryptedAudioData - { - /// - /// Gets or sets the encrypted audio bytes. - /// - public byte[] EncryptedBytes { get; set; } = Array.Empty(); - - /// - /// Gets or sets the initialization vector. - /// - public byte[] IV { get; set; } = Array.Empty(); - - /// - /// Gets or sets the key identifier. - /// - public string KeyId { get; set; } = string.Empty; - - /// - /// Gets or sets the encryption algorithm used. - /// - public string Algorithm { get; set; } = "AES-256-GCM"; - - /// - /// Gets or sets the authentication tag. - /// - public byte[] AuthTag { get; set; } = Array.Empty(); - - /// - /// Gets or sets the encrypted metadata. - /// - public string? EncryptedMetadata { get; set; } - - /// - /// Gets or sets when the data was encrypted. - /// - public DateTime EncryptedAt { get; set; } = DateTime.UtcNow; - } - - /// - /// Metadata for audio encryption. - /// - public class AudioEncryptionMetadata - { - /// - /// Gets or sets the audio format. - /// - public string Format { get; set; } = string.Empty; - - /// - /// Gets or sets the original size. - /// - public long OriginalSize { get; set; } - - /// - /// Gets or sets the duration in seconds. - /// - public double? DurationSeconds { get; set; } - - /// - /// Gets or sets the virtual key. - /// - public string? VirtualKey { get; set; } - - /// - /// Gets or sets custom properties. - /// - public Dictionary CustomProperties { get; set; } = new(); - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioMetricsCollector.cs b/ConduitLLM.Core/Interfaces/IAudioMetricsCollector.cs deleted file mode 100644 index 57d30c9ce..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioMetricsCollector.cs +++ /dev/null @@ -1,575 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for collecting audio operation metrics. - /// - public interface IAudioMetricsCollector - { - /// - /// Records a transcription operation metric. - /// - /// The transcription metric. - Task RecordTranscriptionMetricAsync(TranscriptionMetric metric); - - /// - /// Records a text-to-speech operation metric. - /// - /// The TTS metric. - Task RecordTtsMetricAsync(TtsMetric metric); - - /// - /// Records a real-time session metric. - /// - /// The real-time metric. - Task RecordRealtimeMetricAsync(RealtimeMetric metric); - - /// - /// Records an audio routing decision. - /// - /// The routing metric. - Task RecordRoutingMetricAsync(RoutingMetric metric); - - /// - /// Records a provider health metric. - /// - /// The health metric. - Task RecordProviderHealthMetricAsync(ProviderHealthMetric metric); - - /// - /// Gets aggregated metrics for a time period. - /// - /// Start time for metrics. - /// End time for metrics. - /// Optional provider filter. - /// Aggregated audio metrics. - Task GetAggregatedMetricsAsync( - DateTime startTime, - DateTime endTime, - string? provider = null); - - /// - /// Gets real-time metrics snapshot. - /// - /// Current metrics snapshot. - Task GetCurrentSnapshotAsync(); - } - - /// - /// Base class for audio metrics. - /// - public abstract class AudioMetricBase - { - /// - /// Gets or sets the timestamp. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets the virtual key. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Gets or sets whether the operation was successful. - /// - public bool Success { get; set; } - - /// - /// Gets or sets the error code if failed. - /// - public string? ErrorCode { get; set; } - - /// - /// Gets or sets the duration in milliseconds. - /// - public double DurationMs { get; set; } - - /// - /// Gets or sets custom tags. - /// - public Dictionary Tags { get; set; } = new(); - } - - /// - /// Transcription operation metric. - /// - public class TranscriptionMetric : AudioMetricBase - { - /// - /// Gets or sets the audio format. - /// - public string AudioFormat { get; set; } = string.Empty; - - /// - /// Gets or sets the audio duration in seconds. - /// - public double AudioDurationSeconds { get; set; } - - /// - /// Gets or sets the file size in bytes. - /// - public long FileSizeBytes { get; set; } - - /// - /// Gets or sets the detected language. - /// - public string? DetectedLanguage { get; set; } - - /// - /// Gets or sets the confidence score. - /// - public double? Confidence { get; set; } - - /// - /// Gets or sets the word count. - /// - public int WordCount { get; set; } - - /// - /// Gets or sets whether it was served from cache. - /// - public bool ServedFromCache { get; set; } - } - - /// - /// Text-to-speech operation metric. - /// - public class TtsMetric : AudioMetricBase - { - /// - /// Gets or sets the voice used. - /// - public string Voice { get; set; } = string.Empty; - - /// - /// Gets or sets the character count. - /// - public int CharacterCount { get; set; } - - /// - /// Gets or sets the output format. - /// - public string OutputFormat { get; set; } = string.Empty; - - /// - /// Gets or sets the generated audio duration. - /// - public double GeneratedDurationSeconds { get; set; } - - /// - /// Gets or sets the output size in bytes. - /// - public long OutputSizeBytes { get; set; } - - /// - /// Gets or sets whether it was served from cache. - /// - public bool ServedFromCache { get; set; } - - /// - /// Gets or sets whether it was uploaded to CDN. - /// - public bool UploadedToCdn { get; set; } - } - - /// - /// Real-time session metric. - /// - public class RealtimeMetric : AudioMetricBase - { - /// - /// Gets or sets the session ID. - /// - public string SessionId { get; set; } = string.Empty; - - /// - /// Gets or sets the session duration. - /// - public double SessionDurationSeconds { get; set; } - - /// - /// Gets or sets the number of turns. - /// - public int TurnCount { get; set; } - - /// - /// Gets or sets the total audio sent. - /// - public double TotalAudioSentSeconds { get; set; } - - /// - /// Gets or sets the total audio received. - /// - public double TotalAudioReceivedSeconds { get; set; } - - /// - /// Gets or sets the average latency. - /// - public double AverageLatencyMs { get; set; } - - /// - /// Gets or sets the disconnect reason. - /// - public string? DisconnectReason { get; set; } - } - - /// - /// Routing decision metric. - /// - public class RoutingMetric : AudioMetricBase - { - /// - /// Gets or sets the operation type. - /// - public AudioOperation Operation { get; set; } - - /// - /// Gets or sets the routing strategy used. - /// - public string RoutingStrategy { get; set; } = string.Empty; - - /// - /// Gets or sets the selected provider. - /// - public string SelectedProvider { get; set; } = string.Empty; - - /// - /// Gets or sets the candidate providers considered. - /// - public List CandidateProviders { get; set; } = new(); - - /// - /// Gets or sets the routing decision time. - /// - public double DecisionTimeMs { get; set; } - - /// - /// Gets or sets the routing reason. - /// - public string? RoutingReason { get; set; } - } - - /// - /// Provider health metric. - /// - public class ProviderHealthMetric - { - /// - /// Gets or sets the timestamp. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets whether the provider is healthy. - /// - public bool IsHealthy { get; set; } - - /// - /// Gets or sets the response time. - /// - public double ResponseTimeMs { get; set; } - - /// - /// Gets or sets the error rate. - /// - public double ErrorRate { get; set; } - - /// - /// Gets or sets the success rate. - /// - public double SuccessRate { get; set; } - - /// - /// Gets or sets the active connections. - /// - public int ActiveConnections { get; set; } - - /// - /// Gets or sets health check details. - /// - public Dictionary HealthDetails { get; set; } = new(); - } - - /// - /// Aggregated audio metrics. - /// - public class AggregatedAudioMetrics - { - /// - /// Gets or sets the time period. - /// - public DateTimeRange Period { get; set; } = new(); - - /// - /// Gets or sets transcription statistics. - /// - public OperationStatistics Transcription { get; set; } = new(); - - /// - /// Gets or sets TTS statistics. - /// - public OperationStatistics TextToSpeech { get; set; } = new(); - - /// - /// Gets or sets real-time statistics. - /// - public RealtimeStatistics Realtime { get; set; } = new(); - - /// - /// Gets or sets provider statistics. - /// - public Dictionary ProviderStats { get; set; } = new(); - - /// - /// Gets or sets cost analysis. - /// - public CostAnalysis Costs { get; set; } = new(); - } - - /// - /// Operation statistics. - /// - public class OperationStatistics - { - /// - /// Gets or sets total requests. - /// - public long TotalRequests { get; set; } - - /// - /// Gets or sets successful requests. - /// - public long SuccessfulRequests { get; set; } - - /// - /// Gets or sets failed requests. - /// - public long FailedRequests { get; set; } - - /// - /// Gets or sets average duration. - /// - public double AverageDurationMs { get; set; } - - /// - /// Gets or sets P95 duration. - /// - public double P95DurationMs { get; set; } - - /// - /// Gets or sets P99 duration. - /// - public double P99DurationMs { get; set; } - - /// - /// Gets or sets cache hit rate. - /// - public double CacheHitRate { get; set; } - - /// - /// Gets or sets total data processed. - /// - public long TotalDataBytes { get; set; } - } - - /// - /// Real-time statistics. - /// - public class RealtimeStatistics - { - /// - /// Gets or sets total sessions. - /// - public long TotalSessions { get; set; } - - /// - /// Gets or sets average session duration. - /// - public double AverageSessionDurationSeconds { get; set; } - - /// - /// Gets or sets total audio minutes. - /// - public double TotalAudioMinutes { get; set; } - - /// - /// Gets or sets average latency. - /// - public double AverageLatencyMs { get; set; } - - /// - /// Gets or sets disconnect reasons. - /// - public Dictionary DisconnectReasons { get; set; } = new(); - } - - /// - /// Provider statistics. - /// - public class ProviderStatistics - { - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets request count. - /// - public long RequestCount { get; set; } - - /// - /// Gets or sets success rate. - /// - public double SuccessRate { get; set; } - - /// - /// Gets or sets average latency. - /// - public double AverageLatencyMs { get; set; } - - /// - /// Gets or sets uptime percentage. - /// - public double UptimePercentage { get; set; } - - /// - /// Gets or sets error breakdown. - /// - public Dictionary ErrorBreakdown { get; set; } = new(); - } - - /// - /// Cost analysis. - /// - public class CostAnalysis - { - /// - /// Gets or sets total cost. - /// - public decimal TotalCost { get; set; } - - /// - /// Gets or sets transcription cost. - /// - public decimal TranscriptionCost { get; set; } - - /// - /// Gets or sets TTS cost. - /// - public decimal TextToSpeechCost { get; set; } - - /// - /// Gets or sets real-time cost. - /// - public decimal RealtimeCost { get; set; } - - /// - /// Gets or sets cost by provider. - /// - public Dictionary CostByProvider { get; set; } = new(); - - /// - /// Gets or sets cost savings from caching. - /// - public decimal CachingSavings { get; set; } - } - - /// - /// Current metrics snapshot. - /// - public class AudioMetricsSnapshot - { - /// - /// Gets or sets the snapshot time. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets active transcriptions. - /// - public int ActiveTranscriptions { get; set; } - - /// - /// Gets or sets active TTS operations. - /// - public int ActiveTtsOperations { get; set; } - - /// - /// Gets or sets active real-time sessions. - /// - public int ActiveRealtimeSessions { get; set; } - - /// - /// Gets or sets requests per second. - /// - public double RequestsPerSecond { get; set; } - - /// - /// Gets or sets current error rate. - /// - public double CurrentErrorRate { get; set; } - - /// - /// Gets or sets provider health status. - /// - public Dictionary ProviderHealth { get; set; } = new(); - - /// - /// Gets or sets system resources. - /// - public SystemResources Resources { get; set; } = new(); - } - - /// - /// System resources. - /// - public class SystemResources - { - /// - /// Gets or sets CPU usage percentage. - /// - public double CpuUsagePercent { get; set; } - - /// - /// Gets or sets memory usage in MB. - /// - public double MemoryUsageMb { get; set; } - - /// - /// Gets or sets active connections. - /// - public int ActiveConnections { get; set; } - - /// - /// Gets or sets cache size in MB. - /// - public double CacheSizeMb { get; set; } - } - - /// - /// Date time range. - /// - public class DateTimeRange - { - /// - /// Gets or sets the start time. - /// - public DateTime Start { get; set; } - - /// - /// Gets or sets the end time. - /// - public DateTime End { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioPiiDetector.cs b/ConduitLLM.Core/Interfaces/IAudioPiiDetector.cs deleted file mode 100644 index 744b9cf1b..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioPiiDetector.cs +++ /dev/null @@ -1,200 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for detecting and redacting PII in audio content. - /// - public interface IAudioPiiDetector - { - /// - /// Detects PII in transcribed text. - /// - /// The text to scan for PII. - /// Cancellation token. - /// PII detection result. - Task DetectPiiAsync( - string text, - CancellationToken cancellationToken = default); - - /// - /// Redacts PII from text. - /// - /// The text containing PII. - /// The PII detection result. - /// Options for redaction. - /// Text with PII redacted. - Task RedactPiiAsync( - string text, - PiiDetectionResult detectionResult, - PiiRedactionOptions? redactionOptions = null); - } - - /// - /// Result of PII detection. - /// - public class PiiDetectionResult - { - /// - /// Gets or sets whether PII was detected. - /// - public bool ContainsPii { get; set; } - - /// - /// Gets or sets the detected PII entities. - /// - public List Entities { get; set; } = new(); - - /// - /// Gets or sets the overall risk score. - /// - public double RiskScore { get; set; } - } - - /// - /// Represents a detected PII entity. - /// - public class PiiEntity - { - /// - /// Gets or sets the type of PII. - /// - public PiiType Type { get; set; } - - /// - /// Gets or sets the detected text. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Gets or sets the start position. - /// - public int StartIndex { get; set; } - - /// - /// Gets or sets the end position. - /// - public int EndIndex { get; set; } - - /// - /// Gets or sets the confidence score. - /// - public double Confidence { get; set; } - } - - /// - /// Types of PII that can be detected. - /// - public enum PiiType - { - /// - /// Social Security Number. - /// - SSN, - - /// - /// Credit card number. - /// - CreditCard, - - /// - /// Email address. - /// - Email, - - /// - /// Phone number. - /// - Phone, - - /// - /// Physical address. - /// - Address, - - /// - /// Person name. - /// - Name, - - /// - /// Date of birth. - /// - DateOfBirth, - - /// - /// Medical record number. - /// - MedicalRecord, - - /// - /// Bank account number. - /// - BankAccount, - - /// - /// Driver's license number. - /// - DriversLicense, - - /// - /// Passport number. - /// - Passport, - - /// - /// Other sensitive information. - /// - Other - } - - /// - /// Options for PII redaction. - /// - public class PiiRedactionOptions - { - /// - /// Gets or sets the redaction method. - /// - public RedactionMethod Method { get; set; } = RedactionMethod.Mask; - - /// - /// Gets or sets the mask character. - /// - public char MaskCharacter { get; set; } = '*'; - - /// - /// Gets or sets whether to preserve length. - /// - public bool PreserveLength { get; set; } = true; - - /// - /// Gets or sets custom replacement patterns. - /// - public Dictionary CustomReplacements { get; set; } = new(); - } - - /// - /// Methods for redacting PII. - /// - public enum RedactionMethod - { - /// - /// Replace with mask characters. - /// - Mask, - - /// - /// Replace with type placeholder. - /// - Placeholder, - - /// - /// Remove entirely. - /// - Remove, - - /// - /// Use custom replacement. - /// - Custom - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioProcessingService.cs b/ConduitLLM.Core/Interfaces/IAudioProcessingService.cs deleted file mode 100644 index 03b24d489..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioProcessingService.cs +++ /dev/null @@ -1,373 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Provides audio processing capabilities including format conversion, compression, and enhancement. - /// - /// - /// - /// The IAudioProcessingService interface defines operations for manipulating audio data, - /// including format conversion, compression, noise reduction, and caching. This service - /// enables the system to handle various audio formats and optimize audio quality and size. - /// - /// - /// Implementations should be efficient and support common audio processing scenarios - /// required by speech-to-text and text-to-speech operations. - /// - /// - public interface IAudioProcessingService - { - /// - /// Converts audio from one format to another. - /// - /// The input audio data. - /// The source audio format (e.g., "mp3", "wav"). - /// The target audio format. - /// A token to cancel the operation. - /// The converted audio data. - /// Thrown when the conversion is not supported. - /// Thrown when the audio data or formats are invalid. - /// - /// - /// This method handles conversion between common audio formats used in speech processing: - /// - /// - /// MP3 - Compressed format, widely supported - /// WAV - Uncompressed format, high quality - /// FLAC - Lossless compression - /// WebM - Web-optimized format - /// OGG - Open-source compressed format - /// - /// - Task ConvertFormatAsync( - byte[] audioData, - string sourceFormat, - string targetFormat, - CancellationToken cancellationToken = default); - - /// - /// Compresses audio data to reduce file size. - /// - /// The input audio data. - /// The audio format. - /// Compression quality (0.0 = lowest, 1.0 = highest). - /// A token to cancel the operation. - /// The compressed audio data. - /// - /// - /// Applies intelligent compression based on the audio content and target use case. - /// Higher quality values preserve more detail but result in larger files. - /// - /// - /// The compression algorithm adapts based on: - /// - /// - /// Speech vs. music content - /// Target bitrate requirements - /// Perceptual quality metrics - /// - /// - Task CompressAudioAsync( - byte[] audioData, - string format, - double quality = 0.8, - CancellationToken cancellationToken = default); - - /// - /// Applies noise reduction to improve audio quality. - /// - /// The input audio data. - /// The audio format. - /// Noise reduction level (0.0 = minimal, 1.0 = maximum). - /// A token to cancel the operation. - /// The processed audio data with reduced noise. - /// - /// - /// Removes background noise while preserving speech clarity. This is particularly - /// useful for improving STT accuracy in noisy environments. - /// - /// - /// The noise reduction algorithm: - /// - /// - /// Identifies and suppresses constant background noise - /// Preserves speech frequencies - /// Adapts to changing noise conditions - /// - /// - Task ReduceNoiseAsync( - byte[] audioData, - string format, - double aggressiveness = 0.5, - CancellationToken cancellationToken = default); - - /// - /// Normalizes audio volume levels. - /// - /// The input audio data. - /// The audio format. - /// Target normalization level in dB (default: -3dB). - /// A token to cancel the operation. - /// The normalized audio data. - /// - /// - /// Adjusts audio levels to ensure consistent volume across different recordings. - /// This improves both STT accuracy and TTS output quality. - /// - /// - Task NormalizeAudioAsync( - byte[] audioData, - string format, - double targetLevel = -3.0, - CancellationToken cancellationToken = default); - - /// - /// Caches processed audio for faster retrieval. - /// - /// The cache key for the audio. - /// The audio data to cache. - /// Optional metadata about the audio. - /// Cache expiration time in seconds (default: 3600). - /// A token to cancel the operation. - /// A task representing the caching operation. - /// - /// - /// Stores processed audio in a distributed cache to avoid redundant processing. - /// The cache key should be deterministic based on the audio content and processing parameters. - /// - /// - Task CacheAudioAsync( - string key, - byte[] audioData, - Dictionary? metadata = null, - int expiration = 3600, - CancellationToken cancellationToken = default); - - /// - /// Retrieves cached audio if available. - /// - /// The cache key for the audio. - /// A token to cancel the operation. - /// The cached audio data and metadata, or null if not found. - /// - /// Returns null if the audio is not in cache or has expired. - /// - Task GetCachedAudioAsync( - string key, - CancellationToken cancellationToken = default); - - /// - /// Extracts audio metadata and characteristics. - /// - /// The audio data to analyze. - /// The audio format. - /// A token to cancel the operation. - /// Metadata about the audio file. - /// - /// - /// Analyzes audio to extract useful information for processing decisions: - /// - /// - /// Duration and bitrate - /// Sample rate and channels - /// Average volume and peak levels - /// Detected language hints - /// Speech vs. music classification - /// - /// - Task GetAudioMetadataAsync( - byte[] audioData, - string format, - CancellationToken cancellationToken = default); - - /// - /// Splits audio into smaller segments for processing. - /// - /// The audio data to split. - /// The audio format. - /// Target segment duration in seconds. - /// Overlap between segments in seconds (for context). - /// A token to cancel the operation. - /// A list of audio segments. - /// - /// - /// Useful for processing long audio files that exceed provider limits or for - /// parallel processing of audio chunks. - /// - /// - Task> SplitAudioAsync( - byte[] audioData, - string format, - double segmentDuration = 30.0, - double overlap = 0.5, - CancellationToken cancellationToken = default); - - /// - /// Merges multiple audio segments into a single file. - /// - /// The audio segments to merge. - /// The output audio format. - /// A token to cancel the operation. - /// The merged audio data. - /// - /// Combines audio segments with smooth transitions, useful for reassembling - /// processed audio chunks. - /// - Task MergeAudioAsync( - List segments, - string format, - CancellationToken cancellationToken = default); - - /// - /// Checks if a format conversion is supported. - /// - /// The source format. - /// The target format. - /// True if the conversion is supported. - bool IsConversionSupported(string sourceFormat, string targetFormat); - - /// - /// Gets the list of supported audio formats. - /// - /// A list of supported format identifiers. - List GetSupportedFormats(); - - /// - /// Estimates the processing time for an audio operation. - /// - /// The size of the audio in bytes. - /// The type of operation (e.g., "convert", "compress", "noise-reduce"). - /// Estimated processing time in milliseconds. - /// - /// Helps with capacity planning and user experience by providing processing time estimates. - /// - double EstimateProcessingTime(long audioSizeBytes, string operation); - } - - /// - /// Represents cached audio data with metadata. - /// - public class CachedAudio - { - /// - /// Gets or sets the audio data. - /// - public byte[] Data { get; set; } = System.Array.Empty(); - - /// - /// Gets or sets the audio format. - /// - public string Format { get; set; } = string.Empty; - - /// - /// Gets or sets the metadata. - /// - public Dictionary Metadata { get; set; } = new(); - - /// - /// Gets or sets when the audio was cached. - /// - public System.DateTime CachedAt { get; set; } - - /// - /// Gets or sets when the cache expires. - /// - public System.DateTime ExpiresAt { get; set; } - } - - /// - /// Metadata about an audio file. - /// - public class AudioMetadata - { - /// - /// Gets or sets the duration in seconds. - /// - public double DurationSeconds { get; set; } - - /// - /// Gets or sets the bitrate in bits per second. - /// - public int Bitrate { get; set; } - - /// - /// Gets or sets the sample rate in Hz. - /// - public int SampleRate { get; set; } - - /// - /// Gets or sets the number of channels. - /// - public int Channels { get; set; } - - /// - /// Gets or sets the average volume level in dB. - /// - public double AverageVolume { get; set; } - - /// - /// Gets or sets the peak volume level in dB. - /// - public double PeakVolume { get; set; } - - /// - /// Gets or sets whether the audio contains speech. - /// - public bool ContainsSpeech { get; set; } - - /// - /// Gets or sets whether the audio contains music. - /// - public bool ContainsMusic { get; set; } - - /// - /// Gets or sets the estimated noise level. - /// - public double NoiseLevel { get; set; } - - /// - /// Gets or sets detected language hints. - /// - public List LanguageHints { get; set; } = new(); - - /// - /// Gets or sets the file size in bytes. - /// - public long FileSizeBytes { get; set; } - } - - /// - /// Represents a segment of audio data. - /// - public class AudioSegment - { - /// - /// Gets or sets the segment index. - /// - public int Index { get; set; } - - /// - /// Gets or sets the audio data. - /// - public byte[] Data { get; set; } = System.Array.Empty(); - - /// - /// Gets or sets the start time in seconds. - /// - public double StartTime { get; set; } - - /// - /// Gets or sets the end time in seconds. - /// - public double EndTime { get; set; } - - /// - /// Gets or sets the duration in seconds. - /// - public double Duration => EndTime - StartTime; - - /// - /// Gets or sets whether this segment overlaps with the next. - /// - public bool HasOverlap { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioQualityTracker.cs b/ConduitLLM.Core/Interfaces/IAudioQualityTracker.cs deleted file mode 100644 index a31ba0fbc..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioQualityTracker.cs +++ /dev/null @@ -1,403 +0,0 @@ -using ConduitLLM.Core.Models; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for tracking and analyzing audio quality metrics. - /// - public interface IAudioQualityTracker - { - /// - /// Tracks transcription quality metrics. - /// - /// The quality metric to track. - Task TrackTranscriptionQualityAsync(AudioQualityMetric metric); - - /// - /// Gets a quality report for a time period. - /// - /// Start time for the report. - /// End time for the report. - /// Optional provider filter. - /// Audio quality report. - Task GetQualityReportAsync( - DateTime startTime, - DateTime endTime, - string? provider = null); - - /// - /// Gets quality thresholds for a provider. - /// - /// The provider name. - /// Quality thresholds. - Task GetQualityThresholdsAsync(string provider); - - /// - /// Checks if quality metrics are acceptable. - /// - /// The provider name. - /// The confidence score. - /// Optional word error rate. - /// True if quality is acceptable. - Task IsQualityAcceptableAsync( - string provider, - double confidence, - double? wordErrorRate = null); - } - - /// - /// Audio quality metric. - /// - public class AudioQualityMetric - { - /// - /// Gets or sets the timestamp. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets the model used. - /// - public string? Model { get; set; } - - /// - /// Gets or sets the virtual key. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// Gets or sets the confidence score (0-1). - /// - public double? Confidence { get; set; } - - /// - /// Gets or sets the word error rate (0-1). - /// - public double? WordErrorRate { get; set; } - - /// - /// Gets or sets the accuracy score (0-1). - /// - public double? AccuracyScore { get; set; } - - /// - /// Gets or sets the language. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the audio duration in seconds. - /// - public double AudioDurationSeconds { get; set; } - - /// - /// Gets or sets the processing duration in milliseconds. - /// - public double ProcessingDurationMs { get; set; } - - /// - /// Gets or sets quality metadata. - /// - public Dictionary Metadata { get; set; } = new(); - } - - /// - /// Audio quality report. - /// - public class AudioQualityReport - { - /// - /// Gets or sets the report period. - /// - public DateTimeRange Period { get; set; } = new(); - - /// - /// Gets or sets provider quality statistics. - /// - public Dictionary ProviderQuality { get; set; } = new(); - - /// - /// Gets or sets model quality statistics. - /// - public Dictionary ModelQuality { get; set; } = new(); - - /// - /// Gets or sets language quality statistics. - /// - public Dictionary LanguageQuality { get; set; } = new(); - - /// - /// Gets or sets quality trends. - /// - public List QualityTrends { get; set; } = new(); - - /// - /// Gets or sets recommendations. - /// - public List Recommendations { get; set; } = new(); - } - - /// - /// Provider quality statistics. - /// - public class ProviderQualityStats - { - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets average confidence. - /// - public double AverageConfidence { get; set; } - - /// - /// Gets or sets minimum confidence. - /// - public double MinimumConfidence { get; set; } - - /// - /// Gets or sets maximum confidence. - /// - public double MaximumConfidence { get; set; } - - /// - /// Gets or sets confidence standard deviation. - /// - public double ConfidenceStdDev { get; set; } - - /// - /// Gets or sets average accuracy. - /// - public double AverageAccuracy { get; set; } - - /// - /// Gets or sets sample count. - /// - public long SampleCount { get; set; } - - /// - /// Gets or sets low confidence rate. - /// - public double LowConfidenceRate { get; set; } - - /// - /// Gets or sets high confidence rate. - /// - public double HighConfidenceRate { get; set; } - } - - /// - /// Model quality statistics. - /// - public class ModelQualityStats - { - /// - /// Gets or sets the model name. - /// - public string Model { get; set; } = string.Empty; - - /// - /// Gets or sets average confidence. - /// - public double AverageConfidence { get; set; } - - /// - /// Gets or sets average accuracy. - /// - public double AverageAccuracy { get; set; } - - /// - /// Gets or sets sample count. - /// - public long SampleCount { get; set; } - - /// - /// Gets or sets performance rating (0-1). - /// - public double PerformanceRating { get; set; } - } - - /// - /// Language quality statistics. - /// - public class LanguageQualityStats - { - /// - /// Gets or sets the language code. - /// - public string Language { get; set; } = string.Empty; - - /// - /// Gets or sets average confidence. - /// - public double AverageConfidence { get; set; } - - /// - /// Gets or sets average word error rate. - /// - public double AverageWordErrorRate { get; set; } - - /// - /// Gets or sets sample count. - /// - public long SampleCount { get; set; } - - /// - /// Gets or sets quality score (0-1). - /// - public double QualityScore { get; set; } - } - - /// - /// Quality trend information. - /// - public class QualityTrend - { - /// - /// Gets or sets the provider. - /// - public string? Provider { get; set; } - - /// - /// Gets or sets the metric name. - /// - public string Metric { get; set; } = string.Empty; - - /// - /// Gets or sets the trend direction. - /// - public AudioQualityTrendDirection Direction { get; set; } - - /// - /// Gets or sets the change percentage. - /// - public double ChangePercent { get; set; } - } - - - /// - /// Quality recommendation. - /// - public class QualityRecommendation - { - /// - /// Gets or sets the recommendation type. - /// - public RecommendationType Type { get; set; } - - /// - /// Gets or sets the severity. - /// - public RecommendationSeverity Severity { get; set; } - - /// - /// Gets or sets the affected provider. - /// - public string? Provider { get; set; } - - /// - /// Gets or sets the affected language. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the recommendation message. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Gets or sets the expected impact. - /// - public string? Impact { get; set; } - } - - /// - /// Recommendation type. - /// - public enum RecommendationType - { - /// - /// Switch to a different provider. - /// - ProviderSwitch, - - /// - /// Upgrade to a better model. - /// - ModelUpgrade, - - /// - /// Adjust configuration settings. - /// - ConfigurationChange, - - /// - /// Improve audio preprocessing. - /// - PreprocessingImprovement - } - - /// - /// Recommendation severity. - /// - public enum RecommendationSeverity - { - /// - /// Low severity. - /// - Low, - - /// - /// Medium severity. - /// - Medium, - - /// - /// High severity. - /// - High - } - - /// - /// Quality thresholds for acceptable performance. - /// - public class QualityThresholds - { - /// - /// Gets or sets minimum acceptable confidence. - /// - public double MinimumConfidence { get; set; } - - /// - /// Gets or sets maximum acceptable word error rate. - /// - public double MaximumWordErrorRate { get; set; } - - /// - /// Gets or sets minimum acceptable accuracy. - /// - public double MinimumAccuracy { get; set; } - - /// - /// Gets or sets optimal confidence level. - /// - public double OptimalConfidence { get; set; } - - /// - /// Gets or sets optimal word error rate. - /// - public double OptimalWordErrorRate { get; set; } - - /// - /// Gets or sets optimal accuracy level. - /// - public double OptimalAccuracy { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Interfaces/IAudioRouter.cs b/ConduitLLM.Core/Interfaces/IAudioRouter.cs deleted file mode 100644 index 12b936570..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioRouter.cs +++ /dev/null @@ -1,246 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for routing audio requests to appropriate providers based on capabilities and availability. - /// - /// - /// - /// The IAudioRouter provides intelligent routing for audio operations across multiple providers. - /// It considers factors such as: - /// - /// - /// Provider capabilities (which operations they support) - /// Model availability (specific models for transcription or TTS) - /// Voice availability (for TTS operations) - /// Language support - /// Cost optimization - /// Provider health and availability - /// - /// - public interface IAudioRouter - { - /// - /// Gets an audio transcription client based on the request requirements. - /// - /// The transcription request with requirements. - /// The virtual key for authentication and routing rules. - /// Cancellation token for the operation. - /// An appropriate transcription client, or null if none available. - /// - /// - /// The router will consider: - /// - /// - /// Requested model (e.g., "whisper-1") - /// Language requirements - /// Audio format support - /// Provider availability - /// - /// - Task GetTranscriptionClientAsync( - AudioTranscriptionRequest request, - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Gets a text-to-speech client based on the request requirements. - /// - /// The TTS request with requirements. - /// The virtual key for authentication and routing rules. - /// Cancellation token for the operation. - /// An appropriate TTS client, or null if none available. - /// - /// - /// The router will consider: - /// - /// - /// Requested voice availability - /// Model preferences (e.g., "tts-1" vs "tts-1-hd") - /// Language support - /// Audio format requirements - /// Streaming capability needs - /// - /// - Task GetTextToSpeechClientAsync( - TextToSpeechRequest request, - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Gets a real-time audio client based on the session configuration. - /// - /// The real-time session configuration. - /// The virtual key for authentication and routing rules. - /// Cancellation token for the operation. - /// An appropriate real-time client, or null if none available. - /// - /// - /// Real-time routing is more complex as it considers: - /// - /// - /// Provider support for real-time conversations - /// Model availability (e.g., "gpt-4o-realtime") - /// Voice preferences - /// Function calling requirements - /// Latency requirements - /// - /// - Task GetRealtimeClientAsync( - RealtimeSessionConfig config, - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Gets all available transcription providers for a virtual key. - /// - /// The virtual key to check access for. - /// Cancellation token for the operation. - /// List of provider names that support transcription. - Task> GetAvailableTranscriptionProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Gets all available TTS providers for a virtual key. - /// - /// The virtual key to check access for. - /// Cancellation token for the operation. - /// List of provider names that support TTS. - Task> GetAvailableTextToSpeechProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Gets all available real-time providers for a virtual key. - /// - /// The virtual key to check access for. - /// Cancellation token for the operation. - /// List of provider names that support real-time audio. - Task> GetAvailableRealtimeProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Validates that a specific audio operation can be performed. - /// - /// The type of audio operation. - /// The provider to validate. - /// The request to validate. - /// Error message if validation fails. - /// True if the operation can be performed, false otherwise. - bool ValidateAudioOperation( - AudioOperation operation, - string provider, - AudioRequestBase request, - out string errorMessage); - - /// - /// Gets routing statistics for audio operations. - /// - /// The virtual key to get statistics for. - /// Cancellation token for the operation. - /// Statistics about audio routing decisions. - Task GetRoutingStatisticsAsync( - string virtualKey, - CancellationToken cancellationToken = default); - } - - /// - /// Statistics about audio routing decisions. - /// - public class AudioRoutingStatistics - { - /// - /// Total number of transcription requests routed. - /// - public long TranscriptionRequests { get; set; } - - /// - /// Total number of TTS requests routed. - /// - public long TextToSpeechRequests { get; set; } - - /// - /// Total number of real-time sessions routed. - /// - public long RealtimeSessions { get; set; } - - /// - /// Provider usage breakdown. - /// - public Dictionary ProviderStats { get; set; } = new(); - - /// - /// Failed routing attempts. - /// - public long FailedRoutingAttempts { get; set; } - - /// - /// Average routing decision time in milliseconds. - /// - public double AverageRoutingTimeMs { get; set; } - - /// - /// Provider name (for single-provider statistics). - /// - public string? Provider { get; set; } - - /// - /// Total number of requests processed. - /// - public int TotalRequests { get; set; } - - /// - /// Overall success rate (0-1). - /// - public double SuccessRate { get; set; } - - /// - /// Average latency across all operations. - /// - public double AverageLatencyMs { get; set; } - - /// - /// When the statistics were last updated. - /// - public DateTime LastUpdated { get; set; } - } - - /// - /// Audio statistics for a specific provider. - /// - public class ProviderAudioStats - { - /// - /// Number of transcription requests handled. - /// - public long TranscriptionCount { get; set; } - - /// - /// Number of TTS requests handled. - /// - public long TextToSpeechCount { get; set; } - - /// - /// Number of real-time sessions handled. - /// - public long RealtimeCount { get; set; } - - /// - /// Total audio minutes processed. - /// - public double TotalAudioMinutes { get; set; } - - /// - /// Total characters synthesized. - /// - public long TotalCharactersSynthesized { get; set; } - - /// - /// Success rate percentage. - /// - public double SuccessRate { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioRoutingStrategy.cs b/ConduitLLM.Core/Interfaces/IAudioRoutingStrategy.cs deleted file mode 100644 index f34f2e298..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioRoutingStrategy.cs +++ /dev/null @@ -1,256 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines strategies for routing audio requests to providers. - /// - public interface IAudioRoutingStrategy - { - /// - /// Gets the name of the routing strategy. - /// - string Name { get; } - - /// - /// Selects the best provider for a transcription request. - /// - /// The transcription request. - /// List of available providers with their capabilities. - /// Cancellation token. - /// The selected provider name, or null if no suitable provider found. - Task SelectTranscriptionProviderAsync( - AudioTranscriptionRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default); - - /// - /// Selects the best provider for a text-to-speech request. - /// - /// The TTS request. - /// List of available providers with their capabilities. - /// Cancellation token. - /// The selected provider name, or null if no suitable provider found. - Task SelectTextToSpeechProviderAsync( - TextToSpeechRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default); - - /// - /// Updates routing metrics after a request completes. - /// - /// The provider that handled the request. - /// The performance metrics from the request. - /// Cancellation token. - Task UpdateMetricsAsync( - string provider, - AudioRequestMetrics metrics, - CancellationToken cancellationToken = default); - } - - /// - /// Information about an audio provider's capabilities and current status. - /// - public class AudioProviderInfo - { - /// - /// Gets or sets the provider name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets whether the provider is currently available. - /// - public bool IsAvailable { get; set; } - - /// - /// Gets or sets the provider's capabilities. - /// - public AudioProviderRoutingCapabilities Capabilities { get; set; } = new(); - - /// - /// Gets or sets the current performance metrics. - /// - public AudioProviderMetrics Metrics { get; set; } = new(); - - /// - /// Gets or sets the provider's geographic region. - /// - public string? Region { get; set; } - - /// - /// Gets or sets the provider's cost per unit. - /// - public AudioProviderCosts Costs { get; set; } = new(); - } - - /// - /// Capabilities of an audio provider for routing decisions. - /// - public class AudioProviderRoutingCapabilities - { - /// - /// Gets or sets whether streaming is supported. - /// - public bool SupportsStreaming { get; set; } - - /// - /// Gets or sets the supported languages (ISO 639-1 codes). - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Gets or sets the supported audio formats. - /// - public List SupportedFormats { get; set; } = new(); - - /// - /// Gets or sets the maximum audio duration in seconds. - /// - public int MaxAudioDurationSeconds { get; set; } - - /// - /// Gets or sets whether real-time processing is supported. - /// - public bool SupportsRealtime { get; set; } - - /// - /// Gets or sets the supported voice IDs for TTS. - /// - public List SupportedVoices { get; set; } = new(); - - /// - /// Gets or sets whether custom vocabulary is supported. - /// - public bool SupportsCustomVocabulary { get; set; } - - /// - /// Gets or sets the quality score (0-100). - /// - public double QualityScore { get; set; } - } - - /// - /// Current performance metrics for an audio provider. - /// - public class AudioProviderMetrics - { - /// - /// Gets or sets the average latency in milliseconds. - /// - public double AverageLatencyMs { get; set; } - - /// - /// Gets or sets the 95th percentile latency. - /// - public double P95LatencyMs { get; set; } - - /// - /// Gets or sets the success rate (0-1). - /// - public double SuccessRate { get; set; } - - /// - /// Gets or sets the current load (0-1). - /// - public double CurrentLoad { get; set; } - - /// - /// Gets or sets when metrics were last updated. - /// - public DateTime LastUpdated { get; set; } - - /// - /// Gets or sets the number of requests in the sample. - /// - public int SampleSize { get; set; } - } - - /// - /// Cost information for an audio provider. - /// - public class AudioProviderCosts - { - /// - /// Gets or sets the cost per minute for STT. - /// - public decimal TranscriptionPerMinute { get; set; } - - /// - /// Gets or sets the cost per 1000 characters for TTS. - /// - public decimal TextToSpeechPer1kChars { get; set; } - - /// - /// Gets or sets the cost per minute for real-time audio. - /// - public decimal RealtimePerMinute { get; set; } - } - - /// - /// Metrics from an audio request. - /// - public class AudioRequestMetrics - { - /// - /// Gets or sets the request type. - /// - public AudioRequestType RequestType { get; set; } - - /// - /// Gets or sets the total latency in milliseconds. - /// - public double LatencyMs { get; set; } - - /// - /// Gets or sets whether the request succeeded. - /// - public bool Success { get; set; } - - /// - /// Gets or sets the error code if failed. - /// - public string? ErrorCode { get; set; } - - /// - /// Gets or sets the audio duration in seconds. - /// - public double? DurationSeconds { get; set; } - - /// - /// Gets or sets the character count (for TTS). - /// - public int? CharacterCount { get; set; } - - /// - /// Gets or sets the language used. - /// - public string? Language { get; set; } - - /// - /// Gets or sets when the request occurred. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - } - - /// - /// Type of audio request. - /// - public enum AudioRequestType - { - /// - /// Speech-to-text transcription. - /// - Transcription, - - /// - /// Text-to-speech synthesis. - /// - TextToSpeech, - - /// - /// Real-time audio conversation. - /// - Realtime - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioStreamCache.cs b/ConduitLLM.Core/Interfaces/IAudioStreamCache.cs deleted file mode 100644 index ad8fa011f..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioStreamCache.cs +++ /dev/null @@ -1,210 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for caching audio streams and transcriptions. - /// - public interface IAudioStreamCache - { - /// - /// Caches transcription results. - /// - /// The original request. - /// The transcription response. - /// Time to live for the cache entry. - /// Cancellation token. - Task CacheTranscriptionAsync( - AudioTranscriptionRequest request, - AudioTranscriptionResponse response, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default); - - /// - /// Gets cached transcription if available. - /// - /// The transcription request. - /// Cancellation token. - /// Cached response or null. - Task GetCachedTranscriptionAsync( - AudioTranscriptionRequest request, - CancellationToken cancellationToken = default); - - /// - /// Caches TTS audio data. - /// - /// The original request. - /// The TTS response. - /// Time to live for the cache entry. - /// Cancellation token. - Task CacheTtsAudioAsync( - TextToSpeechRequest request, - TextToSpeechResponse response, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default); - - /// - /// Gets cached TTS audio if available. - /// - /// The TTS request. - /// Cancellation token. - /// Cached response or null. - Task GetCachedTtsAudioAsync( - TextToSpeechRequest request, - CancellationToken cancellationToken = default); - - /// - /// Streams cached audio chunks for real-time playback. - /// - /// The cache key. - /// Cancellation token. - /// Stream of audio chunks. - IAsyncEnumerable StreamCachedAudioAsync( - string cacheKey, - CancellationToken cancellationToken = default); - - /// - /// Gets cache statistics. - /// - /// Cache statistics. - Task GetStatisticsAsync(); - - /// - /// Clears expired cache entries. - /// - /// Number of entries cleared. - Task ClearExpiredAsync(); - - /// - /// Preloads frequently used content into cache. - /// - /// Content to preload. - /// Cancellation token. - Task PreloadContentAsync( - PreloadContent content, - CancellationToken cancellationToken = default); - } - - /// - /// Statistics about the audio cache. - /// - public class AudioCacheStatistics - { - /// - /// Gets or sets the total cache entries. - /// - public long TotalEntries { get; set; } - - /// - /// Gets or sets the total cache size in bytes. - /// - public long TotalSizeBytes { get; set; } - - /// - /// Gets or sets the transcription cache hits. - /// - public long TranscriptionHits { get; set; } - - /// - /// Gets or sets the transcription cache misses. - /// - public long TranscriptionMisses { get; set; } - - /// - /// Gets or sets the TTS cache hits. - /// - public long TtsHits { get; set; } - - /// - /// Gets or sets the TTS cache misses. - /// - public long TtsMisses { get; set; } - - /// - /// Gets or sets the average entry size. - /// - public long AverageEntrySizeBytes { get; set; } - - /// - /// Gets or sets the oldest entry age. - /// - public TimeSpan OldestEntryAge { get; set; } - - /// - /// Gets the transcription hit rate. - /// - public double TranscriptionHitRate => - TranscriptionHits + TranscriptionMisses == 0 ? 0 : - (double)TranscriptionHits / (TranscriptionHits + TranscriptionMisses); - - /// - /// Gets the TTS hit rate. - /// - public double TtsHitRate => - TtsHits + TtsMisses == 0 ? 0 : - (double)TtsHits / (TtsHits + TtsMisses); - } - - /// - /// Content to preload into cache. - /// - public class PreloadContent - { - /// - /// Gets or sets common phrases to cache TTS for. - /// - public List CommonPhrases { get; set; } = new(); - - /// - /// Gets or sets common audio files to cache transcriptions for. - /// - public List CommonAudioFiles { get; set; } = new(); - } - - /// - /// TTS content to preload. - /// - public class PreloadTtsItem - { - /// - /// Gets or sets the text to synthesize. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Gets or sets the voice to use. - /// - public string Voice { get; set; } = string.Empty; - - /// - /// Gets or sets the language. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the cache TTL. - /// - public TimeSpan? Ttl { get; set; } - } - - /// - /// Transcription content to preload. - /// - public class PreloadTranscriptionItem - { - /// - /// Gets or sets the audio file URL or path. - /// - public string AudioSource { get; set; } = string.Empty; - - /// - /// Gets or sets the expected language. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the cache TTL. - /// - public TimeSpan? Ttl { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioTracingService.cs b/ConduitLLM.Core/Interfaces/IAudioTracingService.cs deleted file mode 100644 index 6a7d1dee1..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioTracingService.cs +++ /dev/null @@ -1,449 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for distributed tracing of audio operations. - /// - public interface IAudioTracingService - { - /// - /// Starts a new trace for an audio operation. - /// - /// The operation name. - /// The operation type. - /// Optional tags. - /// The trace context. - IAudioTraceContext StartTrace( - string operationName, - AudioOperation operationType, - Dictionary? tags = null); - - /// - /// Creates a child span within an existing trace. - /// - /// The parent trace context. - /// The span name. - /// Optional tags. - /// The span context. - IAudioSpanContext CreateSpan( - IAudioTraceContext parentContext, - string spanName, - Dictionary? tags = null); - - /// - /// Gets a trace by ID. - /// - /// The trace ID. - /// The trace details. - Task GetTraceAsync(string traceId); - - /// - /// Searches for traces. - /// - /// The search query. - /// List of matching traces. - Task> SearchTracesAsync(TraceSearchQuery query); - - /// - /// Gets trace statistics. - /// - /// Start time. - /// End time. - /// Trace statistics. - Task GetStatisticsAsync( - DateTime startTime, - DateTime endTime); - } - - /// - /// Audio trace context. - /// - public interface IAudioTraceContext : IDisposable - { - /// - /// Gets the trace ID. - /// - string TraceId { get; } - - /// - /// Gets the span ID. - /// - string SpanId { get; } - - /// - /// Adds a tag to the trace. - /// - /// Tag key. - /// Tag value. - void AddTag(string key, string value); - - /// - /// Adds an event to the trace. - /// - /// Event name. - /// Event attributes. - void AddEvent(string eventName, Dictionary? attributes = null); - - /// - /// Sets the status of the trace. - /// - /// The status. - /// Optional description. - void SetStatus(TraceStatus status, string? description = null); - - /// - /// Records an exception. - /// - /// The exception. - void RecordException(Exception exception); - - /// - /// Gets the trace propagation headers. - /// - /// Headers for trace propagation. - Dictionary GetPropagationHeaders(); - } - - /// - /// Audio span context. - /// - public interface IAudioSpanContext : IAudioTraceContext - { - /// - /// Gets the parent span ID. - /// - string? ParentSpanId { get; } - } - - /// - /// Audio trace details. - /// - public class AudioTrace - { - /// - /// Gets or sets the trace ID. - /// - public string TraceId { get; set; } = string.Empty; - - /// - /// Gets or sets the operation name. - /// - public string OperationName { get; set; } = string.Empty; - - /// - /// Gets or sets the operation type. - /// - public AudioOperation OperationType { get; set; } - - /// - /// Gets or sets the start time. - /// - public DateTime StartTime { get; set; } - - /// - /// Gets or sets the end time. - /// - public DateTime? EndTime { get; set; } - - /// - /// Gets or sets the duration in milliseconds. - /// - public double? DurationMs { get; set; } - - /// - /// Gets or sets the status. - /// - public TraceStatus Status { get; set; } - - /// - /// Gets or sets the status description. - /// - public string? StatusDescription { get; set; } - - /// - /// Gets or sets the tags. - /// - public Dictionary Tags { get; set; } = new(); - - /// - /// Gets or sets the spans. - /// - public List Spans { get; set; } = new(); - - /// - /// Gets or sets the events. - /// - public List Events { get; set; } = new(); - - /// - /// Gets or sets the virtual key. - /// - public string? VirtualKey { get; set; } - - /// - /// Gets or sets the provider used. - /// - public string? Provider { get; set; } - - /// - /// Gets or sets error information. - /// - public TraceError? Error { get; set; } - } - - /// - /// Audio span within a trace. - /// - public class AudioSpan - { - /// - /// Gets or sets the span ID. - /// - public string SpanId { get; set; } = string.Empty; - - /// - /// Gets or sets the parent span ID. - /// - public string? ParentSpanId { get; set; } - - /// - /// Gets or sets the span name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the start time. - /// - public DateTime StartTime { get; set; } - - /// - /// Gets or sets the end time. - /// - public DateTime? EndTime { get; set; } - - /// - /// Gets or sets the duration in milliseconds. - /// - public double? DurationMs { get; set; } - - /// - /// Gets or sets the tags. - /// - public Dictionary Tags { get; set; } = new(); - - /// - /// Gets or sets the events. - /// - public List Events { get; set; } = new(); - - /// - /// Gets or sets the status. - /// - public TraceStatus Status { get; set; } - } - - /// - /// Trace event. - /// - public class TraceEvent - { - /// - /// Gets or sets the event name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Gets or sets the timestamp. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets the attributes. - /// - public Dictionary Attributes { get; set; } = new(); - } - - /// - /// Trace error information. - /// - public class TraceError - { - /// - /// Gets or sets the error type. - /// - public string Type { get; set; } = string.Empty; - - /// - /// Gets or sets the error message. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Gets or sets the stack trace. - /// - public string? StackTrace { get; set; } - - /// - /// Gets or sets when the error occurred. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - } - - /// - /// Trace status. - /// - public enum TraceStatus - { - /// - /// Unset status. - /// - Unset, - - /// - /// Operation succeeded. - /// - Ok, - - /// - /// Operation failed. - /// - Error - } - - /// - /// Trace search query. - /// - public class TraceSearchQuery - { - /// - /// Gets or sets the start time. - /// - public DateTime? StartTime { get; set; } - - /// - /// Gets or sets the end time. - /// - public DateTime? EndTime { get; set; } - - /// - /// Gets or sets the operation type filter. - /// - public AudioOperation? OperationType { get; set; } - - /// - /// Gets or sets the status filter. - /// - public TraceStatus? Status { get; set; } - - /// - /// Gets or sets the provider filter. - /// - public string? Provider { get; set; } - - /// - /// Gets or sets the virtual key filter. - /// - public string? VirtualKey { get; set; } - - /// - /// Gets or sets the minimum duration in ms. - /// - public double? MinDurationMs { get; set; } - - /// - /// Gets or sets the maximum duration in ms. - /// - public double? MaxDurationMs { get; set; } - - /// - /// Gets or sets tag filters. - /// - public Dictionary TagFilters { get; set; } = new(); - - /// - /// Gets or sets the maximum results. - /// - public int MaxResults { get; set; } = 100; - } - - /// - /// Trace statistics. - /// - public class TraceStatistics - { - /// - /// Gets or sets the total traces. - /// - public long TotalTraces { get; set; } - - /// - /// Gets or sets successful traces. - /// - public long SuccessfulTraces { get; set; } - - /// - /// Gets or sets failed traces. - /// - public long FailedTraces { get; set; } - - /// - /// Gets or sets average duration. - /// - public double AverageDurationMs { get; set; } - - /// - /// Gets or sets P95 duration. - /// - public double P95DurationMs { get; set; } - - /// - /// Gets or sets P99 duration. - /// - public double P99DurationMs { get; set; } - - /// - /// Gets or sets operation breakdown. - /// - public Dictionary OperationBreakdown { get; set; } = new(); - - /// - /// Gets or sets provider breakdown. - /// - public Dictionary ProviderBreakdown { get; set; } = new(); - - /// - /// Gets or sets error breakdown. - /// - public Dictionary ErrorBreakdown { get; set; } = new(); - - /// - /// Gets or sets trace timeline. - /// - public List Timeline { get; set; } = new(); - } - - /// - /// Point in trace timeline. - /// - public class TraceTimelinePoint - { - /// - /// Gets or sets the timestamp. - /// - public DateTime Timestamp { get; set; } - - /// - /// Gets or sets the trace count. - /// - public int TraceCount { get; set; } - - /// - /// Gets or sets the error count. - /// - public int ErrorCount { get; set; } - - /// - /// Gets or sets the average duration. - /// - public double AverageDurationMs { get; set; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IAudioTranscriptionClient.cs b/ConduitLLM.Core/Interfaces/IAudioTranscriptionClient.cs deleted file mode 100644 index 82861f467..000000000 --- a/ConduitLLM.Core/Interfaces/IAudioTranscriptionClient.cs +++ /dev/null @@ -1,113 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for audio transcription (Speech-to-Text) capabilities. - /// - /// - /// - /// The IAudioTranscriptionClient interface provides a standardized way to convert - /// audio content into text across different provider implementations. This includes - /// support for various audio formats, languages, and transcription options. - /// - /// - /// Implementations of this interface handle provider-specific details such as: - /// - /// - /// Audio format conversion and validation - /// Language detection and specification - /// Timestamp generation for words or segments - /// Provider-specific parameter mappings - /// Error handling and retry logic - /// - /// - public interface IAudioTranscriptionClient - { - /// - /// Transcribes audio content into text. - /// - /// The transcription request containing audio data and parameters. - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// The transcription response containing the converted text and metadata. - /// Thrown when the request fails validation. - /// Thrown when there is an error communicating with the provider. - /// Thrown when the provider does not support transcription. - /// - /// - /// This method accepts audio in various formats (mp3, mp4, wav, etc.) and converts it to text. - /// The audio can be provided as raw bytes or as a URL reference, depending on provider support. - /// - /// - /// The response includes the transcribed text and may optionally include: - /// - /// - /// Detected language of the audio - /// Word-level or segment-level timestamps - /// Confidence scores for the transcription - /// Alternative transcriptions - /// - /// - Task TranscribeAudioAsync( - AudioTranscriptionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Checks if the client supports audio transcription. - /// - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// True if transcription is supported, false otherwise. - /// - /// - /// This method allows callers to check transcription support before attempting - /// to use the service. This is useful for graceful degradation and routing decisions. - /// - /// - /// Support may vary based on the API key permissions or the specific model configured - /// for the client instance. - /// - /// - Task SupportsTranscriptionAsync( - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Lists the audio formats supported by this transcription client. - /// - /// A token to cancel the operation. - /// A list of supported audio format identifiers. - /// - /// - /// Returns the audio formats that this provider can process, such as: - /// mp3, mp4, mpeg, mpga, m4a, wav, webm, flac, ogg, etc. - /// - /// - /// This information can be used to validate input formats or to convert - /// audio to a supported format before transcription. - /// - /// - Task> GetSupportedFormatsAsync( - CancellationToken cancellationToken = default); - - /// - /// Lists the languages supported by this transcription client. - /// - /// A token to cancel the operation. - /// A list of supported language codes (ISO 639-1 format). - /// - /// - /// Returns the languages that this provider can transcribe, using standard - /// ISO 639-1 language codes (e.g., "en", "es", "fr", "zh"). - /// - /// - /// Some providers may support automatic language detection, in which case - /// the language parameter in the request can be omitted. - /// - /// - Task> GetSupportedLanguagesAsync( - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/IConduit.cs b/ConduitLLM.Core/Interfaces/IConduit.cs index b2c690bf8..f44fa3071 100644 --- a/ConduitLLM.Core/Interfaces/IConduit.cs +++ b/ConduitLLM.Core/Interfaces/IConduit.cs @@ -56,12 +56,6 @@ Task CreateImageAsync( string? apiKey = null, CancellationToken cancellationToken = default); - /// - /// Gets the router instance if one is configured. - /// - /// The router instance or null if none is configured. - ILLMRouter? GetRouter(); - /// /// Gets an LLM client for the specified model. /// diff --git a/ConduitLLM.Core/Interfaces/IHybridAudioService.cs b/ConduitLLM.Core/Interfaces/IHybridAudioService.cs deleted file mode 100644 index 29b2c473c..000000000 --- a/ConduitLLM.Core/Interfaces/IHybridAudioService.cs +++ /dev/null @@ -1,146 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Provides hybrid audio conversation capabilities by chaining STT, LLM, and TTS services. - /// - /// - /// - /// The IHybridAudioService interface enables conversational AI experiences for providers - /// that don't have native real-time audio support. It accomplishes this by: - /// - /// - /// Converting speech to text using an STT provider - /// Processing the text through an LLM for response generation - /// Converting the response back to speech using a TTS provider - /// - /// - /// This service is designed to minimize latency while providing a seamless audio - /// conversation experience, with support for interruptions and context management. - /// - /// - public interface IHybridAudioService - { - /// - /// Processes a single audio input through the STT-LLM-TTS pipeline. - /// - /// The hybrid audio request containing audio data and configuration. - /// A token to cancel the operation. - /// The response containing the generated audio and metadata. - /// Thrown when the request fails validation. - /// Thrown when there is an error in any pipeline stage. - /// - /// - /// This method orchestrates the complete pipeline: - /// - /// - /// Transcribes the input audio to text - /// Sends the text to the LLM with conversation context - /// Converts the LLM response to speech - /// - /// - /// The response includes both the generated audio and intermediate results - /// (transcription and LLM response text) for debugging and logging purposes. - /// - /// - Task ProcessAudioAsync( - HybridAudioRequest request, - CancellationToken cancellationToken = default); - - /// - /// Processes audio input with streaming output for lower latency. - /// - /// The hybrid audio request containing audio data and configuration. - /// A token to cancel the operation. - /// An async enumerable of response chunks as they are generated. - /// Thrown when the request fails validation. - /// Thrown when there is an error in any pipeline stage. - /// - /// - /// This streaming version provides lower latency by: - /// - /// - /// Streaming LLM tokens as they are generated - /// Starting TTS synthesis before the complete response is available - /// Yielding audio chunks progressively for immediate playback - /// - /// - /// Each chunk contains partial audio data and metadata about the generation progress. - /// - /// - IAsyncEnumerable StreamProcessAudioAsync( - HybridAudioRequest request, - CancellationToken cancellationToken = default); - - /// - /// Creates a new conversation session for managing context across multiple interactions. - /// - /// Configuration for the conversation session. - /// A token to cancel the operation. - /// A session ID for tracking the conversation. - /// - /// - /// Sessions maintain conversation history and context, enabling multi-turn - /// conversations with consistent personality and memory of previous interactions. - /// - /// - /// Sessions should be explicitly closed when no longer needed to free resources. - /// - /// - Task CreateSessionAsync( - HybridSessionConfig config, - CancellationToken cancellationToken = default); - - /// - /// Closes an active conversation session and releases associated resources. - /// - /// The ID of the session to close. - /// A token to cancel the operation. - /// A task representing the asynchronous operation. - /// - /// This method cleans up session state and ensures any pending operations - /// are completed or cancelled appropriately. - /// - Task CloseSessionAsync( - string sessionId, - CancellationToken cancellationToken = default); - - /// - /// Checks if the hybrid audio service is available with the current configuration. - /// - /// A token to cancel the operation. - /// True if all required services (STT, LLM, TTS) are available. - /// - /// - /// This method verifies that: - /// - /// - /// An STT provider is configured and available - /// An LLM provider is configured and available - /// A TTS provider is configured and available - /// - /// - Task IsAvailableAsync(CancellationToken cancellationToken = default); - - /// - /// Gets the current latency metrics for the hybrid pipeline. - /// - /// A token to cancel the operation. - /// Latency information for each pipeline stage. - /// - /// - /// Returns timing information that can be used to optimize the pipeline, - /// including average latencies for: - /// - /// - /// Speech-to-text transcription - /// LLM response generation - /// Text-to-speech synthesis - /// Total end-to-end processing - /// - /// - Task GetLatencyMetricsAsync( - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/ILLMRouter.cs b/ConduitLLM.Core/Interfaces/ILLMRouter.cs deleted file mode 100644 index 3ac7221ac..000000000 --- a/ConduitLLM.Core/Interfaces/ILLMRouter.cs +++ /dev/null @@ -1,206 +0,0 @@ -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for a router that manages multiple LLM deployments and provides failover, - /// load balancing, and optimal model selection. - /// - /// - /// - /// The LLM router is responsible for intelligent routing of LLM requests to the appropriate - /// model deployment based on various strategies and system conditions. It provides: - /// - /// - /// - /// Automatic failover when a model is unhealthy or unavailable - /// - /// - /// Multiple routing strategies (simple, round-robin, least cost, etc.) - /// - /// - /// Health monitoring of model deployments - /// - /// - /// Fallback chains for graceful degradation - /// - /// - /// Load balancing capabilities across equivalent models - /// - /// - /// - /// The router acts as a higher-level abstraction over the , - /// adding intelligence to the model selection process beyond simple configuration mapping. - /// - /// - public interface ILLMRouter - { - /// - /// Creates a chat completion using the configured routing strategy. - /// - /// The chat completion request (model will be determined by router). - /// Optional routing strategy to override the default. - /// Optional API key to override the configured key. - /// A token to cancel the operation. - /// The chat completion response from the selected model. - /// Thrown when no suitable model is available for the request. - /// Thrown when all attempts to communicate with suitable models fail. - /// - /// - /// The router selects the appropriate model based on the specified routing strategy, - /// model availability, and health status. It will attempt multiple models if necessary, - /// based on the configured fallbacks and retry settings. - /// - /// - /// If the request contains a specific model in the property, - /// the router will attempt to use that model first, then fall back to alternatives if needed. - /// - /// - /// Available routing strategies include: - /// - /// - /// - /// simple - /// Uses the first available healthy model - /// - /// - /// roundrobin - /// Distributes requests evenly across available models - /// - /// - /// leastcost - /// Selects the model with the lowest token cost - /// - /// - /// leastlatency - /// Selects the model with the lowest average latency - /// - /// - /// highestpriority - /// Selects the model with the highest configured priority - /// - /// - /// passthrough - /// Uses exactly the model specified in the request without routing logic - /// - /// - /// - Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? routingStrategy = null, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Creates a streaming chat completion using the configured routing strategy. - /// - /// The chat completion request (model will be determined by router). - /// Optional routing strategy to override the default. - /// Optional API key to override the configured key. - /// A token to cancel the operation. - /// An asynchronous enumerable of chat completion chunks. - /// Thrown when no suitable model is available for the request. - /// Thrown when all attempts to communicate with suitable models fail. - /// - /// - /// This method is similar to but returns - /// a stream of completion chunks rather than a complete response. Due to the streaming - /// nature, the router must select a single model up front rather than retrying - /// during the stream. - /// - /// - /// The router will mark models as unhealthy if they fail to produce any chunks or - /// encounter errors during streaming. - /// - /// - /// The same routing strategies available to - /// are also available for streaming. - /// - /// - IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? routingStrategy = null, - string? apiKey = null, - CancellationToken cancellationToken = default); - - - /// - /// Gets the available models for routing. - /// - /// List of model names available for routing. - /// - /// - /// This method returns all models registered with the router, regardless of - /// their current health status. Use this to determine which models are - /// configured in the routing system. - /// - /// - /// For more detailed model information, including capabilities and other metadata, - /// use instead. - /// - /// - IReadOnlyList GetAvailableModels(); - - /// - /// Gets the fallback models for a given model. - /// - /// The primary model name. - /// List of fallback model names or empty list if none configured. - /// - /// - /// Fallback models are used when the primary model is unavailable or unhealthy. - /// The router will attempt models in the order they appear in the fallback list. - /// - /// - /// An empty list indicates that no fallbacks are configured for the specified model. - /// - /// - IReadOnlyList GetFallbackModels(string modelName); - - /// - /// Creates embeddings using the configured routing strategy. - /// - /// The embedding request (model will be determined by router). - /// Optional routing strategy to override the default. - /// Optional API key to override the configured key. - /// A token to cancel the operation. - /// The embedding response from the selected model. - /// Thrown when no suitable model is available for the request. - /// Thrown when all attempts to communicate with suitable models fail. - /// Thrown when embedding functionality is not supported by available models. - /// - /// - /// Similar to , this method selects an appropriate - /// model for creating embeddings based on the specified routing strategy and model availability. - /// - /// - /// Note that not all LLM providers support embeddings. The router will automatically - /// filter to models that support this functionality. - /// - /// - Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? routingStrategy = null, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Gets detailed information about the available models suitable for the /models endpoint. - /// - /// A token to cancel the operation. - /// A list of detailed model information objects. - /// - /// - /// This method provides more detailed information about available models than - /// , including capabilities and other metadata. - /// - /// - /// The information returned is suitable for exposing through a /models API endpoint - /// that follows the OpenAI API convention. - /// - /// - Task> GetAvailableModelDetailsAsync( - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/IModelCapabilityService.cs b/ConduitLLM.Core/Interfaces/IModelCapabilityService.cs index 15b5c8f15..9d188eaa1 100644 --- a/ConduitLLM.Core/Interfaces/IModelCapabilityService.cs +++ b/ConduitLLM.Core/Interfaces/IModelCapabilityService.cs @@ -13,26 +13,6 @@ public interface IModelCapabilityService /// True if the model supports vision inputs, false otherwise. Task SupportsVisionAsync(string model); - /// - /// Determines if a model supports audio transcription (Speech-to-Text). - /// - /// The model identifier to check. - /// True if the model supports audio transcription, false otherwise. - Task SupportsAudioTranscriptionAsync(string model); - - /// - /// Determines if a model supports text-to-speech generation. - /// - /// The model identifier to check. - /// True if the model supports TTS, false otherwise. - Task SupportsTextToSpeechAsync(string model); - - /// - /// Determines if a model supports real-time audio streaming. - /// - /// The model identifier to check. - /// True if the model supports real-time audio, false otherwise. - Task SupportsRealtimeAudioAsync(string model); /// /// Determines if a model supports video generation. @@ -48,32 +28,12 @@ public interface IModelCapabilityService /// The tokenizer type (e.g., "cl100k_base", "p50k_base", "claude") or null if not specified. Task GetTokenizerTypeAsync(string model); - /// - /// Gets the list of supported voices for a TTS model. - /// - /// The model identifier. - /// List of supported voice identifiers. - Task> GetSupportedVoicesAsync(string model); - - /// - /// Gets the list of supported languages for a model. - /// - /// The model identifier. - /// List of supported language codes. - Task> GetSupportedLanguagesAsync(string model); - - /// - /// Gets the list of supported audio formats for a model. - /// - /// The model identifier. - /// List of supported audio format identifiers. - Task> GetSupportedFormatsAsync(string model); /// /// Gets the default model for a specific provider and capability type. /// /// The provider name (e.g., "openai", "anthropic"). - /// The capability type (e.g., "chat", "transcription", "tts", "realtime"). + /// The capability type (e.g., "chat", "vision", "embeddings"). /// The default model identifier or null if no default is configured. Task GetDefaultModelAsync(string provider, string capabilityType); diff --git a/ConduitLLM.Core/Interfaces/IModelSelectionStrategy.cs b/ConduitLLM.Core/Interfaces/IModelSelectionStrategy.cs deleted file mode 100644 index ab0422768..000000000 --- a/ConduitLLM.Core/Interfaces/IModelSelectionStrategy.cs +++ /dev/null @@ -1,37 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for model selection strategies used by the router. - /// - /// - /// - /// This interface defines the contract for model selection strategies that can be used - /// by the LLM router to select the most appropriate model for a request based on different criteria. - /// - /// - /// Implementation of this interface should encapsulate a specific selection algorithm, - /// such as least cost, round robin, or least latency. The router can then dynamically - /// switch between strategies based on configuration or request requirements. - /// - /// - public interface IModelSelectionStrategy - { - /// - /// Selects the most appropriate model from a list of available models. - /// - /// The list of available model names. - /// Dictionary of model deployments keyed by deployment name. - /// Dictionary of model usage counts keyed by model name. - /// The name of the selected model, or null if no model could be selected. - /// - /// Implementations should select a model based on the strategy's specific algorithm - /// (e.g., least cost, least used, random) using the provided model information. - /// - string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts); - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeAudioClient.cs b/ConduitLLM.Core/Interfaces/IRealtimeAudioClient.cs deleted file mode 100644 index 33ab00457..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeAudioClient.cs +++ /dev/null @@ -1,223 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for real-time conversational audio AI capabilities. - /// - /// - /// - /// The IRealtimeAudioClient interface provides a standardized way to handle - /// bidirectional audio streaming for conversational AI applications. This enables - /// low-latency, natural conversations with AI models that can process audio input - /// and generate audio responses in real-time. - /// - /// - /// Key features supported by implementations: - /// - /// - /// Bidirectional audio streaming via WebSockets - /// Voice activity detection (VAD) and turn management - /// Interruption handling for natural conversations - /// Function calling during audio conversations - /// Multiple voice and persona options - /// Real-time transcription of both user and AI speech - /// - /// - /// This interface abstracts provider-specific implementations from OpenAI Realtime API, - /// Ultravox, ElevenLabs Conversational AI, and other emerging real-time AI platforms. - /// - /// - public interface IRealtimeAudioClient - { - /// - /// Creates a new real-time audio session with the AI provider. - /// - /// Configuration for the real-time session including voice, model, and behavior settings. - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// A session object representing the established connection. - /// Thrown when the configuration fails validation. - /// Thrown when there is an error establishing the connection. - /// Thrown when the provider does not support real-time audio. - /// - /// - /// This method establishes a WebSocket connection to the provider's real-time endpoint - /// and configures the session with the specified parameters. The session must be - /// properly disposed when no longer needed to close the connection. - /// - /// - /// Configuration options vary by provider but typically include: - /// - /// - /// Model selection (e.g., gpt-4o-realtime) - /// Voice selection for AI responses - /// Turn detection settings (VAD parameters) - /// System prompt for conversation context - /// Function definitions for tool use - /// Audio format specifications - /// - /// - Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Starts bidirectional audio streaming for the established session. - /// - /// The active real-time session to stream with. - /// A token to cancel the streaming operation. - /// A duplex stream for sending and receiving audio/events. - /// Thrown when the session is not in a valid state for streaming. - /// Thrown when there is an error during streaming. - /// - /// - /// The returned duplex stream allows simultaneous sending of audio input and - /// receiving of AI responses. The stream handles multiple event types: - /// - /// - /// Audio input frames from the user - /// Audio output frames from the AI - /// Transcription updates for both parties - /// Turn start/end events - /// Function call requests and responses - /// Error and status events - /// - /// - /// The stream continues until explicitly closed or an error occurs. Proper - /// error handling and reconnection logic should be implemented by callers. - /// - /// - IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default); - - /// - /// Updates the configuration of an active real-time session. - /// - /// The session to update. - /// The configuration updates to apply. - /// A token to cancel the operation. - /// A task representing the update operation. - /// Thrown when the session cannot be updated. - /// Thrown when the updates are invalid. - /// - /// - /// Allows dynamic updates to session parameters without disconnecting, such as: - /// - /// - /// Changing the system prompt mid-conversation - /// Updating voice settings - /// Modifying turn detection parameters - /// Adding or removing function definitions - /// - /// - /// Not all parameters may be updatable depending on the provider's capabilities. - /// Some changes may only take effect for subsequent turns in the conversation. - /// - /// - Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default); - - /// - /// Closes an active real-time session. - /// - /// The session to close. - /// A token to cancel the operation. - /// A task representing the close operation. - /// - /// - /// Properly closes the WebSocket connection and cleans up resources associated - /// with the session. This method should always be called when a session is no - /// longer needed, even if an error occurred during streaming. - /// - /// - /// After closing, the session object should not be reused. A new session must - /// be created for subsequent conversations. - /// - /// - Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default); - - /// - /// Checks if the client supports real-time audio conversations. - /// - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// True if real-time audio is supported, false otherwise. - /// - /// - /// This method allows callers to check real-time support before attempting - /// to create a session. Support may depend on: - /// - /// - /// Provider capabilities - /// API key permissions - /// Model availability - /// Regional restrictions - /// - /// - Task SupportsRealtimeAsync( - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Gets the capabilities and limitations of this real-time client. - /// - /// A token to cancel the operation. - /// Detailed information about supported features. - /// - /// - /// Returns information about provider-specific capabilities such as: - /// - /// - /// Supported audio formats and sample rates - /// Available voices and their characteristics - /// Turn detection options - /// Function calling support - /// Maximum session duration - /// Concurrent session limits - /// - /// - Task GetCapabilitiesAsync( - CancellationToken cancellationToken = default); - } - - /// - /// Represents a bidirectional stream for real-time audio communication. - /// - /// The type of data sent to the stream. - /// The type of data received from the stream. - public interface IAsyncDuplexStream - { - /// - /// Sends data to the stream. - /// - /// The data to send. - /// A token to cancel the send operation. - /// A task representing the send operation. - ValueTask SendAsync(TInput item, CancellationToken cancellationToken = default); - - /// - /// Receives data from the stream. - /// - /// A token to cancel the receive operation. - /// An async enumerable of received data. - IAsyncEnumerable ReceiveAsync(CancellationToken cancellationToken = default); - - /// - /// Completes the input side of the stream, signaling no more data will be sent. - /// - /// A task representing the completion operation. - ValueTask CompleteAsync(); - - /// - /// Gets whether the stream is still connected and operational. - /// - bool IsConnected { get; } - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeConnectionManager.cs b/ConduitLLM.Core/Interfaces/IRealtimeConnectionManager.cs deleted file mode 100644 index 88e428960..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeConnectionManager.cs +++ /dev/null @@ -1,118 +0,0 @@ -using System.Net.WebSockets; - -using ConduitLLM.Core.Models.Realtime; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Manages active real-time WebSocket connections. - /// - /// - /// This service tracks all active connections, enforces connection limits, - /// and provides connection lifecycle management. - /// - public interface IRealtimeConnectionManager - { - /// - /// Registers a new WebSocket connection. - /// - /// Unique identifier for the connection. - /// The virtual key ID associated with this connection. - /// The model being used. - /// The WebSocket instance. - /// A task that completes when registration is done. - /// Thrown when connection limit is exceeded. - Task RegisterConnectionAsync( - string connectionId, - int virtualKeyId, - string model, - WebSocket webSocket); - - /// - /// Unregisters a connection when it closes. - /// - /// The connection ID to unregister. - /// A task that completes when unregistration is done. - Task UnregisterConnectionAsync(string connectionId); - - /// - /// Gets all active connections for a virtual key. - /// - /// The virtual key ID to query. - /// List of active connections. - Task> GetActiveConnectionsAsync(int virtualKeyId); - - /// - /// Gets the total number of active connections across all keys. - /// - /// The total connection count. - Task GetTotalConnectionCountAsync(); - - /// - /// Gets connection information by ID. - /// - /// The connection ID to query. - /// Connection information, or null if not found. - Task GetConnectionAsync(string connectionId); - - /// - /// Attempts to terminate a specific connection. - /// - /// The connection ID to terminate. - /// The virtual key ID (for ownership verification). - /// True if terminated, false if not found or not owned. - Task TerminateConnectionAsync(string connectionId, int virtualKeyId); - - /// - /// Checks if a virtual key has reached its connection limit. - /// - /// The virtual key ID to check. - /// True if at limit, false otherwise. - Task IsAtConnectionLimitAsync(int virtualKeyId); - - /// - /// Updates usage statistics for a connection. - /// - /// The connection ID. - /// The usage statistics to update. - /// A task that completes when the update is done. - Task UpdateUsageStatsAsync(string connectionId, ConnectionUsageStats stats); - - /// - /// Performs health checks on all connections and removes stale ones. - /// - /// Number of connections cleaned up. - Task CleanupStaleConnectionsAsync(); - } - - /// - /// Detailed information about a managed connection. - /// - public class ManagedConnection - { - /// - /// The connection information. - /// - public ConnectionInfo Info { get; set; } = new(); - - /// - /// The WebSocket instance. - /// - public WebSocket? WebSocket { get; set; } - - /// - /// The virtual key ID. - /// - public int VirtualKeyId { get; set; } - - /// - /// Last heartbeat timestamp. - /// - public DateTime LastHeartbeat { get; set; } - - /// - /// Whether the connection is healthy. - /// - public bool IsHealthy { get; set; } = true; - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslator.cs b/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslator.cs deleted file mode 100644 index 4a8e0b149..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslator.cs +++ /dev/null @@ -1,166 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for translating real-time messages between - /// Conduit's unified format and provider-specific formats. - /// - /// - /// Each provider (OpenAI, Ultravox, ElevenLabs) has different message - /// formats and protocols. This interface allows for bidirectional - /// translation while maintaining a consistent API for clients. - /// - public interface IRealtimeMessageTranslator - { - /// - /// Gets the provider this translator handles. - /// - string Provider { get; } - - /// - /// Translates a message from Conduit format to provider format. - /// - /// The Conduit-format message. - /// Provider-specific message data (usually JSON string). - /// Thrown when message type is not supported by provider. - Task TranslateToProviderAsync(RealtimeMessage message); - - /// - /// Translates a message from provider format to Conduit format. - /// - /// The provider-specific message data. - /// One or more Conduit-format messages (providers may send compound messages). - /// Thrown when provider message is malformed. - Task> TranslateFromProviderAsync(string providerMessage); - - /// - /// Validates that a session configuration is supported by the provider. - /// - /// The session configuration to validate. - /// Validation result with any warnings or errors. - Task ValidateSessionConfigAsync(RealtimeSessionConfig config); - - /// - /// Transforms a session configuration to provider-specific format. - /// - /// The Conduit session configuration. - /// Provider-specific configuration data. - Task TransformSessionConfigAsync(RealtimeSessionConfig config); - - /// - /// Gets the WebSocket subprotocol required by the provider, if any. - /// - /// Subprotocol string or null if not required. - string? GetRequiredSubprotocol(); - - /// - /// Gets custom headers required for the WebSocket connection. - /// - /// The session configuration. - /// Dictionary of header names and values. - Task> GetConnectionHeadersAsync(RealtimeSessionConfig config); - - /// - /// Handles provider-specific connection initialization. - /// - /// The session configuration. - /// Initial messages to send after connection. - Task> GetInitializationMessagesAsync(RealtimeSessionConfig config); - - /// - /// Maps provider-specific error codes to Conduit error types. - /// - /// The provider error message or code. - /// Standardized error information. - RealtimeError TranslateError(string providerError); - } - - /// - /// Result of validating a configuration for translation. - /// - public class TranslationValidationResult - { - /// - /// Whether the configuration is valid. - /// - public bool IsValid { get; set; } - - /// - /// Any validation errors. - /// - public List Errors { get; set; } = new(); - - /// - /// Any warnings (non-fatal issues). - /// - public List Warnings { get; set; } = new(); - - /// - /// Suggested configuration adjustments. - /// - public Dictionary? SuggestedAdjustments { get; set; } - } - - /// - /// Standardized error information for real-time connections. - /// - public class RealtimeError - { - /// - /// Error code. - /// - public string Code { get; set; } = string.Empty; - - /// - /// Human-readable error message. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Error severity. - /// - public ErrorSeverity Severity { get; set; } - - /// - /// Whether the connection should be terminated. - /// - public bool IsTerminal { get; set; } - - /// - /// Suggested retry delay in milliseconds, if applicable. - /// - public int? RetryAfterMs { get; set; } - - /// - /// Additional error details from the provider. - /// - public Dictionary? Details { get; set; } - } - - /// - /// Severity levels for real-time errors. - /// - public enum ErrorSeverity - { - /// - /// Informational, not an actual error. - /// - Info, - - /// - /// Warning that doesn't affect functionality. - /// - Warning, - - /// - /// Error that affects some functionality. - /// - Error, - - /// - /// Critical error requiring immediate action. - /// - Critical - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslatorFactory.cs b/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslatorFactory.cs deleted file mode 100644 index 732f541a0..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeMessageTranslatorFactory.cs +++ /dev/null @@ -1,35 +0,0 @@ -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Factory for creating real-time message translators for different providers. - /// - public interface IRealtimeMessageTranslatorFactory - { - /// - /// Gets a translator for the specified provider. - /// - /// The provider name (e.g., "OpenAI", "Ultravox", "ElevenLabs"). - /// The message translator, or null if provider is not supported. - IRealtimeMessageTranslator? GetTranslator(string provider); - - /// - /// Registers a translator for a provider. - /// - /// The provider name. - /// The translator implementation. - void RegisterTranslator(string provider, IRealtimeMessageTranslator translator); - - /// - /// Checks if a translator is available for a provider. - /// - /// The provider name. - /// True if a translator is registered. - bool HasTranslator(string provider); - - /// - /// Gets all registered provider names. - /// - /// Array of provider names. - string[] GetRegisteredProviders(); - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeProxyService.cs b/ConduitLLM.Core/Interfaces/IRealtimeProxyService.cs deleted file mode 100644 index 26acb37e1..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeProxyService.cs +++ /dev/null @@ -1,154 +0,0 @@ -using System.Net.WebSockets; - -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for the real-time audio proxy service that handles - /// WebSocket connections between clients and providers. - /// - /// - /// The proxy service is responsible for: - /// - Establishing connections to the appropriate provider - /// - Translating messages between Conduit's format and provider formats - /// - Tracking usage and costs - /// - Handling connection lifecycle and errors - /// - Ensuring message delivery and resilience - /// - public interface IRealtimeProxyService - { - /// - /// Handles a WebSocket connection from a client, proxying messages to/from the provider. - /// - /// Unique identifier for this connection. - /// The client's WebSocket connection. - /// The authenticated virtual key entity. - /// The model to use for the session. - /// Optional provider override (null to use routing). - /// Cancellation token for the operation. - /// A task that completes when the connection is closed. - /// Thrown when required parameters are null. - /// Thrown when no suitable provider is available. - /// Thrown when the virtual key lacks necessary permissions. - Task HandleConnectionAsync( - string connectionId, - WebSocket clientWebSocket, - VirtualKey virtualKey, - string model, - string? provider, - CancellationToken cancellationToken = default); - - /// - /// Gets the current status of a proxy connection. - /// - /// The connection ID to query. - /// The connection status, or null if not found. - Task GetConnectionStatusAsync(string connectionId); - - /// - /// Attempts to gracefully close a proxy connection. - /// - /// The connection ID to close. - /// Optional reason for closing. - /// True if the connection was closed, false if not found. - Task CloseConnectionAsync(string connectionId, string? reason = null); - } - - /// - /// Status information for a proxy connection. - /// - public class ProxyConnectionStatus - { - /// - /// The connection identifier. - /// - public string ConnectionId { get; set; } = string.Empty; - - /// - /// Current state of the connection. - /// - public ProxyConnectionState State { get; set; } - - /// - /// The provider being used. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// The model being used. - /// - public string Model { get; set; } = string.Empty; - - /// - /// When the connection was established. - /// - public DateTime ConnectedAt { get; set; } - - /// - /// Last activity timestamp. - /// - public DateTime LastActivityAt { get; set; } - - /// - /// Number of messages sent to provider. - /// - public long MessagesToProvider { get; set; } - - /// - /// Number of messages received from provider. - /// - public long MessagesFromProvider { get; set; } - - /// - /// Total bytes sent. - /// - public long BytesSent { get; set; } - - /// - /// Total bytes received. - /// - public long BytesReceived { get; set; } - - /// - /// Current estimated cost. - /// - public decimal EstimatedCost { get; set; } - - /// - /// Any error information. - /// - public string? LastError { get; set; } - } - - /// - /// States for a proxy connection. - /// - public enum ProxyConnectionState - { - /// - /// Connection is being established. - /// - Connecting, - - /// - /// Connection is active and passing messages. - /// - Active, - - /// - /// Connection is closing gracefully. - /// - Closing, - - /// - /// Connection has been closed. - /// - Closed, - - /// - /// Connection failed with an error. - /// - Failed - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeSessionStore.cs b/ConduitLLM.Core/Interfaces/IRealtimeSessionStore.cs deleted file mode 100644 index 70ab83573..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeSessionStore.cs +++ /dev/null @@ -1,88 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for storing and managing real-time audio session state. - /// - public interface IRealtimeSessionStore - { - /// - /// Stores a new session. - /// - /// The session to store. - /// Time to live for the session data. - /// Cancellation token. - Task StoreSessionAsync( - RealtimeSession session, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default); - - /// - /// Retrieves a session by ID. - /// - /// The session ID. - /// Cancellation token. - /// The session if found, null otherwise. - Task GetSessionAsync( - string sessionId, - CancellationToken cancellationToken = default); - - /// - /// Updates an existing session. - /// - /// The updated session. - /// Cancellation token. - Task UpdateSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default); - - /// - /// Removes a session from storage. - /// - /// The session ID to remove. - /// Cancellation token. - Task RemoveSessionAsync( - string sessionId, - CancellationToken cancellationToken = default); - - /// - /// Gets all active sessions. - /// - /// Cancellation token. - /// List of active sessions. - Task> GetActiveSessionsAsync( - CancellationToken cancellationToken = default); - - /// - /// Gets sessions by virtual key. - /// - /// The virtual key to filter by. - /// Cancellation token. - /// List of sessions for the virtual key. - Task> GetSessionsByVirtualKeyAsync( - string virtualKey, - CancellationToken cancellationToken = default); - - /// - /// Updates session metrics. - /// - /// The session ID. - /// Updated metrics. - /// Cancellation token. - Task UpdateSessionMetricsAsync( - string sessionId, - SessionStatistics metrics, - CancellationToken cancellationToken = default); - - /// - /// Performs cleanup of expired sessions. - /// - /// Maximum age for sessions before cleanup. - /// Cancellation token. - /// Number of sessions cleaned up. - Task CleanupExpiredSessionsAsync( - TimeSpan maxAge, - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/IRealtimeUsageTracker.cs b/ConduitLLM.Core/Interfaces/IRealtimeUsageTracker.cs deleted file mode 100644 index 26a73d62b..000000000 --- a/ConduitLLM.Core/Interfaces/IRealtimeUsageTracker.cs +++ /dev/null @@ -1,168 +0,0 @@ -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Realtime; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Tracks usage and costs for real-time audio sessions. - /// - public interface IRealtimeUsageTracker - { - /// - /// Starts tracking usage for a new session. - /// - /// The connection identifier. - /// The virtual key ID. - /// The model being used. - /// The provider being used. - /// A task that completes when tracking is initialized. - Task StartTrackingAsync(string connectionId, int virtualKeyId, string model, string provider); - - /// - /// Updates usage statistics for an active session. - /// - /// The connection identifier. - /// The usage statistics to update. - /// A task that completes when the update is recorded. - Task UpdateUsageAsync(string connectionId, ConnectionUsageStats stats); - - /// - /// Records audio usage for billing purposes. - /// - /// The connection identifier. - /// Duration of audio in seconds. - /// True for input audio, false for output. - /// A task that completes when the usage is recorded. - Task RecordAudioUsageAsync(string connectionId, double audioSeconds, bool isInput); - - /// - /// Records token usage for text portions of the conversation. - /// - /// The connection identifier. - /// Token usage information. - /// A task that completes when the usage is recorded. - Task RecordTokenUsageAsync(string connectionId, Usage usage); - - /// - /// Records a function call for billing purposes. - /// - /// The connection identifier. - /// Optional function name for logging. - /// A task that completes when the function call is recorded. - Task RecordFunctionCallAsync(string connectionId, string? functionName = null); - - /// - /// Gets the current estimated cost for a session. - /// - /// The connection identifier. - /// The estimated cost in the billing currency. - Task GetEstimatedCostAsync(string connectionId); - - /// - /// Finalizes usage tracking for a completed session. - /// - /// The connection identifier. - /// The final usage statistics. - /// The final cost for the session. - Task FinalizeUsageAsync(string connectionId, ConnectionUsageStats finalStats); - - /// - /// Gets detailed usage breakdown for a session. - /// - /// The connection identifier. - /// Detailed usage information. - Task GetUsageDetailsAsync(string connectionId); - } - - /// - /// Detailed usage information for a real-time session. - /// - public class RealtimeUsageDetails - { - /// - /// The connection identifier. - /// - public string ConnectionId { get; set; } = string.Empty; - - /// - /// Total input audio duration in seconds. - /// - public double InputAudioSeconds { get; set; } - - /// - /// Total output audio duration in seconds. - /// - public double OutputAudioSeconds { get; set; } - - /// - /// Total input tokens (for text/function calls). - /// - public int InputTokens { get; set; } - - /// - /// Total output tokens (for text/function responses). - /// - public int OutputTokens { get; set; } - - /// - /// Number of function calls made. - /// - public int FunctionCalls { get; set; } - - /// - /// Session duration in seconds. - /// - public double SessionDurationSeconds { get; set; } - - /// - /// Cost breakdown by category. - /// - public CostBreakdown Costs { get; set; } = new(); - - /// - /// When the session started. - /// - public DateTime StartedAt { get; set; } - - /// - /// When the session ended (null if still active). - /// - public DateTime? EndedAt { get; set; } - } - - /// - /// Cost breakdown for real-time usage. - /// - public class CostBreakdown - { - /// - /// Cost for input audio processing. - /// - public decimal InputAudioCost { get; set; } - - /// - /// Cost for output audio generation. - /// - public decimal OutputAudioCost { get; set; } - - /// - /// Cost for text token processing. - /// - public decimal TokenCost { get; set; } - - /// - /// Cost for function calling. - /// - public decimal FunctionCallCost { get; set; } - - /// - /// Any additional fees (connection time, etc.). - /// - public decimal AdditionalFees { get; set; } - - /// - /// Total cost. - /// - public decimal Total => InputAudioCost + OutputAudioCost + TokenCost + FunctionCallCost + AdditionalFees; - } -} diff --git a/ConduitLLM.Core/Interfaces/IRouterConfigRepository.cs b/ConduitLLM.Core/Interfaces/IRouterConfigRepository.cs deleted file mode 100644 index d3640d6be..000000000 --- a/ConduitLLM.Core/Interfaces/IRouterConfigRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for a repository that stores router configuration - /// - public interface IRouterConfigRepository - { - /// - /// Gets the router configuration - /// - /// Cancellation token - /// Router configuration or null if not found - Task GetRouterConfigAsync(CancellationToken cancellationToken = default); - - /// - /// Saves the router configuration - /// - /// Router configuration to save - /// Cancellation token - Task SaveRouterConfigAsync(RouterConfig config, CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Interfaces/IRouterService.cs b/ConduitLLM.Core/Interfaces/IRouterService.cs deleted file mode 100644 index 2dbec3e00..000000000 --- a/ConduitLLM.Core/Interfaces/IRouterService.cs +++ /dev/null @@ -1,56 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Interface for a service that manages LLM router configuration - /// - public interface ILLMRouterService - { - /// - /// Initializes the router with the latest configuration - /// - Task InitializeRouterAsync(CancellationToken cancellationToken = default); - - /// - /// Gets the current router configuration - /// - Task GetRouterConfigAsync(CancellationToken cancellationToken = default); - - /// - /// Updates the router configuration - /// - Task UpdateRouterConfigAsync(RouterConfig config, CancellationToken cancellationToken = default); - - /// - /// Adds a model deployment to the router - /// - Task AddModelDeploymentAsync(ModelDeployment deployment, CancellationToken cancellationToken = default); - - /// - /// Updates an existing model deployment - /// - Task UpdateModelDeploymentAsync(ModelDeployment deployment, CancellationToken cancellationToken = default); - - /// - /// Removes a model deployment from the router - /// - Task RemoveModelDeploymentAsync(string deploymentName, CancellationToken cancellationToken = default); - - /// - /// Gets all available model deployments - /// - Task> GetModelDeploymentsAsync(CancellationToken cancellationToken = default); - - /// - /// Sets fallback models for a primary model - /// - Task SetFallbackModelsAsync(string primaryModel, List fallbacks, CancellationToken cancellationToken = default); - - /// - /// Gets fallback models for a primary model - /// - Task> GetFallbackModelsAsync(string primaryModel, CancellationToken cancellationToken = default); - - } -} diff --git a/ConduitLLM.Core/Interfaces/ITextToSpeechClient.cs b/ConduitLLM.Core/Interfaces/ITextToSpeechClient.cs deleted file mode 100644 index 80eab540a..000000000 --- a/ConduitLLM.Core/Interfaces/ITextToSpeechClient.cs +++ /dev/null @@ -1,148 +0,0 @@ -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Interfaces -{ - /// - /// Defines the contract for text-to-speech (TTS) synthesis capabilities. - /// - /// - /// - /// The ITextToSpeechClient interface provides a standardized way to convert text - /// into spoken audio across different provider implementations. This includes - /// support for multiple voices, languages, audio formats, and speech parameters. - /// - /// - /// Implementations of this interface handle provider-specific details such as: - /// - /// - /// Voice selection and customization - /// Speech rate, pitch, and volume control - /// Audio format encoding (MP3, WAV, etc.) - /// SSML (Speech Synthesis Markup Language) support - /// Streaming audio generation for long texts - /// - /// - public interface ITextToSpeechClient - { - /// - /// Converts text into speech audio. - /// - /// The text-to-speech request containing the text and synthesis parameters. - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// The speech synthesis response containing the audio data. - /// Thrown when the request fails validation. - /// Thrown when there is an error communicating with the provider. - /// Thrown when the provider does not support text-to-speech. - /// - /// - /// This method synthesizes speech from the provided text using the specified voice - /// and audio parameters. The response contains the complete audio data. - /// - /// - /// For long texts or real-time applications, consider using the streaming version - /// which provides audio chunks as they are generated. - /// - /// - Task CreateSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Converts text into speech audio with streaming output. - /// - /// The text-to-speech request containing the text and synthesis parameters. - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// An async enumerable of audio chunks as they are generated. - /// Thrown when the request fails validation. - /// Thrown when there is an error communicating with the provider. - /// Thrown when the provider does not support streaming text-to-speech. - /// - /// - /// This method is similar to but returns audio - /// data incrementally as it is generated. This enables: - /// - /// - /// Lower latency for first audio output - /// Progressive playback while generation continues - /// Memory-efficient processing of long texts - /// Real-time audio streaming applications - /// - /// - /// Not all providers support streaming. Implementations should throw a - /// if streaming is not available. - /// - /// - IAsyncEnumerable StreamSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Lists the voices available from this text-to-speech provider. - /// - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// A list of available voices with their metadata. - /// Thrown when there is an error communicating with the provider. - /// - /// - /// Returns detailed information about each available voice, including: - /// - /// - /// Voice ID for use in synthesis requests - /// Display name and description - /// Supported languages and accents - /// Gender and age characteristics - /// Voice style capabilities (e.g., emotional range) - /// - /// - /// Some providers may offer voice cloning or custom voices, which may appear - /// in this list if the API key has appropriate permissions. - /// - /// - Task> ListVoicesAsync( - string? apiKey = null, - CancellationToken cancellationToken = default); - - /// - /// Gets the audio formats supported by this text-to-speech client. - /// - /// A token to cancel the operation. - /// A list of supported audio format identifiers. - /// - /// - /// Returns the audio output formats that this provider can generate, such as: - /// mp3, wav, flac, ogg, aac, opus, etc. - /// - /// - /// Different formats may have different quality, compression, and compatibility - /// characteristics. Choose based on your application's requirements. - /// - /// - Task> GetSupportedFormatsAsync( - CancellationToken cancellationToken = default); - - /// - /// Checks if the client supports text-to-speech synthesis. - /// - /// Optional API key override to use instead of the client's configured key. - /// A token to cancel the operation. - /// True if text-to-speech is supported, false otherwise. - /// - /// - /// This method allows callers to check TTS support before attempting - /// to use the service. This is useful for graceful degradation and routing decisions. - /// - /// - /// Support may vary based on the API key permissions or the specific model/deployment - /// configured for the client instance. - /// - /// - Task SupportsTextToSpeechAsync( - string? apiKey = null, - CancellationToken cancellationToken = default); - } -} diff --git a/ConduitLLM.Core/Models/Audio/AudioProviderCapabilities.cs b/ConduitLLM.Core/Models/Audio/AudioProviderCapabilities.cs deleted file mode 100644 index b7ca69119..000000000 --- a/ConduitLLM.Core/Models/Audio/AudioProviderCapabilities.cs +++ /dev/null @@ -1,352 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Comprehensive capability information for an audio provider. - /// - public class AudioProviderCapabilities - { - /// - /// The provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Provider display name. - /// - public string DisplayName { get; set; } = string.Empty; - - /// - /// Transcription capabilities. - /// - public TranscriptionCapabilities? Transcription { get; set; } - - /// - /// Text-to-speech capabilities. - /// - public TextToSpeechCapabilities? TextToSpeech { get; set; } - - /// - /// Real-time conversation capabilities. - /// - public RealtimeCapabilities? Realtime { get; set; } - - /// - /// General audio capabilities. - /// - public List SupportedCapabilities { get; set; } = new(); - - /// - /// Provider-specific limitations. - /// - public AudioLimitations? Limitations { get; set; } - - /// - /// Cost information for audio operations. - /// - public AudioCostInfo? CostInfo { get; set; } - - /// - /// Quality ratings for different operations. - /// - public QualityRatings? Quality { get; set; } - - /// - /// Regional availability. - /// - public List? AvailableRegions { get; set; } - - /// - /// Provider-specific features. - /// - public Dictionary? CustomFeatures { get; set; } - } - - /// - /// Transcription-specific capabilities. - /// - public class TranscriptionCapabilities - { - /// - /// Supported audio input formats. - /// - public List SupportedFormats { get; set; } = new(); - - /// - /// Supported languages for transcription. - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Available transcription models. - /// - public List Models { get; set; } = new(); - - /// - /// Whether automatic language detection is supported. - /// - public bool SupportsAutoLanguageDetection { get; set; } - - /// - /// Whether word-level timestamps are supported. - /// - public bool SupportsWordTimestamps { get; set; } - - /// - /// Whether speaker diarization is supported. - /// - public bool SupportsSpeakerDiarization { get; set; } - - /// - /// Whether punctuation can be controlled. - /// - public bool SupportsPunctuationControl { get; set; } - - /// - /// Whether profanity filtering is available. - /// - public bool SupportsProfanityFilter { get; set; } - - /// - /// Maximum audio file size in bytes. - /// - public long? MaxFileSizeBytes { get; set; } - - /// - /// Maximum audio duration in seconds. - /// - public int? MaxDurationSeconds { get; set; } - - /// - /// Supported output formats. - /// - public List OutputFormats { get; set; } = new(); - } - - /// - /// Text-to-speech specific capabilities. - /// - public class TextToSpeechCapabilities - { - /// - /// Available voices. - /// - public List Voices { get; set; } = new(); - - /// - /// Supported output audio formats. - /// - public List SupportedFormats { get; set; } = new(); - - /// - /// Available TTS models. - /// - public List Models { get; set; } = new(); - - /// - /// Supported languages. - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Whether SSML is supported. - /// - public bool SupportsSSML { get; set; } - - /// - /// Whether streaming output is supported. - /// - public bool SupportsStreaming { get; set; } - - /// - /// Whether voice cloning is available. - /// - public bool SupportsVoiceCloning { get; set; } - - /// - /// Speed adjustment range. - /// - public RangeLimit? SpeedRange { get; set; } - - /// - /// Pitch adjustment range. - /// - public RangeLimit? PitchRange { get; set; } - - /// - /// Maximum input text length. - /// - public int? MaxTextLength { get; set; } - - /// - /// Available voice styles. - /// - public List? VoiceStyles { get; set; } - } - - /// - /// Information about an audio model. - /// - public class AudioModelInfo - { - /// - /// Model identifier. - /// - public string ModelId { get; set; } = string.Empty; - - /// - /// Model display name. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Model description. - /// - public string? Description { get; set; } - - /// - /// Model version. - /// - public string? Version { get; set; } - - /// - /// Whether this is the default model. - /// - public bool IsDefault { get; set; } - - /// - /// Model-specific capabilities. - /// - public Dictionary? Capabilities { get; set; } - } - - /// - /// Audio operation limitations. - /// - public class AudioLimitations - { - /// - /// Rate limits per minute. - /// - public Dictionary? RateLimitsPerMinute { get; set; } - - /// - /// Concurrent request limits. - /// - public Dictionary? ConcurrentLimits { get; set; } - - /// - /// Daily quota limits. - /// - public Dictionary? DailyQuotas { get; set; } - - /// - /// File size limitations. - /// - public Dictionary? FileSizeLimits { get; set; } - - /// - /// Duration limitations in seconds. - /// - public Dictionary? DurationLimits { get; set; } - - /// - /// API-specific restrictions. - /// - public List? Restrictions { get; set; } - } - - /// - /// Cost information for audio operations. - /// - public class AudioCostInfo - { - /// - /// Cost per minute of transcription. - /// - public decimal? TranscriptionPerMinute { get; set; } - - /// - /// Cost per 1K characters for TTS. - /// - public decimal? TextToSpeechPer1KChars { get; set; } - - /// - /// Cost per minute for real-time conversation. - /// - public decimal? RealtimePerMinute { get; set; } - - /// - /// Currency for the costs (e.g., "USD"). - /// - public string Currency { get; set; } = "USD"; - - /// - /// Model-specific pricing. - /// - public Dictionary? ModelPricing { get; set; } - - /// - /// Additional cost factors. - /// - public Dictionary? AdditionalCosts { get; set; } - } - - /// - /// Quality ratings for audio operations. - /// - public class QualityRatings - { - /// - /// Transcription accuracy rating (0-100). - /// - public int? TranscriptionAccuracy { get; set; } - - /// - /// TTS naturalness rating (0-100). - /// - public int? TTSNaturalness { get; set; } - - /// - /// Real-time latency rating (0-100, lower is better). - /// - public int? RealtimeLatency { get; set; } - - /// - /// Overall reliability rating (0-100). - /// - public int? Reliability { get; set; } - - /// - /// Language coverage rating (0-100). - /// - public int? LanguageCoverage { get; set; } - } - - /// - /// Represents a numeric range limit. - /// - public class RangeLimit - { - /// - /// Minimum value. - /// - public double Min { get; set; } - - /// - /// Maximum value. - /// - public double Max { get; set; } - - /// - /// Default value. - /// - public double Default { get; set; } - - /// - /// Step increment. - /// - public double? Step { get; set; } - } -} diff --git a/ConduitLLM.Core/Models/Audio/AudioTranscriptionRequest.cs b/ConduitLLM.Core/Models/Audio/AudioTranscriptionRequest.cs deleted file mode 100644 index caf795709..000000000 --- a/ConduitLLM.Core/Models/Audio/AudioTranscriptionRequest.cs +++ /dev/null @@ -1,246 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Represents a request to transcribe audio content into text. - /// - /// - /// This model supports various audio input methods and transcription options - /// that are common across different STT providers. - /// - public class AudioTranscriptionRequest : AudioRequestBase - { - /// - /// The audio data to transcribe, provided as raw bytes. - /// - /// - /// Either AudioData or AudioUrl must be provided, but not both. - /// The audio format should match one of the provider's supported formats. - /// - public byte[]? AudioData { get; set; } - - /// - /// URL pointing to the audio file to transcribe. - /// - /// - /// Some providers support direct URL access to audio files. - /// Either AudioData or AudioUrl must be provided, but not both. - /// - [Url] - public string? AudioUrl { get; set; } - - /// - /// The audio file name, used to infer format if not explicitly specified. - /// - /// - /// Optional but recommended when providing AudioData to help - /// providers determine the audio format from the file extension. - /// - public string? FileName { get; set; } - - /// - /// The format of the audio data. - /// - /// - /// If not specified, the format may be inferred from the FileName extension. - /// - public AudioFormat? AudioFormat { get; set; } - - /// - /// The model to use for transcription (e.g., "whisper-1"). - /// - /// - /// If not specified, the provider's default transcription model will be used. - /// - public string? Model { get; set; } - - /// - /// The language of the audio in ISO-639-1 format (e.g., "en", "es", "fr"). - /// - /// - /// Optional. If not specified, the provider will attempt to auto-detect - /// the language. Specifying the language can improve accuracy. - /// - [RegularExpression(@"^[a-z]{2}(-[A-Z]{2})?$", ErrorMessage = "Language must be in ISO-639-1 format")] - public string? Language { get; set; } - - /// - /// Optional prompt to guide the transcription style or provide context. - /// - /// - /// Some providers support prompts to improve transcription accuracy - /// or maintain consistent formatting/spelling of specific terms. - /// - [MaxLength(500)] - public string? Prompt { get; set; } - - /// - /// The sampling temperature for transcription (0-1). - /// - /// - /// Lower values make the transcription more deterministic, - /// higher values allow more variation. Default is provider-specific. - /// - [Range(0.0, 1.0)] - public double? Temperature { get; set; } - - /// - /// The desired output format for the transcription. - /// - /// - /// Common formats include "json", "text", "srt", "vtt". - /// Default is typically "json" with full metadata. - /// - public TranscriptionFormat? ResponseFormat { get; set; } - - /// - /// The minimum quality score required for provider selection. - /// - /// - /// Used by quality-based routing strategies. Range: 0-100. - /// Higher values may limit provider options but ensure better quality. - /// - [Range(0, 100)] - public double? RequiredQuality { get; set; } - - /// - /// Whether to enable streaming for the transcription. - /// - /// - /// When true, the transcription service will stream partial results - /// as they become available. Not all providers support streaming. - /// - public bool EnableStreaming { get; set; } = false; - - /// - /// The level of timestamp detail to include in the response. - /// - /// - /// Controls whether to include word-level or segment-level timestamps, - /// if supported by the provider. - /// - public TimestampGranularity? TimestampGranularity { get; set; } - - /// - /// Whether to include punctuation in the transcription. - /// - /// - /// Some providers allow disabling punctuation for specific use cases. - /// Default is true (include punctuation). - /// - public bool? IncludePunctuation { get; set; } = true; - - /// - /// Whether to filter profanity in the transcription. - /// - /// - /// When enabled, profane words may be censored or removed. - /// Support varies by provider. - /// - public bool? FilterProfanity { get; set; } - - /// - /// Validates that the request has required data. - /// - public override bool IsValid(out string? errorMessage) - { - errorMessage = null; - - if (AudioData == null && string.IsNullOrWhiteSpace(AudioUrl)) - { - errorMessage = "Either AudioData or AudioUrl must be provided"; - return false; - } - - if (AudioData != null && !string.IsNullOrWhiteSpace(AudioUrl)) - { - errorMessage = "Only one of AudioData or AudioUrl should be provided"; - return false; - } - - if (AudioData?.Length == 0) - { - errorMessage = "AudioData cannot be empty"; - return false; - } - - return true; - } - } - - /// - /// Base class for audio-related requests. - /// - public abstract class AudioRequestBase - { - /// - /// Optional user identifier for tracking and billing purposes. - /// - public string? User { get; set; } - - /// - /// Provider-specific options that don't fit the standard model. - /// - public Dictionary? ProviderOptions { get; set; } - - /// - /// Validates that the request contains valid data. - /// - /// Error message if validation fails. - /// True if valid, false otherwise. - public abstract bool IsValid(out string? errorMessage); - } - - /// - /// Supported transcription output formats. - /// - public enum TranscriptionFormat - { - /// - /// JSON format with full metadata. - /// - Json, - - /// - /// Plain text without metadata. - /// - Text, - - /// - /// SubRip subtitle format. - /// - Srt, - - /// - /// WebVTT subtitle format. - /// - Vtt, - - /// - /// Verbose JSON with additional details. - /// - VerboseJson - } - - /// - /// Granularity of timestamps in transcription. - /// - public enum TimestampGranularity - { - /// - /// No timestamps. - /// - None, - - /// - /// Timestamps at segment/sentence level. - /// - Segment, - - /// - /// Timestamps for each word. - /// - Word - } -} diff --git a/ConduitLLM.Core/Models/Audio/AudioTranscriptionResponse.cs b/ConduitLLM.Core/Models/Audio/AudioTranscriptionResponse.cs deleted file mode 100644 index c39d0fa57..000000000 --- a/ConduitLLM.Core/Models/Audio/AudioTranscriptionResponse.cs +++ /dev/null @@ -1,198 +0,0 @@ -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Represents the response from an audio transcription request. - /// - public class AudioTranscriptionResponse - { - /// - /// The primary transcribed text. - /// - /// - /// This contains the full transcription of the audio input, - /// formatted according to the requested output format. - /// - public string Text { get; set; } = string.Empty; - - /// - /// The detected or specified language of the audio. - /// - /// - /// ISO-639-1 language code (e.g., "en", "es", "fr"). - /// May be auto-detected if not specified in the request. - /// - public string? Language { get; set; } - - /// - /// The duration of the audio in seconds. - /// - /// - /// Total length of the processed audio file. - /// - public double? Duration { get; set; } - - /// - /// Segments of the transcription with timestamps. - /// - /// - /// Available when segment-level timestamps are requested. - /// Each segment typically represents a sentence or phrase. - /// - public List? Segments { get; set; } - - /// - /// Individual words with timestamps. - /// - /// - /// Available when word-level timestamps are requested. - /// Provides fine-grained timing information. - /// - public List? Words { get; set; } - - /// - /// Alternative transcriptions with confidence scores. - /// - /// - /// Some providers return multiple possible transcriptions - /// ranked by confidence. The primary transcription is in Text. - /// - public List? Alternatives { get; set; } - - /// - /// Overall confidence score for the transcription (0-1). - /// - /// - /// Indicates the provider's confidence in the accuracy - /// of the transcription. Higher values indicate higher confidence. - /// - public double? Confidence { get; set; } - - /// - /// Provider-specific metadata or additional information. - /// - public Dictionary? Metadata { get; set; } - - /// - /// The model used for transcription. - /// - /// - /// Indicates which STT model was actually used, - /// which may differ from the requested model. - /// - public string? Model { get; set; } - - /// - /// Usage information for billing purposes. - /// - public AudioUsage? Usage { get; set; } - } - - /// - /// Represents a segment of transcribed text with timing information. - /// - public class TranscriptionSegment - { - /// - /// The segment identifier. - /// - public int Id { get; set; } - - /// - /// Start time of the segment in seconds. - /// - public double Start { get; set; } - - /// - /// End time of the segment in seconds. - /// - public double End { get; set; } - - /// - /// The transcribed text for this segment. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Confidence score for this segment (0-1). - /// - public double? Confidence { get; set; } - - /// - /// Speaker identifier if speaker diarization is enabled. - /// - public string? Speaker { get; set; } - } - - /// - /// Represents a single transcribed word with timing information. - /// - public class TranscriptionWord - { - /// - /// The transcribed word. - /// - public string Word { get; set; } = string.Empty; - - /// - /// Start time of the word in seconds. - /// - public double Start { get; set; } - - /// - /// End time of the word in seconds. - /// - public double End { get; set; } - - /// - /// Confidence score for this word (0-1). - /// - public double? Confidence { get; set; } - - /// - /// Speaker identifier if speaker diarization is enabled. - /// - public string? Speaker { get; set; } - } - - /// - /// Represents an alternative transcription with confidence score. - /// - public class TranscriptionAlternative - { - /// - /// The alternative transcription text. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Confidence score for this alternative (0-1). - /// - public double Confidence { get; set; } - - /// - /// Segments for this alternative, if available. - /// - public List? Segments { get; set; } - } - - /// - /// Usage information for audio operations. - /// - public class AudioUsage - { - /// - /// Duration of audio processed in seconds. - /// - public double AudioSeconds { get; set; } - - /// - /// Number of characters in the transcription. - /// - public int? CharacterCount { get; set; } - - /// - /// Provider-specific usage metrics. - /// - public Dictionary? AdditionalMetrics { get; set; } - } -} diff --git a/ConduitLLM.Core/Models/Audio/ContentFilterResult.cs b/ConduitLLM.Core/Models/Audio/ContentFilterResult.cs deleted file mode 100644 index 6929bf4f2..000000000 --- a/ConduitLLM.Core/Models/Audio/ContentFilterResult.cs +++ /dev/null @@ -1,100 +0,0 @@ -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Result of content filtering operation. - /// - public class ContentFilterResult - { - /// - /// Gets or sets whether the content passed all filters. - /// - public bool IsApproved { get; set; } - - /// - /// Gets or sets the filtered/cleaned text. - /// - public string FilteredText { get; set; } = string.Empty; - - /// - /// Gets or sets the categories of issues found. - /// - public List ViolationCategories { get; set; } = new(); - - /// - /// Gets or sets the confidence score (0-1). - /// - public double ConfidenceScore { get; set; } - - /// - /// Gets or sets whether content was modified. - /// - public bool WasModified { get; set; } - - /// - /// Gets or sets detailed reasons for filtering. - /// - public List Details { get; set; } = new(); - } - - /// - /// Detailed information about filtered content. - /// - public class ContentFilterDetail - { - /// - /// Gets or sets the type of content filtered. - /// - public string Type { get; set; } = string.Empty; - - /// - /// Gets or sets the severity level. - /// - public FilterSeverity Severity { get; set; } - - /// - /// Gets or sets the original text segment. - /// - public string OriginalText { get; set; } = string.Empty; - - /// - /// Gets or sets the replacement text. - /// - public string ReplacementText { get; set; } = string.Empty; - - /// - /// Gets or sets the start position in text. - /// - public int StartIndex { get; set; } - - /// - /// Gets or sets the end position in text. - /// - public int EndIndex { get; set; } - } - - /// - /// Severity levels for content filtering. - /// - public enum FilterSeverity - { - /// - /// Low severity - minor issues. - /// - Low, - - /// - /// Medium severity - moderate issues. - /// - Medium, - - /// - /// High severity - serious issues. - /// - High, - - /// - /// Critical severity - must be blocked. - /// - Critical - } -} diff --git a/ConduitLLM.Core/Models/Audio/HybridAudioModels.cs b/ConduitLLM.Core/Models/Audio/HybridAudioModels.cs deleted file mode 100644 index 796b748b7..000000000 --- a/ConduitLLM.Core/Models/Audio/HybridAudioModels.cs +++ /dev/null @@ -1,461 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Request for processing audio through the hybrid STT-LLM-TTS pipeline. - /// - public class HybridAudioRequest - { - /// - /// Gets or sets the session ID for maintaining conversation context. - /// - /// - /// The unique identifier of the conversation session. If null, a single-turn interaction is performed. - /// - public string? SessionId { get; set; } - - /// - /// Gets or sets the audio data to be processed. - /// - /// - /// The raw audio bytes in a supported format (e.g., MP3, WAV, WebM). - /// - [Required] - public byte[] AudioData { get; set; } = Array.Empty(); - - /// - /// Gets or sets the format of the input audio. - /// - /// - /// The audio format identifier (e.g., "mp3", "wav", "webm"). - /// - [Required] - public string AudioFormat { get; set; } = "mp3"; - - /// - /// Gets or sets the language of the input audio. - /// - /// - /// ISO 639-1 language code (e.g., "en", "es", "fr"). If null, automatic detection is used. - /// - public string? Language { get; set; } - - /// - /// Gets or sets the system prompt for the LLM. - /// - /// - /// Instructions that define the assistant's behavior and personality. - /// - public string? SystemPrompt { get; set; } - - /// - /// Gets or sets the preferred voice for TTS output. - /// - /// - /// The voice identifier. If null, the default voice is used. - /// - public string? VoiceId { get; set; } - - /// - /// Gets or sets the desired output audio format. - /// - /// - /// The audio format for the response (e.g., "mp3", "wav"). Defaults to "mp3". - /// - public string OutputFormat { get; set; } = "mp3"; - - /// - /// Gets or sets the temperature for LLM response generation. - /// - /// - /// Controls randomness in responses. Range: 0.0 to 2.0. Default: 0.7. - /// - [Range(0.0, 2.0)] - public double Temperature { get; set; } = 0.7; - - /// - /// Gets or sets the maximum tokens for the LLM response. - /// - /// - /// Limits the length of the generated response. Default: 150. - /// - [Range(1, 4096)] - public int MaxTokens { get; set; } = 150; - - /// - /// Gets or sets whether to enable streaming mode. - /// - /// - /// If true, responses are streamed for lower latency. Default: false. - /// - public bool EnableStreaming { get; set; } = false; - - /// - /// Gets or sets custom metadata for the request. - /// - /// - /// Additional key-value pairs for tracking or customization. - /// - public Dictionary? Metadata { get; set; } - - /// - /// Gets or sets the virtual key for authentication and routing. - /// - /// - /// The virtual key used to authenticate and route audio requests. - /// - public string? VirtualKey { get; set; } - } - - /// - /// Response from the hybrid audio processing pipeline. - /// - public class HybridAudioResponse - { - /// - /// Gets or sets the generated audio data. - /// - /// - /// The synthesized speech audio in the requested format. - /// - public byte[] AudioData { get; set; } = Array.Empty(); - - /// - /// Gets or sets the format of the output audio. - /// - /// - /// The audio format identifier (e.g., "mp3", "wav"). - /// - public string AudioFormat { get; set; } = "mp3"; - - /// - /// Gets or sets the transcribed text from the input audio. - /// - /// - /// The text representation of the user's speech input. - /// - public string TranscribedText { get; set; } = string.Empty; - - /// - /// Gets or sets the LLM-generated response text. - /// - /// - /// The text response before TTS conversion. - /// - public string ResponseText { get; set; } = string.Empty; - - /// - /// Gets or sets the detected language of the input. - /// - /// - /// ISO 639-1 language code of the detected language. - /// - public string? DetectedLanguage { get; set; } - - /// - /// Gets or sets the voice used for synthesis. - /// - /// - /// The identifier of the voice used for TTS. - /// - public string? VoiceUsed { get; set; } - - /// - /// Gets or sets the duration of the output audio. - /// - /// - /// The length of the generated audio in seconds. - /// - public double DurationSeconds { get; set; } - - /// - /// Gets or sets the processing metrics. - /// - /// - /// Timing information for each pipeline stage. - /// - public ProcessingMetrics? Metrics { get; set; } - - /// - /// Gets or sets the session ID if part of a conversation. - /// - /// - /// The conversation session identifier. - /// - public string? SessionId { get; set; } - - /// - /// Gets or sets custom metadata from the response. - /// - /// - /// Additional key-value pairs from processing. - /// - public Dictionary? Metadata { get; set; } - } - - /// - /// Represents a chunk of audio data in streaming responses. - /// - public class HybridAudioChunk - { - /// - /// Gets or sets the chunk type. - /// - /// - /// The type of data in this chunk (e.g., "transcription", "text", "audio"). - /// - public string ChunkType { get; set; } = "audio"; - - /// - /// Gets or sets the audio data chunk. - /// - /// - /// Partial audio data, if this is an audio chunk. - /// - public byte[]? AudioData { get; set; } - - /// - /// Gets or sets the text content. - /// - /// - /// Text data for transcription or response chunks. - /// - public string? TextContent { get; set; } - - /// - /// Gets or sets whether this is the final chunk. - /// - /// - /// True if this is the last chunk in the stream. - /// - public bool IsFinal { get; set; } - - /// - /// Gets or sets the sequence number. - /// - /// - /// The order of this chunk in the stream. - /// - public int SequenceNumber { get; set; } - - /// - /// Gets or sets the timestamp of this chunk. - /// - /// - /// When this chunk was generated. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - } - - /// - /// Configuration for a hybrid audio conversation session. - /// - public class HybridSessionConfig - { - /// - /// Gets or sets the STT provider to use. - /// - /// - /// The identifier of the speech-to-text provider. - /// - public string? SttProvider { get; set; } - - /// - /// Gets or sets the LLM model to use. - /// - /// - /// The model identifier for text generation. - /// - public string? LlmModel { get; set; } - - /// - /// Gets or sets the TTS provider to use. - /// - /// - /// The identifier of the text-to-speech provider. - /// - public string? TtsProvider { get; set; } - - /// - /// Gets or sets the system prompt for the conversation. - /// - /// - /// Instructions that persist across the entire conversation. - /// - public string? SystemPrompt { get; set; } - - /// - /// Gets or sets the default voice for responses. - /// - /// - /// The voice identifier for TTS synthesis. - /// - public string? DefaultVoice { get; set; } - - /// - /// Gets or sets the conversation history limit. - /// - /// - /// Maximum number of turns to keep in context. Default: 10. - /// - [Range(1, 100)] - public int MaxHistoryTurns { get; set; } = 10; - - /// - /// Gets or sets the session timeout. - /// - /// - /// Duration of inactivity before session expires. Default: 30 minutes. - /// - public TimeSpan SessionTimeout { get; set; } = TimeSpan.FromMinutes(30); - - /// - /// Gets or sets whether to enable latency optimization. - /// - /// - /// If true, applies various optimizations to reduce latency. - /// - public bool EnableLatencyOptimization { get; set; } = true; - - /// - /// Gets or sets custom session metadata. - /// - /// - /// Additional configuration parameters. - /// - public Dictionary? Metadata { get; set; } - } - - /// - /// Metrics for processing stages in the hybrid pipeline. - /// - public class ProcessingMetrics - { - /// - /// Gets or sets the STT processing time. - /// - /// - /// Time taken for speech-to-text conversion in milliseconds. - /// - public double SttLatencyMs { get; set; } - - /// - /// Gets or sets the LLM processing time. - /// - /// - /// Time taken for response generation in milliseconds. - /// - public double LlmLatencyMs { get; set; } - - /// - /// Gets or sets the TTS processing time. - /// - /// - /// Time taken for text-to-speech conversion in milliseconds. - /// - public double TtsLatencyMs { get; set; } - - /// - /// Gets or sets the total processing time. - /// - /// - /// End-to-end latency in milliseconds. - /// - public double TotalLatencyMs { get; set; } - - /// - /// Gets or sets the input audio duration. - /// - /// - /// Length of the input audio in seconds. - /// - public double InputDurationSeconds { get; set; } - - /// - /// Gets or sets the output audio duration. - /// - /// - /// Length of the generated audio in seconds. - /// - public double OutputDurationSeconds { get; set; } - - /// - /// Gets or sets the tokens used in LLM processing. - /// - /// - /// Token count for the LLM request and response. - /// - public int TokensUsed { get; set; } - } - - /// - /// Latency metrics for the hybrid audio pipeline. - /// - public class HybridLatencyMetrics - { - /// - /// Gets or sets the average STT latency. - /// - /// - /// Average time for speech-to-text across recent requests. - /// - public double AverageSttLatencyMs { get; set; } - - /// - /// Gets or sets the average LLM latency. - /// - /// - /// Average time for LLM response generation. - /// - public double AverageLlmLatencyMs { get; set; } - - /// - /// Gets or sets the average TTS latency. - /// - /// - /// Average time for text-to-speech synthesis. - /// - public double AverageTtsLatencyMs { get; set; } - - /// - /// Gets or sets the average total latency. - /// - /// - /// Average end-to-end processing time. - /// - public double AverageTotalLatencyMs { get; set; } - - /// - /// Gets or sets the 95th percentile latency. - /// - /// - /// P95 latency for the complete pipeline. - /// - public double P95LatencyMs { get; set; } - - /// - /// Gets or sets the 99th percentile latency. - /// - /// - /// P99 latency for the complete pipeline. - /// - public double P99LatencyMs { get; set; } - - /// - /// Gets or sets the sample count. - /// - /// - /// Number of requests used to calculate these metrics. - /// - public int SampleCount { get; set; } - - /// - /// Gets or sets when these metrics were calculated. - /// - /// - /// The timestamp of metric calculation. - /// - public DateTime CalculatedAt { get; set; } = DateTime.UtcNow; - } -} diff --git a/ConduitLLM.Core/Models/Audio/RealtimeMessages.cs b/ConduitLLM.Core/Models/Audio/RealtimeMessages.cs deleted file mode 100644 index dff970eca..000000000 --- a/ConduitLLM.Core/Models/Audio/RealtimeMessages.cs +++ /dev/null @@ -1,552 +0,0 @@ -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Base class for all real-time audio messages. - /// - public abstract class RealtimeMessage - { - /// - /// The type of message. - /// - public abstract string Type { get; } - - /// - /// Session identifier this message belongs to. - /// - public string? SessionId { get; set; } - - /// - /// Timestamp when the message was created. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Sequence number for ordering messages. - /// - public long? SequenceNumber { get; set; } - } - - /// - /// Audio frame to be sent to the real-time service. - /// - public class RealtimeAudioFrame : RealtimeMessage - { - public override string Type => "audio.input"; - - /// - /// Raw audio data. - /// - public byte[] AudioData { get; set; } = Array.Empty(); - - /// - /// Sample rate of the audio data. - /// - public int SampleRate { get; set; } - - /// - /// Number of channels (1 = mono, 2 = stereo). - /// - public int Channels { get; set; } = 1; - - /// - /// Whether this audio is output from the AI (vs input from user). - /// - public bool IsOutput { get; set; } - - /// - /// Duration of this audio frame in milliseconds. - /// - public double DurationMs { get; set; } - - /// - /// Tool response data if this frame contains a function result. - /// - public ToolResponse? ToolResponse { get; set; } - } - - /// - /// Response message from the real-time service. - /// - public class RealtimeResponse : RealtimeMessage - { - public override string Type => EventType.ToString().ToLowerInvariant(); - - /// - /// The type of event in this response. - /// - public RealtimeEventType EventType { get; set; } - - /// - /// Audio output data if this is an audio event. - /// - public AudioDelta? Audio { get; set; } - - /// - /// Transcription data if this is a transcription event. - /// - public TranscriptionDelta? Transcription { get; set; } - - /// - /// Text response for non-audio responses. - /// - public string? TextResponse { get; set; } - - /// - /// Tool call information if this is a function call. - /// - public RealtimeToolCall? ToolCall { get; set; } - - /// - /// Turn event information. - /// - public TurnEvent? Turn { get; set; } - - /// - /// Error information if this is an error event. - /// - public ErrorInfo? Error { get; set; } - - /// - /// Session update confirmation. - /// - public SessionUpdateResult? SessionUpdate { get; set; } - - /// - /// Usage information for billing purposes. - /// - public RealtimeUsageInfo? Usage { get; set; } - } - - /// - /// Types of real-time events. - /// - public enum RealtimeEventType - { - /// - /// Audio output from the AI. - /// - AudioDelta, - - /// - /// Transcription update. - /// - TranscriptionDelta, - - /// - /// Complete text response. - /// - TextResponse, - - /// - /// Tool/function call request. - /// - ToolCallRequest, - - /// - /// Turn has started. - /// - TurnStarted, - - /// - /// Turn has ended. - /// - TurnEnded, - - /// - /// Session configuration updated. - /// - SessionUpdated, - - /// - /// Connection established. - /// - Connected, - - /// - /// Error occurred. - /// - Error, - - /// - /// Latency measurement. - /// - Ping, - - /// - /// User interrupted the AI. - /// - Interrupted, - - /// - /// Response has been completed. - /// - ResponseComplete - } - - /// - /// Delta audio data from the AI. - /// - public class AudioDelta - { - /// - /// The audio data chunk. - /// - public byte[] Data { get; set; } = Array.Empty(); - - /// - /// Whether this completes the current audio response. - /// - public bool IsComplete { get; set; } - - /// - /// Duration of this chunk in milliseconds. - /// - public double DurationMs { get; set; } - - /// - /// Item ID this audio belongs to. - /// - public string? ItemId { get; set; } - - /// - /// Content index for multi-part responses. - /// - public int? ContentIndex { get; set; } - } - - /// - /// Incremental transcription update. - /// - public class TranscriptionDelta - { - /// - /// The role of the speaker (user or assistant). - /// - public string Role { get; set; } = string.Empty; - - /// - /// The transcribed text delta. - /// - public string Text { get; set; } = string.Empty; - - /// - /// Whether this is a final transcription. - /// - public bool IsFinal { get; set; } - - /// - /// Start time of this segment. - /// - public double? StartTime { get; set; } - - /// - /// End time of this segment. - /// - public double? EndTime { get; set; } - - /// - /// Item ID this transcription belongs to. - /// - public string? ItemId { get; set; } - } - - /// - /// Real-time tool/function call. - /// - public class RealtimeToolCall - { - /// - /// Unique identifier for this tool call. - /// - public string CallId { get; set; } = string.Empty; - - /// - /// The function name to call. - /// - public string FunctionName { get; set; } = string.Empty; - - /// - /// JSON arguments for the function. - /// - public string Arguments { get; set; } = "{}"; - - /// - /// Type of tool (usually "function"). - /// - public string Type { get; set; } = "function"; - } - - /// - /// Response to a tool call. - /// - public class ToolResponse - { - /// - /// The tool call ID this is responding to. - /// - public string CallId { get; set; } = string.Empty; - - /// - /// The result of the tool call. - /// - public string Result { get; set; } = string.Empty; - - /// - /// Whether the tool call was successful. - /// - public bool Success { get; set; } = true; - - /// - /// Error message if the call failed. - /// - public string? Error { get; set; } - } - - /// - /// Turn event information. - /// - public class TurnEvent - { - /// - /// Type of turn event. - /// - public TurnEventType EventType { get; set; } - - /// - /// The role taking or ending the turn. - /// - public string Role { get; set; } = string.Empty; - - /// - /// Reason for turn end. - /// - public string? EndReason { get; set; } - - /// - /// Turn identifier. - /// - public string? TurnId { get; set; } - } - - /// - /// Types of turn events. - /// - public enum TurnEventType - { - /// - /// A turn has started. - /// - Started, - - /// - /// A turn has ended. - /// - Ended, - - /// - /// Turn was interrupted. - /// - Interrupted - } - - /// - /// Error information from real-time service. - /// - public class ErrorInfo - { - /// - /// Error code. - /// - public string Code { get; set; } = string.Empty; - - /// - /// Human-readable error message. - /// - public string Message { get; set; } = string.Empty; - - /// - /// Error severity level. - /// - public ErrorSeverity Severity { get; set; } = ErrorSeverity.Error; - - /// - /// Whether this error is recoverable. - /// - public bool Recoverable { get; set; } - - /// - /// Additional error details. - /// - public Dictionary? Details { get; set; } - } - - /// - /// Error severity levels. - /// - public enum ErrorSeverity - { - /// - /// Informational message. - /// - Info, - - /// - /// Warning that doesn't interrupt service. - /// - Warning, - - /// - /// Error that may affect functionality. - /// - Error, - - /// - /// Critical error requiring reconnection. - /// - Critical - } - - /// - /// Result of a session update operation. - /// - public class SessionUpdateResult - { - /// - /// Whether the update was successful. - /// - public bool Success { get; set; } - - /// - /// Updated fields. - /// - public List UpdatedFields { get; set; } = new(); - - /// - /// Fields that failed to update. - /// - public Dictionary? FailedFields { get; set; } - - /// - /// New effective configuration. - /// - public Dictionary? EffectiveConfig { get; set; } - } - - /// - /// Capabilities of a real-time audio provider. - /// - public class RealtimeCapabilities - { - /// - /// Supported input audio formats. - /// - public List SupportedInputFormats { get; set; } = new(); - - /// - /// Supported output audio formats. - /// - public List SupportedOutputFormats { get; set; } = new(); - - /// - /// Available voices. - /// - public List AvailableVoices { get; set; } = new(); - - /// - /// Supported languages. - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Turn detection options. - /// - public TurnDetectionCapabilities? TurnDetection { get; set; } - - /// - /// Whether function calling is supported. - /// - public bool SupportsFunctionCalling { get; set; } - - /// - /// Whether interruptions are supported. - /// - public bool SupportsInterruptions { get; set; } - - /// - /// Maximum session duration in seconds. - /// - public int? MaxSessionDurationSeconds { get; set; } - - /// - /// Maximum concurrent sessions. - /// - public int? MaxConcurrentSessions { get; set; } - - /// - /// Provider-specific capabilities. - /// - public Dictionary? ProviderCapabilities { get; set; } - } - - /// - /// Turn detection capability details. - /// - public class TurnDetectionCapabilities - { - /// - /// Supported turn detection types. - /// - public List SupportedTypes { get; set; } = new(); - - /// - /// Minimum silence threshold in ms. - /// - public int MinSilenceThresholdMs { get; set; } - - /// - /// Maximum silence threshold in ms. - /// - public int MaxSilenceThresholdMs { get; set; } - - /// - /// Whether custom VAD parameters are supported. - /// - public bool SupportsCustomParameters { get; set; } - } - - /// - /// Usage information for real-time sessions. - /// - public class RealtimeUsageInfo - { - /// - /// Total tokens used. - /// - public long? TotalTokens { get; set; } - - /// - /// Input tokens used. - /// - public long? InputTokens { get; set; } - - /// - /// Output tokens used. - /// - public long? OutputTokens { get; set; } - - /// - /// Input audio seconds. - /// - public double? InputAudioSeconds { get; set; } - - /// - /// Output audio seconds. - /// - public double? OutputAudioSeconds { get; set; } - - /// - /// Number of function calls made. - /// - public int? FunctionCalls { get; set; } - } -} diff --git a/ConduitLLM.Core/Models/Audio/RealtimeSession.cs b/ConduitLLM.Core/Models/Audio/RealtimeSession.cs deleted file mode 100644 index 86efd4fdf..000000000 --- a/ConduitLLM.Core/Models/Audio/RealtimeSession.cs +++ /dev/null @@ -1,278 +0,0 @@ -using System.Net.WebSockets; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Represents an active real-time audio conversation session. - /// - public class RealtimeSession : IDisposable - { - /// - /// Unique identifier for the session. - /// - public string Id { get; set; } = Guid.NewGuid().ToString(); - - /// - /// The provider hosting this session. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// The WebSocket connection for this session. - /// - /// - /// Internal use only. The actual WebSocket is managed by the client implementation. - /// - internal WebSocket? WebSocket { get; set; } - - /// - /// The configuration used to create this session. - /// - public RealtimeSessionConfig Config { get; set; } = new(); - - /// - /// When the session was created. - /// - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - - /// - /// Current state of the session. - /// - public SessionState State { get; set; } = SessionState.Connecting; - - /// - /// Session metadata from the provider. - /// - public Dictionary? Metadata { get; set; } - - /// - /// Connection information. - /// - public ConnectionInfo? Connection { get; set; } - - /// - /// Statistics for the current session. - /// - public SessionStatistics Statistics { get; set; } = new(); - - /// - /// Disposes of the session resources. - /// - public void Dispose() - { - Dispose(true); - GC.SuppressFinalize(this); - } - - /// - /// Disposes of the session resources. - /// - protected virtual void Dispose(bool disposing) - { - if (disposing) - { - if (WebSocket?.State == WebSocketState.Open) - { - try - { - WebSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Session disposed", - CancellationToken.None).Wait(TimeSpan.FromSeconds(5)); - } - catch - { - // Best effort cleanup - } - } - WebSocket?.Dispose(); - State = SessionState.Closed; - } - } - } - - /// - /// Session state enumeration. - /// - public enum SessionState - { - /// - /// Session is being established. - /// - Connecting, - - /// - /// Session is connected and ready. - /// - Connected, - - /// - /// Session is actively in a conversation. - /// - Active, - - /// - /// Session is temporarily disconnected. - /// - Disconnected, - - /// - /// Session is reconnecting. - /// - Reconnecting, - - /// - /// Session has been closed. - /// - Closed, - - /// - /// Session ended due to an error. - /// - Error - } - - /// - /// Connection information for a real-time session. - /// - public class ConnectionInfo - { - /// - /// The endpoint URL for the connection. - /// - public string? Endpoint { get; set; } - - /// - /// Connection protocol version. - /// - public string? ProtocolVersion { get; set; } - - /// - /// Measured latency in milliseconds. - /// - public double? LatencyMs { get; set; } - - /// - /// Connection quality indicator. - /// - public ConnectionQuality Quality { get; set; } = ConnectionQuality.Good; - } - - /// - /// Connection quality levels. - /// - public enum ConnectionQuality - { - /// - /// Excellent connection quality. - /// - Excellent, - - /// - /// Good connection quality. - /// - Good, - - /// - /// Fair connection quality. - /// - Fair, - - /// - /// Poor connection quality. - /// - Poor - } - - /// - /// Statistics for a real-time session. - /// - public class SessionStatistics - { - /// - /// Total duration of the session. - /// - public TimeSpan Duration { get; set; } - - /// - /// Total input audio duration. - /// - public TimeSpan InputAudioDuration { get; set; } - - /// - /// Total output audio duration. - /// - public TimeSpan OutputAudioDuration { get; set; } - - /// - /// Number of turns in the conversation. - /// - public int TurnCount { get; set; } - - /// - /// Number of interruptions. - /// - public int InterruptionCount { get; set; } - - /// - /// Number of function calls made. - /// - public int FunctionCallCount { get; set; } - - /// - /// Total input tokens (if available). - /// - public int? InputTokens { get; set; } - - /// - /// Total output tokens (if available). - /// - public int? OutputTokens { get; set; } - - /// - /// Number of errors encountered. - /// - public int ErrorCount { get; set; } - - /// - /// Average response latency in milliseconds. - /// - public double? AverageLatencyMs { get; set; } - } - - /// - /// Update configuration for an active session. - /// - public class RealtimeSessionUpdate - { - /// - /// Updated system prompt. - /// - public string? SystemPrompt { get; set; } - - /// - /// Updated voice settings. - /// - public RealtimeVoiceSettings? VoiceSettings { get; set; } - - /// - /// Updated turn detection settings. - /// - public TurnDetectionConfig? TurnDetection { get; set; } - - /// - /// Updated temperature. - /// - public double? Temperature { get; set; } - - /// - /// Updated tool definitions. - /// - public List? Tools { get; set; } - - /// - /// Provider-specific updates. - /// - public Dictionary? ProviderUpdates { get; set; } - } -} diff --git a/ConduitLLM.Core/Models/Audio/RealtimeSessionConfig.cs b/ConduitLLM.Core/Models/Audio/RealtimeSessionConfig.cs deleted file mode 100644 index c9598f459..000000000 --- a/ConduitLLM.Core/Models/Audio/RealtimeSessionConfig.cs +++ /dev/null @@ -1,315 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Configuration for establishing a real-time audio conversation session. - /// - public class RealtimeSessionConfig - { - /// - /// The model to use for the conversation (e.g., "gpt-4o-realtime-preview"). - /// - /// - /// Model availability varies by provider. Examples: - /// - OpenAI: "gpt-4o-realtime-preview" - /// - Ultravox: "ultravox-v1" - /// - ElevenLabs: agent ID or "default-agent" - /// - public string? Model { get; set; } - - /// - /// The voice to use for AI responses. - /// - /// - /// Voice IDs are provider-specific. Examples: - /// - OpenAI: "alloy", "echo", "shimmer" - /// - ElevenLabs: specific voice IDs - /// - Ultravox: voice names or IDs - /// - [Required] - public string Voice { get; set; } = string.Empty; - - /// - /// Audio format for input (user speech). - /// - public RealtimeAudioFormat InputFormat { get; set; } = RealtimeAudioFormat.PCM16_24kHz; - - /// - /// Audio format for output (AI speech). - /// - public RealtimeAudioFormat OutputFormat { get; set; } = RealtimeAudioFormat.PCM16_24kHz; - - /// - /// The language for the conversation (ISO-639-1). - /// - [RegularExpression(@"^[a-z]{2}(-[A-Z]{2})?$", ErrorMessage = "Language must be in ISO-639-1 format")] - public string? Language { get; set; } = "en"; - - /// - /// System prompt to set the AI's behavior and context. - /// - [MaxLength(2000)] - public string? SystemPrompt { get; set; } - - /// - /// Turn detection configuration. - /// - /// - /// Controls how the system detects when the user has finished speaking - /// and when to start the AI response. - /// - public TurnDetectionConfig TurnDetection { get; set; } = new(); - - /// - /// Function definitions for tool use during conversation. - /// - /// - /// Allows the AI to call functions during the conversation. - /// Not supported by all providers. - /// - public List? Tools { get; set; } - - /// - /// Transcription settings for the session. - /// - public TranscriptionConfig? Transcription { get; set; } - - /// - /// Voice customization settings. - /// - public RealtimeVoiceSettings? VoiceSettings { get; set; } - - /// - /// Temperature for response generation (0-2). - /// - [Range(0.0, 2.0)] - public double? Temperature { get; set; } - - /// - /// Maximum response duration in seconds. - /// - /// - /// Limits how long the AI can speak in a single turn. - /// - [Range(1, 300)] - public int? MaxResponseDurationSeconds { get; set; } - - /// - /// Conversation mode preset. - /// - public ConversationMode Mode { get; set; } = ConversationMode.Conversational; - - /// - /// Provider-specific configuration options. - /// - public Dictionary? ProviderConfig { get; set; } - } - - /// - /// Configuration for turn detection in real-time conversations. - /// - public class TurnDetectionConfig - { - /// - /// Whether turn detection is enabled. - /// - public bool Enabled { get; set; } = true; - - /// - /// Type of turn detection to use. - /// - public TurnDetectionType Type { get; set; } = TurnDetectionType.ServerVAD; - - /// - /// Silence duration in milliseconds before ending turn. - /// - /// - /// How long to wait after speech stops before considering - /// the turn complete. Typical range: 300-1000ms. - /// - [Range(100, 5000)] - public int? SilenceThresholdMs { get; set; } = 500; - - /// - /// Audio level threshold for voice activity detection. - /// - /// - /// Provider-specific. Often a value between 0-1 or in decibels. - /// - public double? Threshold { get; set; } - - /// - /// Padding to include before detected speech starts (ms). - /// - /// - /// Captures a bit of audio before speech is detected to avoid - /// cutting off the beginning of utterances. - /// - [Range(0, 1000)] - public int? PrefixPaddingMs { get; set; } = 300; - } - - /// - /// Configuration for transcription during real-time sessions. - /// - public class TranscriptionConfig - { - /// - /// Whether to enable transcription of user speech. - /// - public bool EnableUserTranscription { get; set; } = true; - - /// - /// Whether to enable transcription of AI speech. - /// - public bool EnableAssistantTranscription { get; set; } = true; - - /// - /// Whether to include partial (interim) transcriptions. - /// - public bool IncludePartialTranscriptions { get; set; } = true; - - /// - /// Transcription model to use if different from conversation model. - /// - public string? TranscriptionModel { get; set; } - } - - /// - /// Voice settings for real-time conversations. - /// - public class RealtimeVoiceSettings - { - /// - /// Speech speed adjustment (0.5-2.0, where 1.0 is normal). - /// - [Range(0.5, 2.0)] - public double? Speed { get; set; } - - /// - /// Pitch adjustment (provider-specific scale). - /// - public double? Pitch { get; set; } - - /// - /// Voice stability (ElevenLabs specific, 0-1). - /// - [Range(0.0, 1.0)] - public double? Stability { get; set; } - - /// - /// Similarity boost (ElevenLabs specific, 0-1). - /// - [Range(0.0, 1.0)] - public double? SimilarityBoost { get; set; } - - /// - /// Emotional style or tone. - /// - public string? Style { get; set; } - - /// - /// Provider-specific voice settings. - /// - public Dictionary? CustomSettings { get; set; } - } - - /// - /// Real-time audio format specifications. - /// - public enum RealtimeAudioFormat - { - /// - /// 16-bit PCM at 8kHz (telephone quality). - /// - PCM16_8kHz, - - /// - /// 16-bit PCM at 16kHz (wideband). - /// - PCM16_16kHz, - - /// - /// 16-bit PCM at 24kHz (high quality). - /// - PCM16_24kHz, - - /// - /// 16-bit PCM at 48kHz (studio quality). - /// - PCM16_48kHz, - - /// - /// G.711 μ-law at 8kHz (telephony). - /// - G711_ULAW, - - /// - /// G.711 A-law at 8kHz (telephony). - /// - G711_ALAW, - - /// - /// Opus codec (variable bitrate). - /// - Opus, - - /// - /// MP3 format (compressed). - /// - MP3 - } - - /// - /// Turn detection types. - /// - public enum TurnDetectionType - { - /// - /// Server-side voice activity detection. - /// - ServerVAD, - - /// - /// Manual turn control by the client. - /// - Manual, - - /// - /// Push-to-talk mode. - /// - PushToTalk - } - - /// - /// Conversation mode presets. - /// - public enum ConversationMode - { - /// - /// Natural conversational style with interruptions allowed. - /// - Conversational, - - /// - /// Interview style with clear turn-taking. - /// - Interview, - - /// - /// Command mode for short interactions. - /// - Command, - - /// - /// Presentation mode with minimal interruptions. - /// - Presentation, - - /// - /// Custom mode with manual settings. - /// - Custom - } -} diff --git a/ConduitLLM.Core/Models/Audio/TextToSpeechRequest.cs b/ConduitLLM.Core/Models/Audio/TextToSpeechRequest.cs deleted file mode 100644 index 9fee932ed..000000000 --- a/ConduitLLM.Core/Models/Audio/TextToSpeechRequest.cs +++ /dev/null @@ -1,252 +0,0 @@ -using System.ComponentModel.DataAnnotations; - -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Represents a request to convert text into speech audio. - /// - public class TextToSpeechRequest : AudioRequestBase - { - /// - /// The text to convert to speech. - /// - /// - /// This can be plain text or SSML markup, depending on provider support - /// and the EnableSSML flag. - /// - [Required] - [MaxLength(10000)] - public string Input { get; set; } = string.Empty; - - /// - /// The TTS model to use (e.g., "tts-1", "tts-1-hd"). - /// - /// - /// Different models may offer different quality levels, - /// latency characteristics, or voice options. - /// - public string? Model { get; set; } - - /// - /// The voice ID to use for synthesis. - /// - /// - /// Voice IDs are provider-specific. Common examples include - /// "alloy", "echo", "fable" for OpenAI, or specific voice IDs - /// for ElevenLabs and other providers. - /// - [Required] - public string Voice { get; set; } = string.Empty; - - /// - /// The desired audio output format. - /// - /// - /// Common formats include mp3, wav, flac, ogg, aac. - /// Default varies by provider. - /// - public AudioFormat? ResponseFormat { get; set; } - - /// - /// The speed of the generated speech (0.25-4.0). - /// - /// - /// 1.0 is normal speed, < 1.0 is slower, > 1.0 is faster. - /// Not all providers support speed adjustment. - /// - [Range(0.25, 4.0)] - public double? Speed { get; set; } - - /// - /// The pitch adjustment for the voice. - /// - /// - /// Provider-specific. Often a percentage or semitone adjustment. - /// May not be supported by all providers. - /// - public double? Pitch { get; set; } - - /// - /// The volume/gain adjustment (0-1). - /// - /// - /// 1.0 is normal volume. Some providers may support values > 1.0 - /// for amplification. - /// - [Range(0.0, 2.0)] - public double? Volume { get; set; } - - /// - /// Advanced voice settings for providers that support them. - /// - /// - /// Includes provider-specific parameters like emotion, style, - /// or voice characteristics. - /// - public VoiceSettings? VoiceSettings { get; set; } - - /// - /// The language code for synthesis (ISO-639-1). - /// - /// - /// Some voices support multiple languages. This ensures - /// proper pronunciation for the target language. - /// - [RegularExpression(@"^[a-z]{2}(-[A-Z]{2})?$", ErrorMessage = "Language must be in ISO-639-1 format")] - public string? Language { get; set; } - - /// - /// Whether the input contains SSML markup. - /// - /// - /// When true, the input is interpreted as SSML (Speech Synthesis Markup Language) - /// allowing fine control over pronunciation, pauses, emphasis, etc. - /// - public bool? EnableSSML { get; set; } - - /// - /// Sample rate for the output audio in Hz. - /// - /// - /// Common values: 8000 (telephone), 16000 (wideband), 24000 (high quality). - /// Provider may override based on format selection. - /// - public int? SampleRate { get; set; } - - /// - /// Whether to optimize for streaming playback. - /// - /// - /// When true, the provider may optimize chunk sizes and - /// encoding for progressive playback. - /// - public bool? OptimizeStreaming { get; set; } - - /// - /// Validates the request. - /// - public override bool IsValid(out string? errorMessage) - { - errorMessage = null; - - if (string.IsNullOrWhiteSpace(Input)) - { - errorMessage = "Input text is required"; - return false; - } - - if (string.IsNullOrWhiteSpace(Voice)) - { - errorMessage = "Voice selection is required"; - return false; - } - - if (Input.Length > 10000) - { - errorMessage = "Input text exceeds maximum length of 10000 characters"; - return false; - } - - return true; - } - } - - /// - /// Advanced voice settings for TTS. - /// - public class VoiceSettings - { - /// - /// Emotional tone (provider-specific scale). - /// - /// - /// For ElevenLabs, this might be "stability" (0-1). - /// For other providers, it could be emotion names. - /// - public double? Emotion { get; set; } - - /// - /// Voice style preset. - /// - /// - /// Examples: "news", "conversational", "narrative", "cheerful". - /// Support varies by provider and voice. - /// - public string? Style { get; set; } - - /// - /// Emphasis level for the speech. - /// - /// - /// Controls how much emphasis or expressiveness to add. - /// Scale and support vary by provider. - /// - public double? Emphasis { get; set; } - - /// - /// Voice similarity/consistency (ElevenLabs specific). - /// - public double? SimilarityBoost { get; set; } - - /// - /// Voice stability (ElevenLabs specific). - /// - public double? Stability { get; set; } - - /// - /// Additional provider-specific settings. - /// - public Dictionary? CustomSettings { get; set; } - } - - /// - /// Audio format options for TTS output. - /// - public enum AudioFormat - { - /// - /// MP3 format (most compatible). - /// - Mp3, - - /// - /// WAV format (uncompressed). - /// - Wav, - - /// - /// FLAC format (lossless compression). - /// - Flac, - - /// - /// OGG Vorbis format. - /// - Ogg, - - /// - /// AAC format. - /// - Aac, - - /// - /// Opus format (optimized for speech). - /// - Opus, - - /// - /// PCM raw audio data. - /// - Pcm, - - /// - /// μ-law format (telephony). - /// - Ulaw, - - /// - /// A-law format (telephony). - /// - Alaw - } -} diff --git a/ConduitLLM.Core/Models/Audio/TextToSpeechResponse.cs b/ConduitLLM.Core/Models/Audio/TextToSpeechResponse.cs deleted file mode 100644 index 8c3f0a5ae..000000000 --- a/ConduitLLM.Core/Models/Audio/TextToSpeechResponse.cs +++ /dev/null @@ -1,286 +0,0 @@ -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Represents the response from a text-to-speech synthesis request. - /// - public class TextToSpeechResponse - { - /// - /// The generated audio data as raw bytes. - /// - /// - /// Contains the complete audio file in the requested format. - /// For streaming responses, use the streaming API instead. - /// - public byte[] AudioData { get; set; } = Array.Empty(); - - /// - /// The audio format of the generated data. - /// - /// - /// Indicates the actual format of the audio data, - /// which may differ from the requested format if the - /// provider performed conversion. - /// - public string? Format { get; set; } - - /// - /// The sample rate of the audio in Hz. - /// - /// - /// Common values: 8000, 16000, 22050, 24000, 44100, 48000. - /// - public int? SampleRate { get; set; } - - /// - /// The duration of the generated audio in seconds. - /// - public double? Duration { get; set; } - - /// - /// The number of audio channels (1 = mono, 2 = stereo). - /// - public int? Channels { get; set; } - - /// - /// The bit depth of the audio (e.g., 16, 24, 32). - /// - /// - /// Applicable for uncompressed formats like WAV or PCM. - /// - public int? BitDepth { get; set; } - - /// - /// Character count of the input text. - /// - /// - /// Used for usage tracking and billing purposes. - /// - public int? CharacterCount { get; set; } - - /// - /// The voice ID that was actually used. - /// - /// - /// May differ from requested if fallback occurred. - /// - public string? VoiceUsed { get; set; } - - /// - /// The model that was actually used. - /// - /// - /// Indicates which TTS model processed the request. - /// - public string? ModelUsed { get; set; } - - /// - /// Usage information for billing purposes. - /// - public TextToSpeechUsage? Usage { get; set; } - - /// - /// Provider-specific metadata. - /// - public Dictionary? Metadata { get; set; } - } - - /// - /// Represents a chunk of audio data for streaming TTS. - /// - public class AudioChunk - { - /// - /// The audio data chunk. - /// - public byte[] Data { get; set; } = Array.Empty(); - - /// - /// The index of this chunk in the stream. - /// - public int ChunkIndex { get; set; } - - /// - /// Whether this is the final chunk. - /// - public bool IsFinal { get; set; } - - /// - /// The text portion this chunk corresponds to. - /// - /// - /// Some providers include text alignment information - /// to sync audio with text display. - /// - public string? TextSegment { get; set; } - - /// - /// Timestamp information for this chunk. - /// - public ChunkTimestamp? Timestamp { get; set; } - } - - /// - /// Timing information for an audio chunk. - /// - public class ChunkTimestamp - { - /// - /// Start time of this chunk in the overall audio (seconds). - /// - public double Start { get; set; } - - /// - /// End time of this chunk in the overall audio (seconds). - /// - public double End { get; set; } - - /// - /// Character offset in the original text. - /// - public int? TextOffset { get; set; } - } - - /// - /// Usage information for TTS operations. - /// - public class TextToSpeechUsage - { - /// - /// Number of characters processed. - /// - public int Characters { get; set; } - - /// - /// Duration of audio generated in seconds. - /// - public double AudioSeconds { get; set; } - - /// - /// Provider-specific usage metrics. - /// - public Dictionary? AdditionalMetrics { get; set; } - } - - /// - /// Information about an available TTS voice. - /// - public class VoiceInfo - { - /// - /// Unique identifier for the voice. - /// - public string VoiceId { get; set; } = string.Empty; - - /// - /// Display name of the voice. - /// - public string Name { get; set; } = string.Empty; - - /// - /// Description of the voice characteristics. - /// - public string? Description { get; set; } - - /// - /// Gender of the voice. - /// - public VoiceGender? Gender { get; set; } - - /// - /// Age group of the voice. - /// - public VoiceAge? Age { get; set; } - - /// - /// Languages supported by this voice. - /// - /// - /// ISO-639-1 language codes. - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Primary accent or locale of the voice. - /// - public string? Accent { get; set; } - - /// - /// Voice style capabilities. - /// - /// - /// Examples: "news", "conversational", "cheerful", "sad". - /// - public List? SupportedStyles { get; set; } - - /// - /// Whether this is a premium voice. - /// - /// - /// Premium voices may have higher quality or cost. - /// - public bool? IsPremium { get; set; } - - /// - /// Whether this is a custom/cloned voice. - /// - public bool? IsCustom { get; set; } - - /// - /// Sample audio URL for this voice. - /// - public string? SampleUrl { get; set; } - - /// - /// Provider-specific voice metadata. - /// - public Dictionary? Metadata { get; set; } - } - - /// - /// Voice gender categories. - /// - public enum VoiceGender - { - /// - /// Male voice. - /// - Male, - - /// - /// Female voice. - /// - Female, - - /// - /// Neutral/non-binary voice. - /// - Neutral - } - - /// - /// Voice age categories. - /// - public enum VoiceAge - { - /// - /// Child voice. - /// - Child, - - /// - /// Young adult voice. - /// - YoungAdult, - - /// - /// Middle-aged voice. - /// - MiddleAge, - - /// - /// Senior voice. - /// - Senior - } -} diff --git a/ConduitLLM.Core/Models/Audio/TtsCacheEntry.cs b/ConduitLLM.Core/Models/Audio/TtsCacheEntry.cs deleted file mode 100644 index fa7f127b4..000000000 --- a/ConduitLLM.Core/Models/Audio/TtsCacheEntry.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ConduitLLM.Core.Models.Audio -{ - /// - /// Cache entry for TTS (Text-to-Speech) responses. - /// - public class TtsCacheEntry - { - /// - /// Gets or sets the TTS response data. - /// - public TextToSpeechResponse Response { get; set; } = new(); - - /// - /// Gets or sets the UTC timestamp when this entry was cached. - /// - public DateTime CachedAt { get; set; } - - /// - /// Gets or sets the size of the audio data in bytes. - /// - public long SizeBytes { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Models/AudioCostResult.cs b/ConduitLLM.Core/Models/AudioCostResult.cs deleted file mode 100644 index 743befa12..000000000 --- a/ConduitLLM.Core/Models/AudioCostResult.cs +++ /dev/null @@ -1,20 +0,0 @@ -namespace ConduitLLM.Core.Models -{ - /// - /// Result of audio cost calculation. - /// - public class AudioCostResult - { - public string Provider { get; set; } = string.Empty; - public string Operation { get; set; } = string.Empty; - public string Model { get; set; } = string.Empty; - public double UnitCount { get; set; } - public string UnitType { get; set; } = string.Empty; - public decimal RatePerUnit { get; set; } - public double TotalCost { get; set; } - public string? VirtualKey { get; set; } - public string? Voice { get; set; } - public bool IsEstimate { get; set; } - public Dictionary? DetailedBreakdown { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Models/CachedModelCost.cs b/ConduitLLM.Core/Models/CachedModelCost.cs index f74d30912..0c75df606 100644 --- a/ConduitLLM.Core/Models/CachedModelCost.cs +++ b/ConduitLLM.Core/Models/CachedModelCost.cs @@ -103,25 +103,6 @@ public class CachedModelCost /// public int? DefaultInferenceSteps { get; set; } - /// - /// Cost per minute for audio transcription, if applicable. - /// - public decimal? AudioCostPerMinute { get; set; } - - /// - /// Cost per 1000 characters for text-to-speech synthesis, if applicable. - /// - public decimal? AudioCostPerKCharacters { get; set; } - - /// - /// Cost per minute for real-time audio input, if applicable. - /// - public decimal? AudioInputCostPerMinute { get; set; } - - /// - /// Cost per minute for real-time audio output, if applicable. - /// - public decimal? AudioOutputCostPerMinute { get; set; } /// /// Model type for categorization. diff --git a/ConduitLLM.Core/Models/Configuration/ModelConfiguration.cs b/ConduitLLM.Core/Models/Configuration/ModelConfiguration.cs index 1659464f2..dfc04790f 100644 --- a/ConduitLLM.Core/Models/Configuration/ModelConfiguration.cs +++ b/ConduitLLM.Core/Models/Configuration/ModelConfiguration.cs @@ -51,21 +51,6 @@ public class ModelCapabilities /// public bool SupportsVision { get; set; } - /// - /// Gets or sets whether the model supports audio transcription. - /// - public bool SupportsTranscription { get; set; } - - /// - /// Gets or sets whether the model supports text-to-speech. - /// - public bool SupportsTextToSpeech { get; set; } - - /// - /// Gets or sets whether the model supports real-time audio streaming. - /// - public bool SupportsRealtimeAudio { get; set; } - /// /// Gets or sets whether the model supports function calling. /// @@ -96,20 +81,6 @@ public class ModelCapabilities /// public int? MaxTokens { get; set; } - /// - /// Gets or sets the supported voices for TTS models. - /// - public List SupportedVoices { get; set; } = new(); - - /// - /// Gets or sets the supported languages. - /// - public List SupportedLanguages { get; set; } = new(); - - /// - /// Gets or sets the supported audio formats. - /// - public List SupportedFormats { get; set; } = new(); /// /// Gets or sets additional capability metadata. diff --git a/ConduitLLM.Core/Models/ProviderCapabilities.cs b/ConduitLLM.Core/Models/ProviderCapabilities.cs index 01081f85e..cd5a33298 100644 --- a/ConduitLLM.Core/Models/ProviderCapabilities.cs +++ b/ConduitLLM.Core/Models/ProviderCapabilities.cs @@ -73,8 +73,6 @@ public class FeatureSupport public bool ImageGeneration { get; set; } public bool VisionInput { get; set; } public bool FunctionCalling { get; set; } - public bool AudioTranscription { get; set; } - public bool TextToSpeech { get; set; } } /// diff --git a/ConduitLLM.Core/Models/Realtime/ConnectionModels.cs b/ConduitLLM.Core/Models/Realtime/ConnectionModels.cs deleted file mode 100644 index 6e8106aa7..000000000 --- a/ConduitLLM.Core/Models/Realtime/ConnectionModels.cs +++ /dev/null @@ -1,99 +0,0 @@ -namespace ConduitLLM.Core.Models.Realtime -{ - /// - /// Information about an active real-time connection. - /// - public class ConnectionInfo - { - /// - /// Unique connection identifier. - /// - public string ConnectionId { get; set; } = string.Empty; - - /// - /// The model being used. - /// - public string Model { get; set; } = string.Empty; - - /// - /// The provider being used. - /// - public string? Provider { get; set; } - - /// - /// When the connection was established. - /// - public DateTime ConnectedAt { get; set; } - - /// - /// Current connection state. - /// - public string State { get; set; } = "active"; - - /// - /// Usage statistics for this connection. - /// - public ConnectionUsageStats? Usage { get; set; } - - /// - /// The virtual key associated with this connection. - /// - public string VirtualKey { get; set; } = string.Empty; - - /// - /// The provider connection ID. - /// - public string? ProviderConnectionId { get; set; } - - /// - /// Connection start time. - /// - public DateTime StartTime { get; set; } - - /// - /// Last activity timestamp. - /// - public DateTime LastActivity { get; set; } - - /// - /// Total audio bytes processed. - /// - public long AudioBytesProcessed { get; set; } - - /// - /// Total tokens used. - /// - public long TokensUsed { get; set; } - - /// - /// Estimated cost. - /// - public decimal EstimatedCost { get; set; } - } - - /// - /// Usage statistics for a real-time connection. - /// - public class ConnectionUsageStats - { - /// - /// Total audio duration in seconds. - /// - public double AudioDurationSeconds { get; set; } - - /// - /// Number of messages sent. - /// - public int MessagesSent { get; set; } - - /// - /// Number of messages received. - /// - public int MessagesReceived { get; set; } - - /// - /// Estimated cost so far. - /// - public decimal EstimatedCost { get; set; } - } -} diff --git a/ConduitLLM.Core/Models/RefundResult.cs b/ConduitLLM.Core/Models/RefundResult.cs index 678202925..b4e335ec5 100644 --- a/ConduitLLM.Core/Models/RefundResult.cs +++ b/ConduitLLM.Core/Models/RefundResult.cs @@ -96,85 +96,4 @@ public class RefundBreakdown /// public decimal InferenceStepRefund { get; set; } } - - /// - /// Represents the result of an audio refund calculation operation. - /// - public class AudioRefundResult - { - /// - /// Gets or sets the provider name. - /// - public string Provider { get; set; } = string.Empty; - - /// - /// Gets or sets the operation type (transcription, text-to-speech, realtime). - /// - public string Operation { get; set; } = string.Empty; - - /// - /// Gets or sets the model name. - /// - public string Model { get; set; } = string.Empty; - - /// - /// Gets or sets the original usage amount. - /// - public double OriginalAmount { get; set; } - - /// - /// Gets or sets the refund usage amount. - /// - public double RefundAmount { get; set; } - - /// - /// Gets or sets the unit type (minutes, characters, etc.). - /// - public string UnitType { get; set; } = string.Empty; - - /// - /// Gets or sets the total refund cost (always positive). - /// - public double TotalRefund { get; set; } - - /// - /// Gets or sets the original transaction ID if provided. - /// - public string? OriginalTransactionId { get; set; } - - /// - /// Gets or sets the reason for the refund. - /// - public string RefundReason { get; set; } = string.Empty; - - /// - /// Gets or sets the timestamp when the refund was calculated. - /// - public DateTime RefundedAt { get; set; } = DateTime.UtcNow; - - /// - /// Gets or sets whether the refund was partially applied due to validation constraints. - /// - public bool IsPartialRefund { get; set; } - - /// - /// Gets or sets the validation messages if any constraints were applied. - /// - public List ValidationMessages { get; set; } = new List(); - - /// - /// Gets or sets the virtual key associated with the refund. - /// - public string? VirtualKey { get; set; } - - /// - /// Gets or sets the voice used (for TTS operations). - /// - public string? Voice { get; set; } - - /// - /// Gets or sets the detailed breakdown for complex refunds (e.g., realtime sessions). - /// - public Dictionary? DetailedBreakdown { get; set; } - } } \ No newline at end of file diff --git a/ConduitLLM.Core/Models/Routing/FallbackConfiguration.cs b/ConduitLLM.Core/Models/Routing/FallbackConfiguration.cs deleted file mode 100644 index dfc11f7e3..000000000 --- a/ConduitLLM.Core/Models/Routing/FallbackConfiguration.cs +++ /dev/null @@ -1,23 +0,0 @@ -namespace ConduitLLM.Core.Models.Routing -{ - /// - /// Configuration for model fallback strategies - /// - public class FallbackConfiguration - { - /// - /// Unique identifier for this fallback configuration - /// - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The ID of the primary model deployment that will fall back to others if it fails - /// - public string PrimaryModelDeploymentId { get; set; } = string.Empty; - - /// - /// Ordered list of model deployment IDs to use as fallbacks (in priority order) - /// - public List FallbackModelDeploymentIds { get; set; } = new(); - } -} diff --git a/ConduitLLM.Core/Models/Routing/RouterConfig.cs b/ConduitLLM.Core/Models/Routing/RouterConfig.cs deleted file mode 100644 index 78b482c96..000000000 --- a/ConduitLLM.Core/Models/Routing/RouterConfig.cs +++ /dev/null @@ -1,183 +0,0 @@ -namespace ConduitLLM.Core.Models.Routing -{ - /// - /// Configuration for the LLM Router - /// - public class RouterConfig - { - /// - /// List of model deployments available to the router - /// - public List ModelDeployments { get; set; } = new(); - - /// - /// Default routing strategy to use when not explicitly specified - /// - public string DefaultRoutingStrategy { get; set; } = "simple"; - - /// - /// Dictionary of fallback configurations where keys are model names and values are lists of fallback models - /// - public Dictionary> Fallbacks { get; set; } = new(); - - /// - /// Maximum number of retries for a failed request - /// - public int MaxRetries { get; set; } = 3; - - /// - /// Base delay in milliseconds between retries (for exponential backoff) - /// - public int RetryBaseDelayMs { get; set; } = 500; - - /// - /// Maximum delay in milliseconds between retries - /// - public int RetryMaxDelayMs { get; set; } = 10000; - - /// - /// Whether fallbacks are enabled - /// - public bool FallbacksEnabled { get; set; } = false; - - /// - /// List of fallback configurations between models - /// - public List FallbackConfigurations { get; set; } = new(); - } - - /// - /// Represents a model deployment that can be used by the router - /// - public class ModelDeployment - { - /// - /// Unique identifier for this model deployment - /// - public Guid Id { get; set; } = Guid.NewGuid(); - - /// - /// The name of the model (e.g., gpt-4, claude-3-opus) - /// - public string ModelName { get; set; } = string.Empty; - - /// - /// The name of the provider for this model (e.g., OpenAI, Anthropic) - /// - public string ProviderName { get; set; } = string.Empty; - - /// - /// Unique name for this model deployment - compatibility property - /// - public string DeploymentName - { - get => ModelName; - set => ModelName = value; - } - - /// - /// The model alias this deployment refers to - compatibility property - /// - public string ModelAlias - { - get => ProviderName; - set => ProviderName = value; - } - - /// - /// Weight for random selection strategy (higher values increase selection probability) - /// - public int Weight { get; set; } = 1; - - /// - /// Whether health checking is enabled for this deployment - /// - public bool HealthCheckEnabled { get; set; } = true; - - /// - /// Whether this deployment is enabled and available for routing - /// - public bool IsEnabled { get; set; } = true; - - /// - /// Maximum requests per minute for this deployment - /// - public int? RPM { get; set; } - - /// - /// Maximum tokens per minute for this deployment - /// - public int? TPM { get; set; } - - /// - /// Cost per 1000 input tokens - /// - public decimal? InputTokenCostPer1K { get; set; } - - /// - /// Cost per 1000 output tokens - /// - public decimal? OutputTokenCostPer1K { get; set; } - - /// - /// Priority of this deployment (lower values are higher priority) - /// - public int Priority { get; set; } = 1; - - /// - /// Health status of this deployment - /// - public bool IsHealthy { get; set; } = true; - - /// - /// Last time this deployment was used - /// - public DateTime LastUsed { get; set; } = DateTime.MinValue; - - /// - /// Number of requests made to this deployment - /// - public int RequestCount { get; set; } = 0; - - /// - /// Average latency in milliseconds - /// - public double AverageLatencyMs { get; set; } = 0; - - /// - /// Whether this deployment supports embedding operations - /// - public bool SupportsEmbeddings { get; set; } = false; - } - - /// - /// Strategy to use when routing requests to models - /// - public enum RoutingStrategy - { - /// - /// Use the first available model in the list - /// - Simple, - - /// - /// Distribute requests evenly across all available models - /// - RoundRobin, - - /// - /// Use the model with the lowest cost - /// - LeastCost, - - /// - /// Use the model with the lowest latency - /// - LeastLatency, - - /// - /// Use the model with the highest priority (lowest priority value) - /// - HighestPriority - } -} diff --git a/ConduitLLM.Core/Models/Usage.cs b/ConduitLLM.Core/Models/Usage.cs index aaa8f03e2..3ffd73c9d 100644 --- a/ConduitLLM.Core/Models/Usage.cs +++ b/ConduitLLM.Core/Models/Usage.cs @@ -140,16 +140,6 @@ public class Usage [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public int? InferenceSteps { get; set; } - /// - /// Duration of audio in seconds (used for speech generation and transcription). - /// - /// - /// Used by audio models for both text-to-speech and speech-to-text operations. - /// Most providers charge per minute of audio, which is calculated from this value. - /// - [JsonPropertyName("audio_duration_seconds")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public decimal? AudioDurationSeconds { get; set; } /// /// Optional metadata for provider-specific usage information. diff --git a/ConduitLLM.Core/Providers/BaseProviderMetadata.cs b/ConduitLLM.Core/Providers/BaseProviderMetadata.cs index 102ba2011..b50915301 100644 --- a/ConduitLLM.Core/Providers/BaseProviderMetadata.cs +++ b/ConduitLLM.Core/Providers/BaseProviderMetadata.cs @@ -125,9 +125,7 @@ protected virtual ProviderCapabilities CreateDefaultCapabilities() Embeddings = false, ImageGeneration = false, VisionInput = false, - FunctionCalling = false, - AudioTranscription = false, - TextToSpeech = false + FunctionCalling = false } }; } diff --git a/ConduitLLM.Core/Providers/Metadata/AllProviders.cs b/ConduitLLM.Core/Providers/Metadata/AllProviders.cs index 757194175..1cb921154 100644 --- a/ConduitLLM.Core/Providers/Metadata/AllProviders.cs +++ b/ConduitLLM.Core/Providers/Metadata/AllProviders.cs @@ -46,7 +46,6 @@ public class ReplicateProviderMetadata : BaseProviderMetadata public ReplicateProviderMetadata() { Capabilities.Features.ImageGeneration = true; - Capabilities.Features.AudioTranscription = true; Capabilities.Features.VisionInput = true; AuthRequirements.ApiKeyHeaderName = "Authorization"; @@ -110,50 +109,11 @@ public class MiniMaxProviderMetadata : BaseProviderMetadata public MiniMaxProviderMetadata() { - Capabilities.Features.TextToSpeech = true; - Capabilities.Features.AudioTranscription = true; ConfigurationHints.DocumentationUrl = "https://api.minimax.chat/document/introduction"; } } - /// - /// Provider metadata for Ultravox. - /// - public class UltravoxProviderMetadata : BaseProviderMetadata - { - public override ProviderType ProviderType => ProviderType.Ultravox; - public override string DisplayName => "Ultravox"; - public override string DefaultBaseUrl => "https://api.ultravox.ai/v1"; - - public UltravoxProviderMetadata() - { - Capabilities.Features.AudioTranscription = true; - Capabilities.Features.TextToSpeech = true; - - ConfigurationHints.DocumentationUrl = "https://docs.ultravox.ai/"; - } - } - - /// - /// Provider metadata for ElevenLabs. - /// - public class ElevenLabsProviderMetadata : BaseProviderMetadata - { - public override ProviderType ProviderType => ProviderType.ElevenLabs; - public override string DisplayName => "ElevenLabs"; - public override string DefaultBaseUrl => "https://api.elevenlabs.io/v1"; - - public ElevenLabsProviderMetadata() - { - Capabilities.Features.TextToSpeech = true; - Capabilities.Features.Streaming = false; - Capabilities.ChatParameters = new ChatParameterSupport(); // Audio provider, limited chat params - - AuthRequirements.ApiKeyHeaderName = "xi-api-key"; - ConfigurationHints.DocumentationUrl = "https://docs.elevenlabs.io/"; - } - } /// diff --git a/ConduitLLM.Core/Providers/Metadata/OpenAIProviderMetadata.cs b/ConduitLLM.Core/Providers/Metadata/OpenAIProviderMetadata.cs index 56ab4187b..1f0f62f86 100644 --- a/ConduitLLM.Core/Providers/Metadata/OpenAIProviderMetadata.cs +++ b/ConduitLLM.Core/Providers/Metadata/OpenAIProviderMetadata.cs @@ -54,9 +54,7 @@ public OpenAIProviderMetadata() Embeddings = true, ImageGeneration = true, VisionInput = true, - FunctionCalling = true, - AudioTranscription = true, - TextToSpeech = true + FunctionCalling = true } }; diff --git a/ConduitLLM.Core/Routing/AudioRouter.cs b/ConduitLLM.Core/Routing/AudioRouter.cs deleted file mode 100644 index 8bbace2f5..000000000 --- a/ConduitLLM.Core/Routing/AudioRouter.cs +++ /dev/null @@ -1,274 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Routing -{ - /// - /// Default implementation of the audio router for routing audio requests to appropriate providers. - /// Uses Provider IDs and database queries instead of hardcoded provider lists. - /// - public class AudioRouter : IAudioRouter - { - private readonly ILLMClientFactory _clientFactory; - private readonly ILogger _logger; - private readonly IModelProviderMappingService _modelMappingService; - private readonly Dictionary _statistics = new(); - - public AudioRouter( - ILLMClientFactory clientFactory, - ILogger logger, - IModelProviderMappingService modelMappingService) - { - _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _modelMappingService = modelMappingService ?? throw new ArgumentNullException(nameof(modelMappingService)); - } - - public async Task GetTranscriptionClientAsync( - AudioTranscriptionRequest request, - string virtualKey, - CancellationToken cancellationToken = default) - { - try - { - if (string.IsNullOrEmpty(request.Model)) - { - _logger.LogWarning("No model specified in transcription request"); - return null; - } - - // Use the canonical model mapping approach - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(request.Model); - if (modelMapping == null) - { - _logger.LogWarning("No model mapping found for transcription model: {Model}", request.Model); - return null; - } - - // Get the client using the standard factory method (which uses model alias) - var client = _clientFactory.GetClient(request.Model); - - if (client is IAudioTranscriptionClient audioClient) - { - // Update the request to use the provider's model ID - request.Model = modelMapping.ProviderModelId; - - _logger.LogInformation( - "Routed transcription request to provider: {ProviderId} for model: {Model}", - modelMapping.ProviderId, - modelMapping.ModelAlias); - - UpdateStatistics(modelMapping.ProviderId); - return audioClient; - } - - _logger.LogWarning("Client for model {Model} does not implement IAudioTranscriptionClient", - modelMapping.ModelAlias); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error routing transcription request"); - return null; - } - } - - public async Task GetTextToSpeechClientAsync( - TextToSpeechRequest request, - string virtualKey, - CancellationToken cancellationToken = default) - { - try - { - if (string.IsNullOrEmpty(request.Model)) - { - _logger.LogWarning("No model specified in TTS request"); - return null; - } - - // Use the canonical model mapping approach - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(request.Model); - if (modelMapping == null) - { - _logger.LogWarning("No model mapping found for TTS model: {Model}", request.Model); - return null; - } - - // Get the client using the standard factory method (which uses model alias) - var client = _clientFactory.GetClient(request.Model); - - if (client is ITextToSpeechClient ttsClient) - { - // Update the request to use the provider's model ID - request.Model = modelMapping.ProviderModelId; - - _logger.LogInformation( - "Routed TTS request to provider: {ProviderId} for model: {Model}", - modelMapping.ProviderId, - modelMapping.ModelAlias); - - UpdateStatistics(modelMapping.ProviderId); - return ttsClient; - } - - _logger.LogWarning("Client for model {Model} does not implement ITextToSpeechClient", - modelMapping.ModelAlias); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error routing TTS request"); - return null; - } - } - - public async Task GetRealtimeClientAsync( - RealtimeSessionConfig config, - string virtualKey, - CancellationToken cancellationToken = default) - { - try - { - if (string.IsNullOrEmpty(config.Model)) - { - _logger.LogWarning("No model specified in real-time config"); - return null; - } - - // Use the canonical model mapping approach - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(config.Model); - if (modelMapping == null) - { - _logger.LogWarning("No model mapping found for real-time model: {Model}", config.Model); - return null; - } - - // Get the client using the standard factory method (which uses model alias) - var client = _clientFactory.GetClient(config.Model); - - if (client is IRealtimeAudioClient realtimeClient) - { - // Update the config to use the provider's model ID - config.Model = modelMapping.ProviderModelId; - - _logger.LogInformation( - "Routed real-time session to provider: {ProviderId} for model: {Model}", - modelMapping.ProviderId, - modelMapping.ModelAlias); - - UpdateStatistics(modelMapping.ProviderId); - return realtimeClient; - } - - _logger.LogWarning("Client for model {Model} does not implement IRealtimeAudioClient", - modelMapping.ModelAlias); - return null; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error routing real-time request"); - return null; - } - } - - public async Task> GetAvailableTranscriptionProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - // Get all model mappings that support transcription - var allMappings = await _modelMappingService.GetAllMappingsAsync(); - var transcriptionModels = allMappings - .Where(m => m.SupportsAudioTranscription) - .Select(m => m.ModelAlias) - .Distinct() - .ToList(); - - return transcriptionModels; - } - - public async Task> GetAvailableTextToSpeechProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - // Get all model mappings that support TTS - var allMappings = await _modelMappingService.GetAllMappingsAsync(); - var ttsModels = allMappings - .Where(m => m.SupportsTextToSpeech) - .Select(m => m.ModelAlias) - .Distinct() - .ToList(); - - return ttsModels; - } - - public async Task> GetAvailableRealtimeProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - // Get all model mappings that support real-time audio - var allMappings = await _modelMappingService.GetAllMappingsAsync(); - var realtimeModels = allMappings - .Where(m => m.SupportsRealtimeAudio) - .Select(m => m.ModelAlias) - .Distinct() - .ToList(); - - return realtimeModels; - } - - public bool ValidateAudioOperation( - AudioOperation operation, - string provider, - AudioRequestBase request, - out string errorMessage) - { - errorMessage = "Provider-based validation not implemented in refactored AudioRouter"; - return false; - } - - public Task GetRoutingStatisticsAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - lock (_statistics) - { - var combinedStats = new AudioRoutingStatistics(); - if (_statistics.Count() > 0) - { - combinedStats.TranscriptionRequests = _statistics.Values.Sum(s => s.TranscriptionRequests); - combinedStats.TextToSpeechRequests = _statistics.Values.Sum(s => s.TextToSpeechRequests); - combinedStats.RealtimeSessions = _statistics.Values.Sum(s => s.RealtimeSessions); - combinedStats.TotalRequests = _statistics.Values.Sum(s => s.TotalRequests); - combinedStats.FailedRoutingAttempts = _statistics.Values.Sum(s => s.FailedRoutingAttempts); - combinedStats.LastUpdated = DateTime.UtcNow; - } - - return Task.FromResult(combinedStats); - } - } - - - /// - /// Updates routing statistics for a provider. - /// - private void UpdateStatistics(int providerId) - { - lock (_statistics) - { - if (!_statistics.ContainsKey(providerId)) - { - _statistics[providerId] = new AudioRoutingStatistics - { - LastUpdated = DateTime.UtcNow - }; - } - - _statistics[providerId].TotalRequests++; - _statistics[providerId].LastUpdated = DateTime.UtcNow; - } - } - } -} diff --git a/ConduitLLM.Core/Routing/AudioRoutingStrategies/CostOptimizedRoutingStrategy.cs b/ConduitLLM.Core/Routing/AudioRoutingStrategies/CostOptimizedRoutingStrategy.cs deleted file mode 100644 index 28fdbc5c1..000000000 --- a/ConduitLLM.Core/Routing/AudioRoutingStrategies/CostOptimizedRoutingStrategy.cs +++ /dev/null @@ -1,207 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing.AudioRoutingStrategies -{ - /// - /// Routes audio requests to minimize cost while maintaining quality thresholds. - /// - public class CostOptimizedRoutingStrategy : IAudioRoutingStrategy - { - private readonly ILogger _logger; - private readonly double _defaultQualityThreshold; - - /// - public string Name => "CostOptimized"; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// Default minimum quality score (0-100). - public CostOptimizedRoutingStrategy( - ILogger logger, - double defaultQualityThreshold = 70) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _defaultQualityThreshold = defaultQualityThreshold; - } - - /// - public Task SelectTranscriptionProviderAsync( - AudioTranscriptionRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - var qualityThreshold = request.RequiredQuality ?? _defaultQualityThreshold; - var audioFormat = request.AudioFormat ?? AudioFormat.Mp3; - var estimatedDuration = EstimateAudioDuration(request.AudioData?.Length ?? 0, audioFormat); - - return SelectProviderByCostAsync( - availableProviders, - p => CalculateTranscriptionCost(p, estimatedDuration), - qualityThreshold, - p => p.Capabilities.SupportsStreaming || !request.EnableStreaming, - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.AudioFormat?.ToString())); - } - - /// - public Task SelectTextToSpeechProviderAsync( - TextToSpeechRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - var qualityThreshold = _defaultQualityThreshold; - var characterCount = request.Input.Length; - - return SelectProviderByCostAsync( - availableProviders, - p => CalculateTTSCost(p, characterCount), - qualityThreshold, - p => p.Capabilities.SupportedVoices.Contains(request.Voice) || - p.Capabilities.SupportedVoices.Count() == 0, - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.ResponseFormat?.ToString())); - } - - /// - public Task UpdateMetricsAsync( - string provider, - AudioRequestMetrics metrics, - CancellationToken cancellationToken = default) - { - // Log cost efficiency - if (metrics.Success) - { - var costEfficiency = CalculateCostEfficiency(provider, metrics); - _logger.LogDebug( - "Provider {Provider} cost efficiency: ${Cost:F4} per unit", - provider, - costEfficiency); - } - - return Task.CompletedTask; - } - - private Task SelectProviderByCostAsync( - IReadOnlyList availableProviders, - Func costCalculator, - double qualityThreshold, - params Func[] filters) - { - // Filter by availability, quality threshold, and other criteria - var eligibleProviders = availableProviders - .Where(p => p.IsAvailable && - p.Capabilities.QualityScore >= qualityThreshold && - filters.All(f => f(p))) - .ToList(); - - if (eligibleProviders.Count() == 0) - { - _logger.LogWarning( - "No eligible providers found with quality >= {Quality}", - qualityThreshold); - return Task.FromResult(null); - } - - // Calculate effective cost (considering success rate and potential retries) - var costedProviders = eligibleProviders - .Select(p => new - { - Provider = p, - BaseCost = costCalculator(p), - EffectiveCost = CalculateEffectiveCost(costCalculator(p), p.Metrics.SuccessRate), - QualityAdjustedCost = CalculateQualityAdjustedCost( - costCalculator(p), - p.Metrics.SuccessRate, - p.Capabilities.QualityScore) - }) - .OrderBy(x => x.QualityAdjustedCost) - .ToList(); - - var selected = costedProviders.First(); - - _logger.LogInformation( - "Selected {Provider} with cost ${Cost:F4} (effective: ${Effective:F4}, quality-adjusted: ${QualityAdjusted:F4})", - selected.Provider.Name, - selected.BaseCost, - selected.EffectiveCost, - selected.QualityAdjustedCost); - - return Task.FromResult(selected.Provider.Name); - } - - private decimal CalculateTranscriptionCost(AudioProviderInfo provider, double durationMinutes) - { - return provider.Costs.TranscriptionPerMinute * (decimal)durationMinutes; - } - - private decimal CalculateTTSCost(AudioProviderInfo provider, int characterCount) - { - return provider.Costs.TextToSpeechPer1kChars * (characterCount / 1000m); - } - - private decimal CalculateEffectiveCost(decimal baseCost, double successRate) - { - // Account for retries due to failures - if (successRate <= 0) return baseCost * 10; // Penalize heavily - - var expectedAttempts = 1.0 / successRate; - return baseCost * (decimal)expectedAttempts; - } - - private decimal CalculateQualityAdjustedCost(decimal baseCost, double successRate, double qualityScore) - { - // Lower quality should be reflected as higher "true" cost - var qualityMultiplier = 2.0 - (qualityScore / 100.0); // 1.0 to 2.0 - var effectiveCost = CalculateEffectiveCost(baseCost, successRate); - - return effectiveCost * (decimal)qualityMultiplier; - } - - private double EstimateAudioDuration(int audioDataLength, AudioFormat format) - { - // Rough estimates based on typical bitrates - var bytesPerSecond = format switch - { - AudioFormat.Mp3 => 16000, // 128 kbps - AudioFormat.Wav => 176400, // 1411 kbps (CD quality) - AudioFormat.Flac => 88200, // ~700 kbps - AudioFormat.Ogg => 12000, // 96 kbps - AudioFormat.Opus => 6000, // 48 kbps - _ => 16000 // Default to MP3 estimate - }; - - var durationSeconds = audioDataLength / (double)bytesPerSecond; - return durationSeconds / 60.0; // Convert to minutes - } - - private double CalculateCostEfficiency(string provider, AudioRequestMetrics metrics) - { - // This would calculate actual cost based on the request - // For now, return a placeholder - return 0.01; - } - - private bool SupportsLanguage(AudioProviderInfo provider, string? language) - { - if (string.IsNullOrEmpty(language)) - return true; - - return provider.Capabilities.SupportedLanguages.Count() == 0 || - provider.Capabilities.SupportedLanguages.Contains(language); - } - - private bool SupportsFormat(AudioProviderInfo provider, string? format) - { - if (string.IsNullOrEmpty(format)) - return true; - - return provider.Capabilities.SupportedFormats.Count() == 0 || - provider.Capabilities.SupportedFormats.Contains(format, StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/ConduitLLM.Core/Routing/AudioRoutingStrategies/LanguageOptimizedRoutingStrategy.cs b/ConduitLLM.Core/Routing/AudioRoutingStrategies/LanguageOptimizedRoutingStrategy.cs deleted file mode 100644 index eeeacf408..000000000 --- a/ConduitLLM.Core/Routing/AudioRoutingStrategies/LanguageOptimizedRoutingStrategy.cs +++ /dev/null @@ -1,192 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing.AudioRoutingStrategies -{ - /// - /// Routes audio requests based on language expertise and quality scores. - /// - public class LanguageOptimizedRoutingStrategy : IAudioRoutingStrategy - { - private readonly ILogger _logger; - private readonly Dictionary> _languageQualityScores; - - /// - public string Name => "LanguageOptimized"; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - public LanguageOptimizedRoutingStrategy(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _languageQualityScores = InitializeLanguageScores(); - } - - /// - public Task SelectTranscriptionProviderAsync( - AudioTranscriptionRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - var language = request.Language ?? DetectLanguageFromRequest(request); - - return SelectProviderByLanguageAsync( - language, - availableProviders, - p => p.Capabilities.SupportsStreaming || !request.EnableStreaming, - p => SupportsFormat(p, request.AudioFormat.ToString())); - } - - /// - public Task SelectTextToSpeechProviderAsync( - TextToSpeechRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - var language = request.Language ?? "en"; - - return SelectProviderByLanguageAsync( - language, - availableProviders, - p => p.Capabilities.SupportedVoices.Contains(request.Voice) || - p.Capabilities.SupportedVoices.Count() == 0, - p => SupportsFormat(p, request.ResponseFormat?.ToString())); - } - - /// - public Task UpdateMetricsAsync( - string provider, - AudioRequestMetrics metrics, - CancellationToken cancellationToken = default) - { - // Update language-specific quality scores based on success rates - if (!string.IsNullOrEmpty(metrics.Language) && metrics.Success) - { - if (!_languageQualityScores.ContainsKey(provider)) - { - _languageQualityScores[provider] = new Dictionary(); - } - - var currentScore = _languageQualityScores[provider].GetValueOrDefault(metrics.Language, 0.7); - // Exponential moving average - _languageQualityScores[provider][metrics.Language] = (currentScore * 0.9) + (metrics.Success ? 0.1 : 0); - } - - return Task.CompletedTask; - } - - private Task SelectProviderByLanguageAsync( - string language, - IReadOnlyList availableProviders, - params Func[] filters) - { - // Filter available providers - var eligibleProviders = availableProviders - .Where(p => p.IsAvailable && filters.All(f => f(p))) - .Where(p => SupportsLanguage(p, language)) - .ToList(); - - if (eligibleProviders.Count() == 0) - { -_logger.LogWarning("No eligible providers found for language {Language}", language.Replace(Environment.NewLine, "")); - return Task.FromResult(null); - } - - // Score providers based on language expertise - var scoredProviders = eligibleProviders - .Select(p => new - { - Provider = p, - Score = CalculateLanguageScore(p, language) - }) - .OrderByDescending(x => x.Score) - .ToList(); - - var selected = scoredProviders.First(); - - _logger.LogInformation( - "Selected {Provider} for language {Language} with score {Score:F2}", - selected.Provider.Name, - language.Replace(Environment.NewLine, ""), - selected.Score); - - return Task.FromResult(selected.Provider.Name); - } - - private double CalculateLanguageScore(AudioProviderInfo provider, string language) - { - var baseScore = provider.Capabilities.QualityScore / 100.0; - - // Check predefined language expertise - var languageScore = GetPredefinedLanguageScore(provider.Name, language); - - // Check historical performance - if (_languageQualityScores.TryGetValue(provider.Name, out var scores) && - scores.TryGetValue(language, out var historicalScore)) - { - languageScore = (languageScore * 0.4) + (historicalScore * 0.6); - } - - // Factor in current metrics - var performanceScore = provider.Metrics.SuccessRate * (1 - (provider.Metrics.AverageLatencyMs / 5000.0)); - - return (baseScore * 0.3) + (languageScore * 0.5) + (performanceScore * 0.2); - } - - private double GetPredefinedLanguageScore(string provider, string language) - { - // All providers are assumed equally capable unless we have actual metrics - // No arbitrary scores based on assumptions - return 0.80; - } - - private string GetLanguageFamily(string language) - { - return language switch - { - "en" or "en-US" or "en-GB" => "english", - "zh" or "ja" or "ko" or "th" or "vi" => "asian", - "es" or "fr" or "de" or "it" or "pt" or "ru" or "pl" => "european", - _ => "other" - }; - } - - private bool SupportsLanguage(AudioProviderInfo provider, string? language) - { - if (string.IsNullOrEmpty(language)) - return true; - - return provider.Capabilities.SupportedLanguages.Count() == 0 || - provider.Capabilities.SupportedLanguages.Contains(language); - } - - private bool SupportsFormat(AudioProviderInfo provider, string? format) - { - if (string.IsNullOrEmpty(format)) - return true; - - return provider.Capabilities.SupportedFormats.Count() == 0 || - provider.Capabilities.SupportedFormats.Contains(format, StringComparer.OrdinalIgnoreCase); - } - - private string DetectLanguageFromRequest(AudioTranscriptionRequest request) - { - // In a real implementation, we might: - // 1. Use a language detection service on a sample of the audio - // 2. Check metadata - // 3. Use user preferences - // For now, default to English - return "en"; - } - - private Dictionary> InitializeLanguageScores() - { - // Initialize with some baseline scores - return new Dictionary>(StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/ConduitLLM.Core/Routing/AudioRoutingStrategies/LatencyBasedRoutingStrategy.cs b/ConduitLLM.Core/Routing/AudioRoutingStrategies/LatencyBasedRoutingStrategy.cs deleted file mode 100644 index 09fc1ec4d..000000000 --- a/ConduitLLM.Core/Routing/AudioRoutingStrategies/LatencyBasedRoutingStrategy.cs +++ /dev/null @@ -1,162 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing.AudioRoutingStrategies -{ - /// - /// Routes audio requests based on latency, selecting the fastest available provider. - /// - public class LatencyBasedRoutingStrategy : IAudioRoutingStrategy - { - private readonly ILogger _logger; - private readonly Dictionary> _latencyHistory = new(); - private readonly int _maxHistorySize; - - /// - public string Name => "LatencyBased"; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// Maximum number of latency samples to keep per provider. - public LatencyBasedRoutingStrategy( - ILogger logger, - int maxHistorySize = 100) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _maxHistorySize = maxHistorySize; - } - - /// - public Task SelectTranscriptionProviderAsync( - AudioTranscriptionRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - return SelectProviderByLatencyAsync( - availableProviders, - p => p.Capabilities.SupportsStreaming || !request.EnableStreaming, - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.AudioFormat.ToString())); - } - - /// - public Task SelectTextToSpeechProviderAsync( - TextToSpeechRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - return SelectProviderByLatencyAsync( - availableProviders, - p => p.Capabilities.SupportedVoices.Contains(request.Voice) || - p.Capabilities.SupportedVoices.Count() == 0, // Empty means all voices supported - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.ResponseFormat?.ToString())); - } - - /// - public Task UpdateMetricsAsync( - string provider, - AudioRequestMetrics metrics, - CancellationToken cancellationToken = default) - { - if (!_latencyHistory.ContainsKey(provider)) - { - _latencyHistory[provider] = new Queue(); - } - - var history = _latencyHistory[provider]; - history.Enqueue(metrics.LatencyMs); - - // Keep only recent samples - while (history.Count() > _maxHistorySize) - { - history.Dequeue(); - } - - _logger.LogDebug( - "Updated latency metrics for {Provider}: {Latency}ms (avg: {Average}ms)", - provider, - metrics.LatencyMs, - history.Average()); - - return Task.CompletedTask; - } - - private Task SelectProviderByLatencyAsync( - IReadOnlyList availableProviders, - params Func[] filters) - { - // Filter providers - var eligibleProviders = availableProviders - .Where(p => p.IsAvailable && filters.All(f => f(p))) - .ToList(); - - if (eligibleProviders.Count() == 0) - { - _logger.LogWarning("No eligible providers found for latency-based routing"); - return Task.FromResult(null); - } - - // Sort by latency (using both current metrics and historical data) - var sortedProviders = eligibleProviders - .Select(p => new - { - Provider = p, - EffectiveLatency = CalculateEffectiveLatency(p) - }) - .OrderBy(x => x.EffectiveLatency) - .ToList(); - - var selected = sortedProviders.First(); - - _logger.LogInformation( - "Selected {Provider} with effective latency {Latency}ms", - selected.Provider.Name, - selected.EffectiveLatency); - - return Task.FromResult(selected.Provider.Name); - } - - private double CalculateEffectiveLatency(AudioProviderInfo provider) - { - // Use current metrics as base - var baseLatency = provider.Metrics.AverageLatencyMs; - - // Adjust based on historical data if available - if (_latencyHistory.TryGetValue(provider.Name, out var history) && history.Count() > 0) - { - // Weight recent history more heavily - var historicalAvg = history.Average(); - baseLatency = (baseLatency * 0.3) + (historicalAvg * 0.7); - } - - // Penalize based on load and success rate - var loadPenalty = provider.Metrics.CurrentLoad * 100; // Up to 100ms penalty - var successPenalty = (1 - provider.Metrics.SuccessRate) * 200; // Up to 200ms penalty - - return baseLatency + loadPenalty + successPenalty; - } - - private bool SupportsLanguage(AudioProviderInfo provider, string? language) - { - if (string.IsNullOrEmpty(language)) - return true; - - return provider.Capabilities.SupportedLanguages.Count() == 0 || // Empty means all languages - provider.Capabilities.SupportedLanguages.Contains(language); - } - - private bool SupportsFormat(AudioProviderInfo provider, string? format) - { - if (string.IsNullOrEmpty(format)) - return true; - - return provider.Capabilities.SupportedFormats.Count() == 0 || // Empty means all formats - provider.Capabilities.SupportedFormats.Contains(format, StringComparer.OrdinalIgnoreCase); - } - } -} diff --git a/ConduitLLM.Core/Routing/AudioRoutingStrategies/QualityBasedRoutingStrategy.cs b/ConduitLLM.Core/Routing/AudioRoutingStrategies/QualityBasedRoutingStrategy.cs deleted file mode 100644 index 2e00c7236..000000000 --- a/ConduitLLM.Core/Routing/AudioRoutingStrategies/QualityBasedRoutingStrategy.cs +++ /dev/null @@ -1,347 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing.AudioRoutingStrategies -{ - /// - /// Routes audio requests to maximize quality, regardless of cost or latency. - /// - public class QualityBasedRoutingStrategy : IAudioRoutingStrategy - { - private readonly ILogger _logger; - private readonly Dictionary _providerQualityMetrics = new(); - - /// - public string Name => "QualityBased"; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - public QualityBasedRoutingStrategy(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public Task SelectTranscriptionProviderAsync( - AudioTranscriptionRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - return SelectProviderByQualityAsync( - availableProviders, - AudioRequestType.Transcription, - p => p.Capabilities.SupportsStreaming || !request.EnableStreaming, - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.AudioFormat?.ToString()), - p => HasAdequateDuration(p, request.AudioData?.Length ?? 0, request.AudioFormat ?? AudioFormat.Mp3)); - } - - /// - public Task SelectTextToSpeechProviderAsync( - TextToSpeechRequest request, - IReadOnlyList availableProviders, - CancellationToken cancellationToken = default) - { - // For TTS, voice quality is paramount - return SelectProviderByQualityAsync( - availableProviders, - AudioRequestType.TextToSpeech, - p => p.Capabilities.SupportedVoices.Contains(request.Voice) || - p.Capabilities.SupportedVoices.Count() == 0, - p => SupportsLanguage(p, request.Language), - p => SupportsFormat(p, request.ResponseFormat?.ToString()), - p => SupportsAdvancedFeatures(p, request)); - } - - /// - public Task UpdateMetricsAsync( - string provider, - AudioRequestMetrics metrics, - CancellationToken cancellationToken = default) - { - if (!_providerQualityMetrics.ContainsKey(provider)) - { - _providerQualityMetrics[provider] = new QualityMetrics(); - } - - var qualityMetrics = _providerQualityMetrics[provider]; - - // Update quality metrics based on success and user feedback - if (metrics.Success) - { - qualityMetrics.SuccessfulRequests++; - qualityMetrics.UpdateAverageConfidence(0.85); // Default confidence - } - else - { - qualityMetrics.FailedRequests++; - } - - qualityMetrics.LastUpdated = DateTime.UtcNow; - - return Task.CompletedTask; - } - - private Task SelectProviderByQualityAsync( - IReadOnlyList availableProviders, - AudioRequestType requestType, - params Func[] filters) - { - // Filter available providers - var eligibleProviders = availableProviders - .Where(p => p.IsAvailable && filters.All(f => f(p))) - .ToList(); - - if (eligibleProviders.Count() == 0) - { - _logger.LogWarning("No eligible providers found for quality-based routing"); - return Task.FromResult(null); - } - - // Calculate comprehensive quality score - var scoredProviders = eligibleProviders - .Select(p => new - { - Provider = p, - QualityScore = CalculateComprehensiveQualityScore(p, requestType) - }) - .OrderByDescending(x => x.QualityScore) - .ToList(); - - var selected = scoredProviders.First(); - - _logger.LogInformation( - "Selected {Provider} with quality score {Score:F2} for {RequestType}", - selected.Provider.Name, - selected.QualityScore, - requestType); - - // Log why this provider was chosen - LogQualityFactors(selected.Provider, requestType); - - return Task.FromResult(selected.Provider.Name); - } - - private double CalculateComprehensiveQualityScore(AudioProviderInfo provider, AudioRequestType requestType) - { - var baseQuality = provider.Capabilities.QualityScore / 100.0; - var successRate = provider.Metrics.SuccessRate; - - // Get historical quality metrics - var historicalQuality = 0.8; // Default - if (_providerQualityMetrics.TryGetValue(provider.Name, out var metrics)) - { - historicalQuality = metrics.GetQualityScore(); - } - - // Request-type specific adjustments - var typeMultiplier = requestType switch - { - AudioRequestType.Transcription => CalculateTranscriptionQualityMultiplier(provider), - AudioRequestType.TextToSpeech => CalculateTTSQualityMultiplier(provider), - AudioRequestType.Realtime => CalculateRealtimeQualityMultiplier(provider), - _ => 1.0 - }; - - // Feature richness bonus - var featureBonus = CalculateFeatureBonus(provider, requestType); - - // Combine all factors - var finalScore = (baseQuality * 0.3) + - (successRate * 0.2) + - (historicalQuality * 0.2) + - (typeMultiplier * 0.2) + - (featureBonus * 0.1); - - return Math.Min(1.0, finalScore); - } - - private double CalculateTranscriptionQualityMultiplier(AudioProviderInfo provider) - { - var multiplier = 1.0; - - // Bonus for supporting custom vocabulary - if (provider.Capabilities.SupportsCustomVocabulary) - multiplier += 0.1; - - // Bonus for real-time capability - if (provider.Capabilities.SupportsRealtime) - multiplier += 0.1; - - // Bonus for many supported languages - if (provider.Capabilities.SupportedLanguages.Count() > 50) - multiplier += 0.1; - - return Math.Min(1.3, multiplier); - } - - private double CalculateTTSQualityMultiplier(AudioProviderInfo provider) - { - var multiplier = 1.0; - - // Bonus for many voice options - var voiceCount = provider.Capabilities.SupportedVoices.Count(); - if (voiceCount > 100) - multiplier += 0.2; - else if (voiceCount > 50) - multiplier += 0.1; - - // No arbitrary provider bonuses - quality should be based on actual metrics - - return Math.Min(1.4, multiplier); - } - - private double CalculateRealtimeQualityMultiplier(AudioProviderInfo provider) - { - if (!provider.Capabilities.SupportsRealtime) - return 0.5; // Heavy penalty - - var multiplier = 1.0; - - // Bonus for low latency - if (provider.Metrics.AverageLatencyMs < 100) - multiplier += 0.2; - else if (provider.Metrics.AverageLatencyMs < 200) - multiplier += 0.1; - - return multiplier; - } - - private double CalculateFeatureBonus(AudioProviderInfo provider, AudioRequestType requestType) - { - var bonus = 0.0; - - // Format support - if (provider.Capabilities.SupportedFormats.Count() > 5) - bonus += 0.1; - - // Streaming support - if (provider.Capabilities.SupportsStreaming) - bonus += 0.1; - - // Low error rate in recent history - if (_providerQualityMetrics.TryGetValue(provider.Name, out var metrics)) - { - var errorRate = metrics.GetErrorRate(); - if (errorRate < 0.01) // Less than 1% errors - bonus += 0.2; - else if (errorRate < 0.05) // Less than 5% errors - bonus += 0.1; - } - - return bonus; - } - - private void LogQualityFactors(AudioProviderInfo provider, AudioRequestType requestType) - { - _logger.LogDebug( - "Quality factors for {Provider}: Base={Base:F2}, Success={Success:F2}, Features={Features}", - provider.Name, - provider.Capabilities.QualityScore, - provider.Metrics.SuccessRate, - string.Join(", ", GetProviderFeatures(provider))); - } - - private List GetProviderFeatures(AudioProviderInfo provider) - { - var features = new List(); - - if (provider.Capabilities.SupportsStreaming) - features.Add("Streaming"); - if (provider.Capabilities.SupportsRealtime) - features.Add("Realtime"); - if (provider.Capabilities.SupportsCustomVocabulary) - features.Add("CustomVocab"); - if (provider.Capabilities.SupportedLanguages.Count() > 30) - features.Add($"{provider.Capabilities.SupportedLanguages.Count()} Languages"); - if (provider.Capabilities.SupportedVoices.Count() > 20) - features.Add($"{provider.Capabilities.SupportedVoices.Count()} Voices"); - - return features; - } - - private bool SupportsLanguage(AudioProviderInfo provider, string? language) - { - if (string.IsNullOrEmpty(language)) - return true; - - return provider.Capabilities.SupportedLanguages.Count() == 0 || - provider.Capabilities.SupportedLanguages.Contains(language); - } - - private bool SupportsFormat(AudioProviderInfo provider, string? format) - { - if (string.IsNullOrEmpty(format)) - return true; - - return provider.Capabilities.SupportedFormats.Count() == 0 || - provider.Capabilities.SupportedFormats.Contains(format, StringComparer.OrdinalIgnoreCase); - } - - private bool HasAdequateDuration(AudioProviderInfo provider, int audioDataLength, AudioFormat format) - { - // Estimate duration and check against provider limits - var estimatedSeconds = EstimateAudioDuration(audioDataLength, format) * 60; - return estimatedSeconds <= provider.Capabilities.MaxAudioDurationSeconds; - } - - private bool SupportsAdvancedFeatures(AudioProviderInfo provider, TextToSpeechRequest request) - { - // Check if provider supports requested advanced features - // TODO: These capabilities should come from the database ModelCapabilities - // For now, assume all providers can handle the request unless proven otherwise - // through actual capability checks from the database - - return true; - } - - private double EstimateAudioDuration(int audioDataLength, AudioFormat format) - { - var bytesPerSecond = format switch - { - AudioFormat.Mp3 => 16000, - AudioFormat.Wav => 176400, - AudioFormat.Flac => 88200, - AudioFormat.Ogg => 12000, - AudioFormat.Opus => 6000, - _ => 16000 - }; - - return audioDataLength / (double)bytesPerSecond / 60.0; // Minutes - } - - private class QualityMetrics - { - public int SuccessfulRequests { get; set; } - public int FailedRequests { get; set; } - public double AverageConfidence { get; private set; } = 0.8; - public DateTime LastUpdated { get; set; } - - public void UpdateAverageConfidence(double newConfidence) - { - // Exponential moving average - AverageConfidence = (AverageConfidence * 0.9) + (newConfidence * 0.1); - } - - public double GetQualityScore() - { - var total = SuccessfulRequests + FailedRequests; - if (total == 0) return 0.8; // Default - - var successRate = SuccessfulRequests / (double)total; - return (successRate * 0.7) + (AverageConfidence * 0.3); - } - - public double GetErrorRate() - { - var total = SuccessfulRequests + FailedRequests; - if (total == 0) return 0; - return FailedRequests / (double)total; - } - } - } -} diff --git a/ConduitLLM.Core/Routing/DefaultLLMRouter.ChatCompletion.cs b/ConduitLLM.Core/Routing/DefaultLLMRouter.ChatCompletion.cs deleted file mode 100644 index 0d23734c7..000000000 --- a/ConduitLLM.Core/Routing/DefaultLLMRouter.ChatCompletion.cs +++ /dev/null @@ -1,434 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing -{ - /// - /// Chat completion functionality for the DefaultLLMRouter. - /// - public partial class DefaultLLMRouter - { - /// - public async Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? routingStrategy = null, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - // Validate parameters (minimal, provider-agnostic) if validator is available - _parameterValidator?.ValidateTextParameters(request); - - // Determine routing strategy - var strategy = DetermineRoutingStrategy(routingStrategy); - string? originalModelRequested = request.Model; - - _logger.LogDebug("Processing chat completion request using {Strategy} strategy", strategy); - - // Check for passthrough mode first - if (ShouldUsePassthroughMode(request, strategy)) - { - _logger.LogDebug("Using passthrough mode for model {Model}", request.Model); - return await DirectModelPassthroughAsync(request, apiKey, cancellationToken); - } - - // Otherwise use normal routing with retries - return await RouteThroughLoadBalancerAsync(request, originalModelRequested, strategy, apiKey, cancellationToken); - } - - /// - /// Determines if a request should be handled in passthrough mode. - /// - /// The chat completion request. - /// The routing strategy. - /// True if the request should be handled in passthrough mode, false otherwise. - private bool ShouldUsePassthroughMode(ChatCompletionRequest request, string strategy) - { - return !string.IsNullOrEmpty(request.Model) && - strategy.Equals("passthrough", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Directly passes the request to the specified model without routing. - /// - /// The chat completion request. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response. - private async Task DirectModelPassthroughAsync( - ChatCompletionRequest request, - string? apiKey, - CancellationToken cancellationToken) - { - // This is just a renamed version of HandlePassthroughRequestAsync for clarity - try - { - var client = _clientFactory.GetClient(request.Model); - return await client.CreateChatCompletionAsync(request, apiKey, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during pass-through to model {Model}", request.Model); - throw; - } - } - - /// - /// Routes a request through the load balancer with retry logic. - /// - /// The chat completion request. - /// The original model requested. - /// The routing strategy to use. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response. - /// Thrown when all attempts fail due to communication errors. - /// Thrown when no suitable model is available. - private async Task RouteThroughLoadBalancerAsync( - ChatCompletionRequest request, - string? originalModel, - string strategy, - string? apiKey, - CancellationToken cancellationToken) - { - List attemptedModels = new(); - var attemptContext = new AttemptContext(); - - // Attempt to execute the request with retries - var result = await ExecuteWithRetriesAsync( - request, - originalModel, - strategy, - attemptedModels, - attemptContext, - apiKey, - cancellationToken); - - if (result != null) - { - return result; - } - - // Handle the case where all attempts have failed - HandleFailedAttempts(attemptContext.LastException, originalModel, attemptedModels, attemptContext.AttemptCount); - - // This line will never be reached, but is required for compilation - throw new ModelUnavailableException( - $"No suitable model found for {originalModel} after {attemptContext.AttemptCount} attempts"); - } - - /// - /// Executes a chat completion request with retry logic and fallback handling. - /// - /// The chat completion request. - /// The original model name requested. - /// The routing strategy to use. - /// List of models that have already been attempted. - /// Context object holding attempt count and exception details. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response if successful, null otherwise. - /// - /// - /// This method handles the core retry logic for LLM requests, tracking attempted models - /// and managing backoff delays between attempts. It uses the - /// to keep track of the current state of the retry process. - /// - /// - /// For each retry attempt, the method: - /// 1. Updates the attempt count in the context - /// 2. Selects an appropriate model based on the routing strategy - /// 3. Executes the request with that model - /// 4. If successful, returns the result - /// 5. If unsuccessful but the error is recoverable, applies a delay and retries - /// 6. If the error is not recoverable or max retries reached, returns null - /// - /// - private async Task ExecuteWithRetriesAsync( - ChatCompletionRequest request, - string? originalModelRequested, - string strategy, - List attemptedModels, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - for (int retryAttempt = 1; retryAttempt <= _maxRetries; retryAttempt++) - { - // Update attempt counter in context - attemptContext.AttemptCount = retryAttempt; - - // Attempt the request execution with a specific model - var result = await TryRequestExecutionWithSelectedModelAsync( - request, - originalModelRequested, - strategy, - attemptedModels, - attemptContext, - apiKey, - cancellationToken); - - // If successful, return the result - if (result != null) - { - return result; - } - - // Check if we should continue retrying - if (ShouldStopRetrying(attemptContext, retryAttempt)) - { - break; - } - - // Apply backoff delay before next retry - await ApplyRetryDelayAsync(retryAttempt, cancellationToken); - } - - return null; - } - - /// - /// Attempts to execute a request with a selected model. - /// - private async Task TryRequestExecutionWithSelectedModelAsync( - ChatCompletionRequest request, - string? originalModelRequested, - string strategy, - List attemptedModels, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - // This method is a renamed version of AttemptRequestExecutionAsync for clarity - return await AttemptRequestExecutionAsync( - request, - originalModelRequested, - strategy, - attemptedModels, - attemptContext, - apiKey, - cancellationToken); - } - - /// - /// Attempts to execute a request with a dynamically selected model. - /// - /// The chat completion request. - /// The original model name requested. - /// The routing strategy to use. - /// List of models that have already been attempted. - /// Context object holding attempt count and exception tracking information. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response if successful, null otherwise. - /// - /// - /// This method represents a single attempt to execute a request during the retry process. - /// It selects an appropriate model based on the routing strategy and models that haven't - /// been tried yet, then attempts to execute the request with that model. - /// - /// - /// It updates the list to track which models have been tried, - /// which ensures we don't retry with the same model if it failed previously. - /// - /// - /// This method works with the to maintain state between retries, - /// but does not directly update the attempt count (that's managed by ). - /// - /// - private async Task AttemptRequestExecutionAsync( - ChatCompletionRequest request, - string? originalModelRequested, - string strategy, - List attemptedModels, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - // Check if request contains images and requires vision capabilities - bool containsImages = false; - - if (_capabilityDetector != null) - { - containsImages = _capabilityDetector.ContainsImageContent(request); - if (containsImages) - { - _logger.LogInformation("Request contains image content, selecting a vision-capable model"); - } - } - else - { - // Fallback check for images if capability detector isn't available - foreach (var message in request.Messages) - { - if (message.Content != null && message.Content is not string) - { - // Simple check for potential multimodal content - look for non-string content - containsImages = true; // If content is not a string, assume it might contain images - _logger.LogInformation("Request potentially contains non-text content (basic detection)"); - break; - } - } - } - - // Get the next model based on strategy, considering vision requirements - string? selectedModel = await SelectModelAsync( - originalModelRequested, - strategy, - attemptedModels, - cancellationToken, - containsImages); - - if (selectedModel == null) - { - if (containsImages) - { - _logger.LogWarning("No suitable vision-capable model found"); - attemptContext.LastException = new ModelUnavailableException( - "No suitable vision-capable model is available to process this request with image content"); - } - else - { - _logger.LogWarning("No suitable model found"); - } - return null; - } - - _logger.LogInformation("Selected model {ModelName} for request using {Strategy} strategy{VisionCapable}", - selectedModel, strategy, containsImages ? " (vision-capable)" : ""); - - // Add this model to the list of attempted ones - attemptedModels.Add(selectedModel); - - // Try to execute with the selected model - return await TryExecuteRequestAsync( - request, - selectedModel, - attemptContext, - apiKey, - cancellationToken); - } - - /// - /// Attempts to execute a chat completion request with a specific model and tracks any exceptions. - /// - /// The chat completion request. - /// The model to use for the request. - /// Context object to track attempts and capture exceptions. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response if successful, null otherwise. - /// - /// - /// This method performs the actual execution of the LLM request with a specific model. - /// It modifies the request's model property to use the selected model, then attempts to - /// execute the request. - /// - /// - /// If the execution succeeds, it returns the response. If it fails with an exception, - /// it stores the exception in the property - /// for analysis by the retry logic, marks the model as unhealthy if appropriate, - /// and returns null to indicate failure. - /// - /// - /// This method represents the innermost layer of the retry mechanism, with - /// and - /// providing the higher-level retry and model selection logic. - /// - /// - private async Task TryExecuteRequestAsync( - ChatCompletionRequest request, - string selectedModel, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - // Apply the selected model - request.Model = GetModelAliasForDeployment(selectedModel); - - try - { - return await ExecuteModelRequestAsync(request, selectedModel, apiKey, cancellationToken); - } - catch (Exception ex) - { - HandleExecutionException(ex, selectedModel); - attemptContext.LastException = ex; - return null; - } - } - - /// - /// Executes a request with the specified model and tracks metrics. - /// - /// The chat completion request with model set. - /// The model to use for tracking metrics. - /// Optional API key to use for the request. - /// Cancellation token. - /// The chat completion response. - private async Task ExecuteModelRequestAsync( - ChatCompletionRequest request, - string selectedModel, - string? apiKey, - CancellationToken cancellationToken) - { - // Track execution time for metrics - Stopwatch stopwatch = Stopwatch.StartNew(); - - try - { - // Get the client for this model and execute the request - var client = _clientFactory.GetClient(request.Model); - var result = await client.CreateChatCompletionAsync(request, apiKey, cancellationToken); - - stopwatch.Stop(); - - // Update model stats on success - UpdateModelStatistics(selectedModel, stopwatch.ElapsedMilliseconds); - - return result; - } - catch (Exception) - { - stopwatch.Stop(); - throw; // Re-throw to be handled by the caller - } - } - - /// - /// Handles the case where all attempts to execute a request have failed. - /// - /// The last exception that occurred. - /// The original model name requested. - /// List of models that were attempted. - /// The number of attempts that were made. - private void HandleFailedAttempts( - Exception? lastException, - string? originalModelRequested, - List attemptedModels, - int attemptCount) - { - if (lastException != null) - { - _logger.LogError(lastException, - "All attempts failed for model {OriginalModel} after trying {ModelCount} models with {AttemptCount} attempts", - originalModelRequested, attemptedModels.Count, attemptCount); - - throw new LLMCommunicationException( - $"Failed to process request after {attemptCount} attempts across {attemptedModels.Count} models", - lastException); - } - - throw new ModelUnavailableException( - $"No suitable model found for {originalModelRequested} after {attemptCount} attempts"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Routing/DefaultLLMRouter.Embedding.cs b/ConduitLLM.Core/Routing/DefaultLLMRouter.Embedding.cs deleted file mode 100644 index e1712c0ab..000000000 --- a/ConduitLLM.Core/Routing/DefaultLLMRouter.Embedding.cs +++ /dev/null @@ -1,448 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Routing; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing -{ - /// - /// Embedding functionality for the DefaultLLMRouter. - /// - public partial class DefaultLLMRouter - { - /// - public async Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? routingStrategy = null, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - // Determine routing strategy - var strategy = DetermineRoutingStrategy(routingStrategy); - string? originalModelRequested = request.Model; - - _logger.LogDebug("Processing embedding request using {Strategy} strategy", strategy); - - // Check for passthrough mode first - if (ShouldUsePassthroughModeForEmbedding(request, strategy)) - { - _logger.LogDebug("Using passthrough mode for embedding model {Model}", request.Model); - return await DirectEmbeddingPassthroughAsync(request, apiKey, cancellationToken); - } - - // Otherwise use normal routing with retries - return await RouteEmbeddingThroughLoadBalancerAsync(request, originalModelRequested, strategy, apiKey, cancellationToken); - } - - /// - /// Determines if an embedding request should be handled in passthrough mode. - /// - /// The embedding request. - /// The routing strategy. - /// True if the request should be handled in passthrough mode, false otherwise. - private bool ShouldUsePassthroughModeForEmbedding(EmbeddingRequest request, string strategy) - { - return !string.IsNullOrEmpty(request.Model) && - strategy.Equals("passthrough", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Directly passes the embedding request to the specified model without routing. - /// - /// The embedding request. - /// Optional API key to use for the request. - /// Cancellation token. - /// The embedding response. - private async Task DirectEmbeddingPassthroughAsync( - EmbeddingRequest request, - string? apiKey, - CancellationToken cancellationToken) - { - if (string.IsNullOrEmpty(request.Model)) - { - throw new ValidationException("Model must be specified for embedding requests in passthrough mode"); - } - - try - { - var client = _clientFactory.GetClient(request.Model); - return await client.CreateEmbeddingAsync(request, apiKey, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during embedding pass-through to model {Model}", request.Model); - throw; - } - } - - /// - /// Routes an embedding request through the load balancer with retry logic. - /// - /// The embedding request. - /// The original model requested. - /// The routing strategy to use. - /// Optional API key to use for the request. - /// Cancellation token. - /// The embedding response. - /// Thrown when all attempts fail due to communication errors. - /// Thrown when no suitable model is available. - private async Task RouteEmbeddingThroughLoadBalancerAsync( - EmbeddingRequest request, - string? originalModel, - string strategy, - string? apiKey, - CancellationToken cancellationToken) - { - List attemptedModels = new(); - var attemptContext = new AttemptContext(); - - // Attempt to execute the embedding request with retries - var result = await ExecuteEmbeddingWithRetriesAsync( - request, - originalModel, - strategy, - attemptedModels, - attemptContext, - apiKey, - cancellationToken); - - if (result != null) - { - return result; - } - - // Handle the case where all attempts have failed - HandleFailedEmbeddingAttempts(attemptContext.LastException, originalModel, attemptedModels, attemptContext.AttemptCount); - - // This line will never be reached, but is required for compilation - throw new ModelUnavailableException( - $"No suitable embedding model found for {originalModel} after {attemptContext.AttemptCount} attempts"); - } - - /// - /// Executes an embedding request with retry logic and fallback handling. - /// - /// The embedding request. - /// The original model name requested. - /// The routing strategy to use. - /// List of models that have already been attempted. - /// Context object holding attempt count and exception details. - /// Optional API key to use for the request. - /// Cancellation token. - /// The embedding response if successful, null otherwise. - private async Task ExecuteEmbeddingWithRetriesAsync( - EmbeddingRequest request, - string? originalModelRequested, - string strategy, - List attemptedModels, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - for (int retryAttempt = 1; retryAttempt <= _maxRetries; retryAttempt++) - { - // Update attempt counter in context - attemptContext.AttemptCount = retryAttempt; - - // Attempt the embedding request execution with a specific model - var result = await TryEmbeddingRequestExecutionWithSelectedModelAsync( - request, - originalModelRequested, - strategy, - attemptedModels, - attemptContext, - apiKey, - cancellationToken); - - // If successful, return the result - if (result != null) - { - return result; - } - - // Check if we should continue retrying - if (ShouldStopRetrying(attemptContext, retryAttempt)) - { - break; - } - - // Apply backoff delay before next retry - await ApplyRetryDelayAsync(retryAttempt, cancellationToken); - } - - return null; - } - - /// - /// Attempts to execute an embedding request with a selected model. - /// - private async Task TryEmbeddingRequestExecutionWithSelectedModelAsync( - EmbeddingRequest request, - string? originalModelRequested, - string strategy, - List attemptedModels, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - // Get the next model based on strategy, filtering for embedding-capable models - string? selectedModel = await SelectEmbeddingModelAsync( - originalModelRequested, - strategy, - attemptedModels, - cancellationToken); - - if (selectedModel == null) - { - _logger.LogWarning("No suitable embedding model found"); - attemptContext.LastException = new ModelUnavailableException( - "No suitable embedding model is available to process this request"); - return null; - } - - _logger.LogInformation("Selected embedding model {ModelName} for request using {Strategy} strategy", - selectedModel, strategy); - - // Add this model to the list of attempted ones - attemptedModels.Add(selectedModel); - - // Try to execute with the selected model - return await TryExecuteEmbeddingRequestAsync( - request, - selectedModel, - attemptContext, - apiKey, - cancellationToken); - } - - /// - /// Selects an appropriate model for embedding requests based on the routing strategy. - /// - /// The model name originally requested by the client. - /// The routing strategy to use for selection. - /// List of model names to exclude from consideration. - /// A token for cancelling the operation. - /// The name of the selected model, or null if no suitable model could be found. - private async Task SelectEmbeddingModelAsync( - string? requestedModel, - string strategy, - List excludeModels, - CancellationToken cancellationToken) - { - // Small delay to make this actually async - await Task.Delay(1, cancellationToken); - - // Get filtered list of available models that support embeddings - var (availableModels, availableDeployments) = await GetFilteredEmbeddingModelsAsync( - requestedModel, excludeModels, cancellationToken); - - if (availableModels.Count() == 0) - { - _logger.LogWarning("No available embedding models found for requestedModel={RequestedModel}", requestedModel); - return null; - } - - // Handle passthrough strategy as a special case - if (IsPassthroughStrategy(strategy)) - { - return availableModels.FirstOrDefault(); - } - - // Select model using the appropriate strategy - return SelectModelUsingStrategy(strategy, availableModels, availableDeployments, false); - } - - /// - /// Gets a filtered list of available models that support embeddings. - /// - private async Task<(List AvailableModels, Dictionary AvailableDeployments)> - GetFilteredEmbeddingModelsAsync(string? requestedModel, List excludeModels, CancellationToken cancellationToken) - { - // Add small delay to ensure method is truly async - await Task.Delay(1, cancellationToken); - - // Build candidate models list (same as regular routing) - var candidateModels = BuildCandidateModelsList(requestedModel, excludeModels); - - // Filter to only models that support embeddings (checking deployment SupportsEmbeddings or model capabilities) - var embeddingCapableModels = FilterEmbeddingCapableModels(candidateModels); - - // Filter to only healthy models - var availableModels = FilterHealthyModels(embeddingCapableModels); - - // Get deployment information for available models - var availableDeployments = GetAvailableDeployments(availableModels); - - return (availableModels, availableDeployments); - } - - /// - /// Filters a list of candidate models to include only those that support embeddings. - /// - /// The list of candidate model names. - /// A filtered list containing only embedding-capable models. - private List FilterEmbeddingCapableModels(List candidateModels) - { - return candidateModels - .Where(m => - { - // Check if the deployment supports embeddings - if (_modelDeployments.TryGetValue(m, out var deployment)) - { - // Use the SupportsEmbeddings property to determine capability - return deployment.SupportsEmbeddings; - } - - // If no deployment info, check if the model name suggests embedding capability - var modelLower = m.ToLower(); - return modelLower.Contains("embed") || modelLower.Contains("ada") || - modelLower.Contains("text-embedding") || modelLower.Contains("e5"); - }) - .ToList(); - } - - /// - /// Attempts to execute an embedding request with a specific model and tracks any exceptions. - /// - /// The embedding request. - /// The model to use for the request. - /// Context object to track attempts and capture exceptions. - /// Optional API key to use for the request. - /// Cancellation token. - /// The embedding response if successful, null otherwise. - private async Task TryExecuteEmbeddingRequestAsync( - EmbeddingRequest request, - string selectedModel, - AttemptContext attemptContext, - string? apiKey, - CancellationToken cancellationToken) - { - // Apply the selected model - request.Model = GetModelAliasForDeployment(selectedModel); - - try - { - return await ExecuteEmbeddingModelRequestAsync(request, selectedModel, apiKey, cancellationToken); - } - catch (Exception ex) - { - HandleEmbeddingExecutionException(ex, selectedModel); - attemptContext.LastException = ex; - return null; - } - } - - /// - /// Executes an embedding request with the specified model and tracks metrics. - /// - /// The embedding request with model set. - /// The model to use for tracking metrics. - /// Optional API key to use for the request. - /// Cancellation token. - /// The embedding response. - private async Task ExecuteEmbeddingModelRequestAsync( - EmbeddingRequest request, - string selectedModel, - string? apiKey, - CancellationToken cancellationToken) - { - // Check cache first if available - string? cacheKey = null; - if (_embeddingCache?.IsAvailable == true) - { - cacheKey = _embeddingCache.GenerateCacheKey(request); - var cachedResponse = await _embeddingCache.GetEmbeddingAsync(cacheKey); - if (cachedResponse != null) - { - _logger.LogDebug("Cache hit for embedding request with model {Model}", selectedModel); - // Still update model stats for cache hits to track usage - UpdateModelStatistics(selectedModel, 0); // 0ms latency for cache hits - return cachedResponse; - } - } - - // Track execution time for metrics - Stopwatch stopwatch = Stopwatch.StartNew(); - - try - { - // Get the client for this model and execute the request - var client = _clientFactory.GetClient(request.Model!); - var result = await client.CreateEmbeddingAsync(request, apiKey, cancellationToken); - - stopwatch.Stop(); - - // Update model stats on success - UpdateModelStatistics(selectedModel, stopwatch.ElapsedMilliseconds); - - // Cache the result if caching is available - if (_embeddingCache?.IsAvailable == true && cacheKey != null) - { - try - { - await _embeddingCache.SetEmbeddingAsync(cacheKey, result); - _logger.LogDebug("Cached embedding response for model {Model}", selectedModel); - } - catch (Exception cacheEx) - { - _logger.LogWarning(cacheEx, "Failed to cache embedding response for model {Model}", selectedModel); - } - } - - return result; - } - catch (Exception) - { - stopwatch.Stop(); - throw; // Re-throw to be handled by the caller - } - } - - /// - /// Handles exceptions that occur during embedding request execution. - /// - /// The exception that occurred. - /// The model that was used. - private void HandleEmbeddingExecutionException(Exception exception, string selectedModel) - { - _logger.LogWarning(exception, "Embedding request to model {ModelName} failed", - selectedModel); - } - - /// - /// Handles the case where all attempts to execute an embedding request have failed. - /// - /// The last exception that occurred. - /// The original model name requested. - /// List of models that were attempted. - /// The number of attempts that were made. - private void HandleFailedEmbeddingAttempts( - Exception? lastException, - string? originalModelRequested, - List attemptedModels, - int attemptCount) - { - if (lastException != null) - { - _logger.LogError(lastException, - "All embedding attempts failed for model {OriginalModel} after trying {ModelCount} models with {AttemptCount} attempts", - originalModelRequested, attemptedModels.Count(), attemptCount); - - throw new LLMCommunicationException( - $"Failed to process embedding request after {attemptCount} attempts across {attemptedModels.Count()} models", - lastException); - } - - throw new ModelUnavailableException( - $"No suitable embedding model found for {originalModelRequested} after {attemptCount} attempts"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Routing/DefaultLLMRouter.ModelSelection.cs b/ConduitLLM.Core/Routing/DefaultLLMRouter.ModelSelection.cs deleted file mode 100644 index f4fc912f2..000000000 --- a/ConduitLLM.Core/Routing/DefaultLLMRouter.ModelSelection.cs +++ /dev/null @@ -1,243 +0,0 @@ -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Routing.Strategies; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing -{ - /// - /// Model selection and routing strategy functionality for the DefaultLLMRouter. - /// - public partial class DefaultLLMRouter - { - /// - /// Selects the most appropriate model based on the specified strategy and current system state. - /// - /// The model name originally requested by the client, or null if no specific model was requested. - /// The routing strategy to use for selection (e.g., "simple", "roundrobin", "leastcost"). - /// List of model names to exclude from consideration (typically models that have already been attempted). - /// A token for cancelling the operation. - /// Set to true if the request contains images and requires a vision-capable model. - /// The name of the selected model, or null if no suitable model could be found. - /// - /// This method implements the core model selection logic: - /// - /// 1. Builds a candidate list based on the requested model and available fallbacks - /// 2. Filters out excluded models and unhealthy models - /// 3. Gets the appropriate strategy from the factory - /// 4. Delegates model selection to the strategy implementation - /// - /// If no specific model was requested, it will consider all available models. - /// If the strategy is not recognized, it defaults to the "simple" strategy. - /// - private async Task SelectModelAsync( - string? requestedModel, - string strategy, - List excludeModels, - CancellationToken cancellationToken, - bool visionRequest = false) - { - // Small delay to make this actually async - await Task.Delay(1, cancellationToken); - - // Get filtered list of available models - var (availableModels, availableDeployments) = await GetFilteredAvailableModelsAsync( - requestedModel, excludeModels, cancellationToken); - - if (availableModels.Count() == 0) - { - _logger.LogWarning("No available models found for requestedModel={RequestedModel}", requestedModel); - return null; - } - - // Handle passthrough strategy as a special case - if (IsPassthroughStrategy(strategy)) - { - // Even in passthrough mode, we need to check vision capability if required - if (visionRequest && _capabilityDetector != null) - { - var firstModel = availableModels.FirstOrDefault(); - if (firstModel != null) - { - string modelAlias = GetModelAliasForDeployment(firstModel); - if (!_capabilityDetector.HasVisionCapability(modelAlias)) - { - _logger.LogWarning("Requested model {Model} does not support vision capabilities required by this request", - modelAlias); - return null; - } - } - } - return availableModels.FirstOrDefault(); - } - - // Select model using the appropriate strategy, filtering for vision-capable models if needed - return SelectModelUsingStrategy(strategy, availableModels, availableDeployments, visionRequest); - } - - /// - /// Gets a filtered list of available models based on requested model and exclusions. - /// - private async Task<(List AvailableModels, Dictionary AvailableDeployments)> - GetFilteredAvailableModelsAsync(string? requestedModel, List excludeModels, CancellationToken cancellationToken) - { - // Add small delay to ensure method is truly async - await Task.Delay(1, cancellationToken); - - // Build candidate models list - var candidateModels = BuildCandidateModelsList(requestedModel, excludeModels); - - // Filter to only healthy models - var availableModels = FilterHealthyModels(candidateModels); - - // Get deployment information for available models - var availableDeployments = GetAvailableDeployments(availableModels); - - return (availableModels, availableDeployments); - } - - /// - /// Determines if the strategy is a passthrough strategy. - /// - private bool IsPassthroughStrategy(string strategy) - { - return strategy.Equals("passthrough", StringComparison.OrdinalIgnoreCase); - } - - /// - /// Selects a model using the appropriate strategy implementation. - /// - private string? SelectModelUsingStrategy( - string strategy, - List availableModels, - Dictionary availableDeployments, - bool visionRequired = false) - { - // If vision is required, filter models to only vision-capable ones - var candidateModels = availableModels; - if (visionRequired && _capabilityDetector != null) - { - candidateModels = availableModels - .Where(model => _capabilityDetector.HasVisionCapability(GetModelAliasForDeployment(model))) - .ToList(); - - if (candidateModels.Count() == 0) - { - _logger.LogWarning("No vision-capable models available from the {Count} candidate models", - availableModels.Count()); - return null; - } - - _logger.LogInformation("Found {Count} vision-capable models out of {TotalCount} candidates", - candidateModels.Count(), availableModels.Count()); - } - - // Use the strategy factory to get the appropriate strategy and delegate selection - var modelSelectionStrategy = ModelSelectionStrategyFactory.GetStrategy(strategy); - - _logger.LogDebug("Using {Strategy} strategy to select from {ModelCount} models", - strategy, candidateModels.Count()); - - return modelSelectionStrategy.SelectModel( - candidateModels, - availableDeployments.Where(kv => candidateModels.Contains(kv.Key)) - .ToDictionary(kv => kv.Key, kv => kv.Value), - _modelUsageCount); - } - - /// - /// Builds a list of candidate models based on the requested model and available fallbacks. - /// - /// The model name originally requested by the client. - /// List of model names to exclude from consideration. - /// A list of candidate model names. - private List BuildCandidateModelsList(string? requestedModel, List excludeModels) - { - List candidateModels = new(); - - // If we have a specific requested model and it's not in the excluded list, start with that - if (!string.IsNullOrEmpty(requestedModel) && !excludeModels.Contains(requestedModel)) - { - // Find any deployments that correspond to this model alias - var matchingDeployments = _modelDeployments.Values - .Where(d => d.ModelAlias.Equals(requestedModel, StringComparison.OrdinalIgnoreCase)) - .Select(d => d.DeploymentName) - .ToList(); - - if (matchingDeployments.Count() > 0) - { - candidateModels.AddRange(matchingDeployments); - } - else - { - // No matching deployments, treat as deployment name directly - candidateModels.Add(requestedModel); - } - - // Add fallbacks for this model if available - if (_fallbackModels.TryGetValue(requestedModel, out var fallbacks)) - { - candidateModels.AddRange(fallbacks.Where(m => !excludeModels.Contains(m))); - } - } - - // If no candidates yet, use all available models - if (candidateModels.Count() == 0) - { - candidateModels = _modelDeployments.Keys - .Where(m => !excludeModels.Contains(m)) - .ToList(); - } - - return candidateModels; - } - - /// - /// Filters a list of candidate models (currently no filtering applied). - /// - /// The list of candidate model names. - /// The same list of candidate models. - private List FilterHealthyModels(List candidateModels) - { - // Health filtering has been removed - all models are considered available - return candidateModels; - } - - /// - /// Converts a list of model names to a dictionary of their deployment information. - /// - /// The list of model names to convert. - /// A dictionary mapping model names to their deployment information. - private Dictionary GetAvailableDeployments(List modelNames) - { - return modelNames - .Where(m => _modelDeployments.ContainsKey(m)) - .ToDictionary( - m => m, - m => _modelDeployments[m], - StringComparer.OrdinalIgnoreCase); - } - - /// - /// Determines the routing strategy to use based on input and defaults. - /// - /// The strategy requested, or null to use default. - /// The strategy name to use for routing. - private string DetermineRoutingStrategy(string? requestedStrategy) - { - return requestedStrategy ?? _defaultRoutingStrategy; - } - - /// - /// Gets the model alias to use with the client for a deployment - /// - private string GetModelAliasForDeployment(string deploymentName) - { - if (_modelDeployments.TryGetValue(deploymentName, out var deployment)) - { - return deployment.ModelAlias; - } - return deploymentName; // Fallback to the deployment name if not found - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Routing/DefaultLLMRouter.Streaming.cs b/ConduitLLM.Core/Routing/DefaultLLMRouter.Streaming.cs deleted file mode 100644 index 77635eb71..000000000 --- a/ConduitLLM.Core/Routing/DefaultLLMRouter.Streaming.cs +++ /dev/null @@ -1,242 +0,0 @@ -using System.Diagnostics; -using System.Runtime.CompilerServices; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing -{ - /// - /// Streaming functionality for the DefaultLLMRouter. - /// - public partial class DefaultLLMRouter - { - /// - public async IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? routingStrategy = null, - string? apiKey = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - if (request == null) - { - throw new ArgumentNullException(nameof(request)); - } - - // We need to handle streaming differently due to yield return limitations - var strategy = routingStrategy ?? _defaultRoutingStrategy; - - // First, select the appropriate model - string selectedModel = await SelectModelForStreamingRequestAsync(request, strategy, cancellationToken); - - // Update the request with the selected model - request.Model = GetModelAliasForDeployment(selectedModel); - - // Process the streaming request - await foreach (var chunk in ProcessStreamingRequestAsync(request, selectedModel, apiKey, cancellationToken)) - { - yield return chunk; - } - } - - /// - /// Selects the appropriate model for a streaming request. - /// - /// The chat completion request. - /// The routing strategy to use. - /// Cancellation token. - /// The selected model name. - /// Thrown when no suitable model is found. - private async Task SelectModelForStreamingRequestAsync( - ChatCompletionRequest request, - string strategy, - CancellationToken cancellationToken) - { - string? modelToUse = null; - - // Check if request contains images and requires vision capabilities - bool containsImages = false; - if (_capabilityDetector != null) - { - containsImages = _capabilityDetector.ContainsImageContent(request); - if (containsImages) - { - _logger.LogInformation("Streaming request contains image content, selecting a vision-capable model"); - } - } - else - { - // Fallback check if capability detector is not available - foreach (var message in request.Messages) - { - if (message.Content != null && message.Content is not string) - { - containsImages = true; // If content is not a string, assume it might contain images - if (containsImages) - { - _logger.LogInformation("Streaming request potentially contains image content (basic detection)"); - break; - } - } - } - } - - // If we're using a passthrough strategy and have a model, just use it directly - if (!string.IsNullOrEmpty(request.Model) && - strategy.Equals("passthrough", StringComparison.OrdinalIgnoreCase)) - { - modelToUse = request.Model; - - // Still need to check if the passthrough model supports vision if needed - if (containsImages && _capabilityDetector != null && - !_capabilityDetector.HasVisionCapability(modelToUse)) - { - throw new ModelUnavailableException( - $"Model {request.Model} does not support vision capabilities required by this streaming request"); - } - } - else - { - // Otherwise, select a model using our routing logic - modelToUse = await SelectModelForStreamingAsync( - request.Model, strategy, _maxRetries, cancellationToken, containsImages); - - if (modelToUse == null) - { - if (containsImages) - { - throw new ModelUnavailableException( - $"No suitable vision-capable model found for streaming request with original model {request.Model}"); - } - else - { - throw new ModelUnavailableException( - $"No suitable model found for streaming request with original model {request.Model}"); - } - } - } - - return modelToUse; - } - - /// - /// Processes a streaming request with the selected model. - /// - /// The chat completion request with the model already set. - /// The selected model name for metrics and health tracking. - /// Optional API key to use for the request. - /// Cancellation token. - /// An async enumerable of chat completion chunks. - /// Thrown when streaming fails or returns no chunks. - private async IAsyncEnumerable ProcessStreamingRequestAsync( - ChatCompletionRequest request, - string selectedModel, - string? apiKey, - [EnumeratorCancellation] CancellationToken cancellationToken) - { - _logger.LogInformation("Streaming from model {ModelName}", selectedModel); - - var client = _clientFactory.GetClient(request.Model); - IAsyncEnumerable stream; - Stopwatch stopwatch = Stopwatch.StartNew(); - - try - { - // Get the stream outside of the yield section - stream = client.StreamChatCompletionAsync(request, apiKey, cancellationToken); - } - catch (Exception ex) - { - // Handle exceptions during stream creation - _logger.LogError(ex, "Error creating stream from model {ModelName}", selectedModel); - throw; - } - - // Now iterate through the stream - bool receivedAnyChunks = false; - - // We can't use try-catch here, so we'll handle errors at a higher level - await foreach (var chunk in stream.WithCancellation(cancellationToken)) - { - receivedAnyChunks = true; - yield return chunk; - } - - stopwatch.Stop(); - - // After streaming completes, update model statistics - if (receivedAnyChunks) - { - // Success case - update metrics - UpdateModelStatistics(selectedModel, stopwatch.ElapsedMilliseconds); - } - else - { - // No chunks received - update health status - throw new LLMCommunicationException($"No chunks received from model {selectedModel}"); - } - } - - /// - /// Select a model for streaming, handling retries and fallbacks - /// - /// The model name originally requested by the client, or null if no specific model was requested. - /// The routing strategy to use for selection. - /// Maximum number of retry attempts. - /// A token for cancelling the operation. - /// If true, only vision-capable models will be considered. - /// The name of the selected model, or null if no suitable model could be found. - /// - /// This method specifically handles model selection for streaming requests, with retry logic - /// to ensure a healthy model is selected. It reuses the core SelectModelAsync method, - /// which delegates to the strategy pattern implementation. - /// - private async Task SelectModelForStreamingAsync( - string? requestedModel, - string strategy, - int maxRetries, - CancellationToken cancellationToken, - bool visionRequired = false) - { - // Start tracking retry attempt count - int attemptCount = 0; - List attemptedModels = new(); - - while (attemptCount <= maxRetries) - { - attemptCount++; - - // Select a model based on strategy using the same SelectModelAsync method - // that now delegates to our strategy pattern, with vision requirements if needed - string? selectedModel = await SelectModelAsync( - requestedModel, - strategy, - attemptedModels, - cancellationToken, - visionRequired); - - if (selectedModel == null) - { - string visionMessage = visionRequired ? " vision-capable" : ""; - _logger.LogWarning("No suitable{VisionMessage} model found for streaming after {AttemptsCount} attempts", - visionMessage, attemptCount); - break; - } - - _logger.LogInformation("Selected model {ModelName} for streaming using {Strategy} strategy{VisionMessage}", - selectedModel, strategy, visionRequired ? " (vision-capable)" : ""); - - // Add this model to attempted list - attemptedModels.Add(selectedModel); - - // Health checking removed - always use the selected model - return selectedModel; - } - - // No suitable model found - return null; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Routing/DefaultLLMRouter.cs b/ConduitLLM.Core/Routing/DefaultLLMRouter.cs deleted file mode 100644 index 54324f6a9..000000000 --- a/ConduitLLM.Core/Routing/DefaultLLMRouter.cs +++ /dev/null @@ -1,468 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Validation; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Routing -{ - /// - /// Context class for tracking attempt information during request retry logic in the LLM router. - /// - /// - /// - /// The AttemptContext class encapsulates the mutable state used during the retry process - /// when executing LLM requests. This includes the current attempt count and the last exception - /// that occurred during request processing. - /// - /// - /// This class was introduced to replace ref parameters in async methods, as C# does not allow - /// ref parameters in async methods. Using this context object allows for cleaner and more - /// maintainable code while preserving the state across multiple retry attempts. - /// - /// - /// The router's retry logic uses this context to track how many attempts have been made - /// and what errors have occurred, allowing for intelligent decisions about whether to - /// retry a request, use a fallback model, or fail with an appropriate error message. - /// - /// - /// - /// - /// - public class AttemptContext - { - /// - /// Gets or sets the current attempt count for a request execution. - /// - /// - /// This counter starts at 0 and is incremented for each retry attempt. - /// The router uses this value to determine when the maximum number of retries - /// has been reached and to calculate the appropriate backoff delay between retries. - /// - public int AttemptCount { get; set; } - - /// - /// Gets or sets the last exception encountered during request attempts. - /// - /// - /// This property stores the most recent exception that occurred during request execution. - /// It's used by the router to determine whether the error is recoverable and should be - /// retried, or if it's a permanent failure that should be reported to the caller. - /// If multiple attempts fail, this will contain the exception from the most recent attempt. - /// - public Exception? LastException { get; set; } - - /// - /// Creates a new instance of AttemptContext with default values. - /// - /// - /// Initializes a new context with attempt count set to 0 and no last exception. - /// This represents the state before any execution attempts have been made. - /// - public AttemptContext() - { - AttemptCount = 0; - LastException = null; - } - } - - /// - /// Default implementation of the LLM router with multiple routing strategies, - /// load balancing, health checking, and fallback support. - /// - /// - /// The DefaultLLMRouter provides sophisticated request routing capabilities for LLM requests: - /// - /// - Multiple routing strategies (simple, round-robin, least cost, etc.) - /// - Automatic health checking and unhealthy model avoidance - /// - Fallback support for handling model failures - /// - Retry logic with exponential backoff for recoverable errors - /// - Real-time metrics tracking for models (usage count, latency, etc.) - /// - /// The router maintains an internal registry of model deployments and their current - /// health status, and can automatically route requests to the most appropriate - /// model based on the selected strategy. - /// - /// This class is split into partial classes for better organization: - /// - DefaultLLMRouter.cs (core infrastructure and utilities) - /// - DefaultLLMRouter.ChatCompletion.cs (chat completion functionality) - /// - DefaultLLMRouter.Streaming.cs (streaming functionality) - /// - DefaultLLMRouter.Embedding.cs (embedding functionality) - /// - DefaultLLMRouter.ModelSelection.cs (model selection and routing strategies) - /// - public partial class DefaultLLMRouter : ILLMRouter - { - private readonly ILLMClientFactory _clientFactory; - private readonly ILogger _logger; - private readonly IModelCapabilityDetector? _capabilityDetector; - private readonly IEmbeddingCache? _embeddingCache; - private readonly MinimalParameterValidator? _parameterValidator; - - - /// - /// Maps primary models to their list of fallback models - /// - private readonly ConcurrentDictionary> _fallbackModels = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Tracks the usage count of each model for load balancing purposes - /// - private readonly ConcurrentDictionary _modelUsageCount = new(StringComparer.OrdinalIgnoreCase); - - /// - /// Stores the model deployment information for all registered models - /// - private readonly ConcurrentDictionary _modelDeployments = new(StringComparer.OrdinalIgnoreCase); - - private readonly Random _random = new(); - private readonly object _lockObject = new(); - - private string _defaultRoutingStrategy = "simple"; - private int _maxRetries = 3; - private int _retryBaseDelayMs = 500; - private int _retryMaxDelayMs = 10000; - - /// - /// Creates a new DefaultLLMRouter instance - /// - /// Factory for creating LLM clients - /// Logger instance - /// Optional detector for model capabilities like vision support - /// Optional cache for embedding responses - /// Optional validator for request parameters - public DefaultLLMRouter( - ILLMClientFactory clientFactory, - ILogger logger, - IModelCapabilityDetector? capabilityDetector = null, - IEmbeddingCache? embeddingCache = null, - MinimalParameterValidator? parameterValidator = null) - { - _clientFactory = clientFactory ?? throw new ArgumentNullException(nameof(clientFactory)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _capabilityDetector = capabilityDetector; - _embeddingCache = embeddingCache; - _parameterValidator = parameterValidator; - } - - /// - /// Creates a new DefaultLLMRouter instance with the specified configuration - /// - /// Factory for creating LLM clients - /// Logger instance - /// Router configuration - /// Optional detector for model capabilities like vision support - /// Optional cache for embedding responses - /// Optional validator for request parameters - public DefaultLLMRouter( - ILLMClientFactory clientFactory, - ILogger logger, - RouterConfig config, - IModelCapabilityDetector? capabilityDetector = null, - IEmbeddingCache? embeddingCache = null, - MinimalParameterValidator? parameterValidator = null) - : this(clientFactory, logger, capabilityDetector, embeddingCache, parameterValidator) - { - Initialize(config); - } - - /// - /// Initializes the router with the specified configuration - /// - /// Router configuration - public void Initialize(RouterConfig config) - { - if (config == null) - { - throw new ArgumentNullException(nameof(config)); - } - - _logger.LogInformation("Initializing router with {ModelCount} deployments", - config.ModelDeployments?.Count ?? 0); - - // Set router configuration values - _defaultRoutingStrategy = config.DefaultRoutingStrategy; - _maxRetries = config.MaxRetries; - _retryBaseDelayMs = config.RetryBaseDelayMs; - _retryMaxDelayMs = config.RetryMaxDelayMs; - - // Clear existing deployment information - _modelDeployments.Clear(); - _fallbackModels.Clear(); - - // Load model deployments - if (config.ModelDeployments != null) - { - foreach (var deployment in config.ModelDeployments) - { - if (string.IsNullOrWhiteSpace(deployment.DeploymentName) || - string.IsNullOrWhiteSpace(deployment.ModelAlias)) - { - _logger.LogWarning("Skipping deployment with missing name or model alias"); - continue; - } - - _modelDeployments[deployment.DeploymentName] = deployment; - _logger.LogInformation("Added model deployment {DeploymentName} for model {ModelAlias}", - deployment.DeploymentName, deployment.ModelAlias); - } - } - - // Set up fallbacks - if (config.Fallbacks != null) - { - foreach (var fallbackEntry in config.Fallbacks) - { - if (string.IsNullOrWhiteSpace(fallbackEntry.Key) || fallbackEntry.Value == null) - { - continue; - } - - AddFallbackModels(fallbackEntry.Key, fallbackEntry.Value); - } - } - - _logger.LogInformation("Router initialized with {DeploymentCount} deployments and {FallbackCount} fallback configurations", - _modelDeployments.Count, _fallbackModels.Count); - } - - // Chat completion methods implemented in DefaultLLMRouter.ChatCompletion.cs - // Streaming methods implemented in DefaultLLMRouter.Streaming.cs - // Embedding methods implemented in DefaultLLMRouter.Embedding.cs - // Model selection methods implemented in DefaultLLMRouter.ModelSelection.cs - - /// - public Task> GetAvailableModelDetailsAsync( - CancellationToken cancellationToken = default) - { - // Construct ModelInfo list from the internal _modelDeployments dictionary - IReadOnlyList modelInfos = _modelDeployments.Values - .Where(d => d.IsEnabled) // Optionally filter only enabled deployments - .Select(deployment => new ModelInfo - { - // Map ModelDeployment properties to ModelInfo properties - Id = deployment.ModelName, // Use ModelName (deployment name) as the ID - OwnedBy = deployment.ProviderName, // Use ProviderName as OwnedBy - MaxContextTokens = null // Context window info not directly in ModelDeployment - // Could potentially be fetched from client or config if needed - // Object property defaults to "model" in ModelInfo class - }) - .ToList(); - - _logger.LogInformation("Retrieved {Count} available model details.", modelInfos.Count); - - // Return as a completed task since the operation is synchronous - return Task.FromResult>(modelInfos); - } - - /// - public IReadOnlyList GetAvailableModels() - { - // Return all registered deployments - return _modelDeployments.Keys.ToList(); - } - - /// - public IReadOnlyList GetFallbackModels(string modelName) - { - if (_fallbackModels.TryGetValue(modelName, out var fallbacks)) - { - return fallbacks.ToList(); - } - return Array.Empty(); - } - - - /// - /// Add fallback models for a primary model - /// - /// The primary model name - /// List of fallback model names - public void AddFallbackModels(string primaryModel, IEnumerable fallbacks) - { - if (string.IsNullOrEmpty(primaryModel) || fallbacks == null) - { - return; - } - - _fallbackModels[primaryModel] = new List(fallbacks); - _logger.LogInformation("Added {FallbackCount} fallback models for {PrimaryModel}", - _fallbackModels[primaryModel].Count, primaryModel); - } - - /// - /// Removes fallbacks for a primary model - /// - /// The primary model name - public void RemoveFallbacks(string primaryModel) - { - if (_fallbackModels.TryRemove(primaryModel, out _)) - { - _logger.LogInformation("Removed fallbacks for {PrimaryModel}", primaryModel); - } - } - - /// - /// Reset usage statistics for all models - /// - public void ResetUsageStatistics() - { - _modelUsageCount.Clear(); - - // Reset usage metrics in model deployments - foreach (var deployment in _modelDeployments.Values) - { - deployment.RequestCount = 0; - deployment.LastUsed = DateTime.MinValue; - } - - _logger.LogInformation("Reset all model usage statistics"); - } - - #region Shared Utility Methods - - /// - /// Determines if retry attempts should stop based on the error type and retry count. - /// - private bool ShouldStopRetrying(AttemptContext attemptContext, int retryAttempt) - { - // If this is a non-recoverable error, don't retry - if (!IsRecoverableError(attemptContext.LastException)) - { - _logger.LogWarning("Non-recoverable error encountered, stopping retry attempts"); - return true; - } - - // If this is the last retry, don't continue - if (retryAttempt >= _maxRetries) - { - _logger.LogWarning("Maximum retry attempts reached"); - return true; - } - - return false; - } - - /// - /// Applies a delay before the next retry attempt. - /// - /// The current attempt count. - /// Cancellation token. - /// A task representing the asynchronous delay operation. - private async Task ApplyRetryDelayAsync(int attemptCount, CancellationToken cancellationToken) - { - int delayMs = CalculateBackoffDelay(attemptCount); - - _logger.LogInformation( - "Retrying request in {DelayMs}ms (attempt {CurrentAttempt}/{MaxRetries})", - delayMs, attemptCount, _maxRetries); - - await Task.Delay(delayMs, cancellationToken); - } - - /// - /// Handles exceptions that occur during request execution. - /// - /// The exception that occurred. - /// The model that was used. - private void HandleExecutionException(Exception exception, string selectedModel) - { - _logger.LogWarning(exception, "Request to model {ModelName} failed", - selectedModel); - } - - /// - /// Updates all statistics for a model after successful request completion. - /// - /// The name of the model. - /// The request latency in milliseconds. - private void UpdateModelStatistics(string modelName, long latencyMs) - { - IncrementModelUsage(modelName); - UpdateModelLatency(modelName, latencyMs); - } - - /// - /// Increments the usage count for a model - /// - private void IncrementModelUsage(string modelName) - { - _modelUsageCount.AddOrUpdate( - modelName, - 1, - (_, count) => count + 1); - - // Update the model deployment if available - if (_modelDeployments.TryGetValue(modelName, out var deployment)) - { - deployment.RequestCount++; - deployment.LastUsed = DateTime.UtcNow; - } - } - - /// - /// Updates the latency statistics for a model - /// - private void UpdateModelLatency(string modelName, long latencyMs) - { - if (_modelDeployments.TryGetValue(modelName, out var deployment)) - { - // Calculate running average - if (deployment.RequestCount <= 1) - { - deployment.AverageLatencyMs = latencyMs; - } - else - { - // Simple exponential moving average with 0.1 weight for new value - deployment.AverageLatencyMs = (0.9 * deployment.AverageLatencyMs) + (0.1 * latencyMs); - } - } - } - - /// - /// Calculates the backoff delay for retries using exponential backoff - /// - private int CalculateBackoffDelay(int attemptCount) - { - // Calculate exponential backoff with jitter - double backoffFactor = Math.Pow(2, attemptCount - 1); - int baseDelay = (int)(_retryBaseDelayMs * backoffFactor); - int jitter = _random.Next(0, baseDelay / 4); - int delay = baseDelay + jitter; - - // Cap at max delay - return Math.Min(delay, _retryMaxDelayMs); - } - - /// - /// Determines if an error is recoverable (should be retried) - /// - private bool IsRecoverableError(Exception? ex) - { - // If exception is null, treat it as non-recoverable - if (ex == null) - { - return false; - } - - // Categorize exception types as recoverable or not - // Some errors should not be retried as they will always fail (e.g., validation errors) - return ex switch - { - LLMCommunicationException => true, // Network errors can be retried - TimeoutException => true, // Timeouts can be retried - OperationCanceledException => false, // Cancellations should not be retried - ArgumentException => false, // Invalid arguments won't be fixed by retry - ConfigurationException => false, // Configuration issues won't be fixed by retry - InvalidOperationException => false, // Logical errors won't be fixed by retry - _ => true // Default to retry for unknown exceptions - }; - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Routing/RoutingStrategy.cs b/ConduitLLM.Core/Routing/RoutingStrategy.cs deleted file mode 100644 index 4cfd8c666..000000000 --- a/ConduitLLM.Core/Routing/RoutingStrategy.cs +++ /dev/null @@ -1,64 +0,0 @@ -namespace ConduitLLM.Core.Routing -{ - /// - /// Defines the available routing strategies for model selection. - /// - /// - /// - /// The routing strategy determines how models are selected for processing requests - /// when multiple models are available. Each strategy optimizes for different priorities - /// such as cost, performance, or load balancing. - /// - /// - public enum RoutingStrategy - { - /// - /// Simple strategy that selects the first available healthy model. - /// This is the default strategy and is suitable for most use cases. - /// - Simple, - - /// - /// Selects the model with the lowest token costs. - /// This strategy optimizes for minimizing costs when multiple models - /// with different price points are available. - /// - LeastCost, - - /// - /// Round-robin strategy that distributes requests evenly across all available models. - /// This strategy is useful for load balancing across multiple models. - /// - RoundRobin, - - /// - /// Selects the model with the lowest observed latency. - /// This strategy optimizes for performance when responsiveness is critical. - /// - LeastLatency, - - /// - /// Selects models based on a pre-defined priority order. - /// Models with lower priority values are selected first. - /// - HighestPriority, - - /// - /// Selects a random model from the available options. - /// This strategy provides simple load balancing without tracking model usage. - /// - Random, - - /// - /// Selects the model that has been used the least. - /// This strategy provides load balancing by tracking the usage count of each model. - /// - LeastUsed, - - /// - /// Passes through the request to the specified model without selection logic. - /// This strategy is useful when the client wants to control model selection. - /// - Passthrough - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/HighestPriorityModelSelectionStrategy.cs b/ConduitLLM.Core/Routing/Strategies/HighestPriorityModelSelectionStrategy.cs deleted file mode 100644 index 3f6eacf52..000000000 --- a/ConduitLLM.Core/Routing/Strategies/HighestPriorityModelSelectionStrategy.cs +++ /dev/null @@ -1,52 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// A model selection strategy that selects models based on a pre-defined priority order. - /// - /// - /// - /// This strategy selects models based on their assigned priority values, with lower - /// priority values indicating higher priority (i.e., priority 1 is higher than priority 2). - /// - /// - /// This approach allows administrators to explicitly control the order in which models - /// are selected, regardless of other factors like cost or latency. It's useful when - /// certain models are preferred over others for reasons not captured by other metrics. - /// - /// - public class HighestPriorityModelSelectionStrategy : IModelSelectionStrategy - { - /// - public string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts) - { - if (availableModels.Count() == 0) - { - return null; - } - - // Extract deployments for the available models - var availableDeployments = availableModels - .Select(m => modelDeployments.TryGetValue(m, out var deployment) ? deployment : null) - .Where(d => d != null) - .ToList(); - - if (availableDeployments.Count() == 0) - { - // Fall back to simple strategy if no deployment info is available - return availableModels[0]; - } - - // Select the model with the highest priority (lowest priority number) - return availableDeployments - .OrderBy(d => d!.Priority) - .Select(d => d!.DeploymentName) - .FirstOrDefault(); - } - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/LeastCostModelSelectionStrategy.cs b/ConduitLLM.Core/Routing/Strategies/LeastCostModelSelectionStrategy.cs deleted file mode 100644 index 5242d910e..000000000 --- a/ConduitLLM.Core/Routing/Strategies/LeastCostModelSelectionStrategy.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// A model selection strategy that selects the model with the lowest token cost. - /// - /// - /// - /// This strategy optimizes for cost by selecting the model with the lowest token cost. - /// It considers both input and output token costs, with input costs being the primary - /// sorting criterion and output costs being the secondary criterion. - /// - /// - /// This strategy is ideal for cost-sensitive applications where minimizing expenses - /// is more important than other factors like performance or features. - /// - /// - public class LeastCostModelSelectionStrategy : IModelSelectionStrategy - { - /// - public string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts) - { - if (availableModels.Count() == 0) - { - return null; - } - - // Extract deployments for the available models - var availableDeployments = availableModels - .Select(m => modelDeployments.TryGetValue(m, out var deployment) ? deployment : null) - .Where(d => d != null) - .ToList(); - - if (availableDeployments.Count() == 0) - { - // Fall back to simple strategy if no deployment info is available - return availableModels[0]; - } - - // Select the model with the lowest token costs - return availableDeployments - .OrderBy(d => d!.InputTokenCostPer1K ?? decimal.MaxValue) - .ThenBy(d => d!.OutputTokenCostPer1K ?? decimal.MaxValue) - .Select(d => d!.DeploymentName) - .FirstOrDefault(); - } - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/LeastLatencyModelSelectionStrategy.cs b/ConduitLLM.Core/Routing/Strategies/LeastLatencyModelSelectionStrategy.cs deleted file mode 100644 index 5ba6bf3e2..000000000 --- a/ConduitLLM.Core/Routing/Strategies/LeastLatencyModelSelectionStrategy.cs +++ /dev/null @@ -1,52 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// A model selection strategy that selects the model with the lowest observed latency. - /// - /// - /// - /// This strategy optimizes for performance by selecting the model with the lowest - /// average response latency. The latency information is gathered from previous requests - /// and is continuously updated as new requests are processed. - /// - /// - /// This strategy is ideal for latency-sensitive applications where response time - /// is more important than other factors like cost. - /// - /// - public class LeastLatencyModelSelectionStrategy : IModelSelectionStrategy - { - /// - public string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts) - { - if (availableModels.Count() == 0) - { - return null; - } - - // Extract deployments for the available models - var availableDeployments = availableModels - .Select(m => modelDeployments.TryGetValue(m, out var deployment) ? deployment : null) - .Where(d => d != null) - .ToList(); - - if (availableDeployments.Count() == 0) - { - // Fall back to simple strategy if no deployment info is available - return availableModels[0]; - } - - // Select the model with the lowest average latency - return availableDeployments - .OrderBy(d => d!.AverageLatencyMs) - .Select(d => d!.DeploymentName) - .FirstOrDefault(); - } - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/ModelSelectionStrategyFactory.cs b/ConduitLLM.Core/Routing/Strategies/ModelSelectionStrategyFactory.cs deleted file mode 100644 index f3112c8c8..000000000 --- a/ConduitLLM.Core/Routing/Strategies/ModelSelectionStrategyFactory.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// Factory for creating and caching model selection strategy instances. - /// - /// - /// - /// This factory provides a centralized way to obtain strategy instances based on strategy names. - /// It caches strategy instances to avoid unnecessary object creation for frequently used strategies. - /// - /// - /// The factory follows the Strategy pattern, allowing the router to dynamically select and use - /// different model selection algorithms without changing its core logic. - /// - /// - public static class ModelSelectionStrategyFactory - { - // Cache of strategy instances to avoid creating new instances for each request - private static readonly ConcurrentDictionary _strategyCache = - new(StringComparer.OrdinalIgnoreCase); - - /// - /// Gets a model selection strategy instance for the specified strategy name. - /// - /// The name of the strategy to get. - /// An instance of the requested strategy, or a SimpleModelSelectionStrategy if the strategy is not recognized. - /// - /// This method returns cached instances of strategies when possible, creating new ones only when necessary. - /// If an unrecognized strategy name is provided, it defaults to the "simple" strategy. - /// - public static IModelSelectionStrategy GetStrategy(string strategyName) - { - // If we already have a cached instance for this strategy, return it - if (_strategyCache.TryGetValue(strategyName, out var existingStrategy)) - { - return existingStrategy; - } - - // Create a new strategy instance based on the name - var newStrategy = CreateStrategy(strategyName); - - // Cache the new instance for future use - _strategyCache[strategyName] = newStrategy; - - return newStrategy; - } - - /// - /// Creates a new strategy instance based on the strategy name. - /// - /// The name of the strategy to create. - /// A new instance of the requested strategy, or a SimpleModelSelectionStrategy if the strategy is not recognized. - private static IModelSelectionStrategy CreateStrategy(string strategyName) - { - return strategyName.ToLowerInvariant() switch - { - "simple" => new SimpleModelSelectionStrategy(), - "roundrobin" => new RoundRobinModelSelectionStrategy(), - "leastcost" => new LeastCostModelSelectionStrategy(), - "leastlatency" => new LeastLatencyModelSelectionStrategy(), - "priority" => new HighestPriorityModelSelectionStrategy(), - "random" => new RoundRobinModelSelectionStrategy(), // Random removed, maps to round-robin for load distribution - "leastused" => new RoundRobinModelSelectionStrategy(), // Maps to round-robin (identical implementation) - // Default to simple strategy for unrecognized strategy names - _ => new SimpleModelSelectionStrategy() - }; - } - - /// - /// Clears the strategy cache, forcing new instances to be created on next request. - /// - /// - /// This method is primarily useful for testing or when strategy implementations might change at runtime. - /// - public static void ClearCache() - { - _strategyCache.Clear(); - } - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/RoundRobinModelSelectionStrategy.cs b/ConduitLLM.Core/Routing/Strategies/RoundRobinModelSelectionStrategy.cs deleted file mode 100644 index 3bd9a637e..000000000 --- a/ConduitLLM.Core/Routing/Strategies/RoundRobinModelSelectionStrategy.cs +++ /dev/null @@ -1,43 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// A round-robin model selection strategy that distributes requests evenly across available models. - /// - /// - /// - /// This strategy implements a round-robin selection by choosing the model that has been - /// used the least according to the usage counts. This ensures that requests are distributed - /// evenly across all available models over time. - /// - /// - /// Round-robin selection is useful for load balancing and for avoiding overloading any - /// single model deployment, especially in high-traffic scenarios. - /// - /// - /// Note: This strategy also serves the "leastused" and "random" routing strategies in the factory, - /// as they all effectively distribute load across models. - /// - /// - public class RoundRobinModelSelectionStrategy : IModelSelectionStrategy - { - /// - public string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts) - { - if (availableModels.Count() == 0) - { - return null; - } - - // Select the model with the lowest usage count - return availableModels - .OrderBy(m => modelUsageCounts.TryGetValue(m, out var count) ? count : 0) - .First(); - } - } -} diff --git a/ConduitLLM.Core/Routing/Strategies/SimpleModelSelectionStrategy.cs b/ConduitLLM.Core/Routing/Strategies/SimpleModelSelectionStrategy.cs deleted file mode 100644 index 07064239e..000000000 --- a/ConduitLLM.Core/Routing/Strategies/SimpleModelSelectionStrategy.cs +++ /dev/null @@ -1,25 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using ConduitLLM.Core.Interfaces; -namespace ConduitLLM.Core.Routing.Strategies -{ - /// - /// A simple model selection strategy that selects the first available model. - /// - /// - /// This is the most basic strategy and serves as a fallback when no specific - /// optimization is needed. It simply returns the first model in the list of - /// available models. - /// - public class SimpleModelSelectionStrategy : IModelSelectionStrategy - { - /// - public string? SelectModel( - IReadOnlyList availableModels, - IReadOnlyDictionary modelDeployments, - IReadOnlyDictionary modelUsageCounts) - { - return availableModels.Count() > 0 ? availableModels[0] : null; - } - } -} diff --git a/ConduitLLM.Core/Services/AudioAlertingService.Core.cs b/ConduitLLM.Core/Services/AudioAlertingService.Core.cs deleted file mode 100644 index 63d67b2e0..000000000 --- a/ConduitLLM.Core/Services/AudioAlertingService.Core.cs +++ /dev/null @@ -1,150 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Manages audio operation alerts and notifications. - /// - public partial class AudioAlertingService : IAudioAlertingService - { - private readonly ILogger _logger; - private readonly AudioAlertingOptions _options; - private readonly ConcurrentDictionary _alertRules = new(); - private readonly ConcurrentDictionary _lastAlertTimes = new(); - private readonly List _alertHistory = new(); - private readonly HttpClient _httpClient; - private readonly SemaphoreSlim _evaluationSemaphore = new(1); - private readonly object _historyLock = new(); - - /// - /// Initializes a new instance of the class. - /// - public AudioAlertingService( - ILogger logger, - IOptions options, - IHttpClientFactory httpClientFactory) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _httpClient = httpClientFactory?.CreateClient("AlertingService") ?? throw new ArgumentNullException(nameof(httpClientFactory)); - - // Load default alert rules - LoadDefaultRules(); - } - - /// - public Task RegisterAlertRuleAsync(AudioAlertRule rule) - { - ArgumentNullException.ThrowIfNull(rule); - - if (string.IsNullOrEmpty(rule.Id)) - rule.Id = Guid.NewGuid().ToString(); - - _alertRules[rule.Id] = rule; - - _logger.LogInformation( - "Registered alert rule: {RuleName} ({RuleId}) for metric {MetricType}", - rule.Name, rule.Id, rule.MetricType); - - return Task.FromResult(rule.Id); - } - - /// - public Task UpdateAlertRuleAsync(string ruleId, AudioAlertRule rule) - { - if (string.IsNullOrEmpty(ruleId)) - throw new ArgumentException("Rule ID cannot be empty", nameof(ruleId)); - - ArgumentNullException.ThrowIfNull(rule); - - if (!_alertRules.ContainsKey(ruleId)) - throw new InvalidOperationException($"Alert rule {ruleId} not found"); - - rule.Id = ruleId; - _alertRules[ruleId] = rule; - - _logger.LogInformation("Updated alert rule: {RuleId}", ruleId); - - return Task.CompletedTask; - } - - /// - public Task DeleteAlertRuleAsync(string ruleId) - { - if (_alertRules.TryRemove(ruleId, out var rule)) - { - _logger.LogInformation("Deleted alert rule: {RuleName} ({RuleId})", rule.Name, ruleId); - } - - return Task.CompletedTask; - } - - /// - public Task> GetActiveRulesAsync() - { - var activeRules = _alertRules.Values - .Where(r => r.IsEnabled) - .ToList(); - - return Task.FromResult(activeRules); - } - - private void LoadDefaultRules() - { - // High error rate alert - _ = RegisterAlertRuleAsync(new AudioAlertRule - { - Name = "High Error Rate", - Description = "Alert when error rate exceeds 5%", - MetricType = AudioMetricType.ErrorRate, - Condition = new AlertCondition - { - Operator = ComparisonOperator.GreaterThan, - Threshold = 0.05, - TimeWindow = TimeSpan.FromMinutes(5), - MinimumOccurrences = 2 - }, - Severity = AlertSeverity.Error, - IsEnabled = true - }).Result; - - // Provider down alert - _ = RegisterAlertRuleAsync(new AudioAlertRule - { - Name = "Provider Availability Low", - Description = "Alert when provider availability drops below 50%", - MetricType = AudioMetricType.ProviderAvailability, - Condition = new AlertCondition - { - Operator = ComparisonOperator.LessThan, - Threshold = 0.5, - TimeWindow = TimeSpan.FromMinutes(2) - }, - Severity = AlertSeverity.Critical, - IsEnabled = true - }).Result; - - // High request rate alert - _ = RegisterAlertRuleAsync(new AudioAlertRule - { - Name = "High Request Rate", - Description = "Alert when request rate exceeds 100 RPS", - MetricType = AudioMetricType.RequestRate, - Condition = new AlertCondition - { - Operator = ComparisonOperator.GreaterThan, - Threshold = 100, - TimeWindow = TimeSpan.FromMinutes(1) - }, - Severity = AlertSeverity.Warning, - IsEnabled = true - }).Result; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioAlertingService.Evaluation.cs b/ConduitLLM.Core/Services/AudioAlertingService.Evaluation.cs deleted file mode 100644 index ab25e3fb6..000000000 --- a/ConduitLLM.Core/Services/AudioAlertingService.Evaluation.cs +++ /dev/null @@ -1,139 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class AudioAlertingService - { - /// - public async Task EvaluateMetricsAsync( - AudioMetricsSnapshot metrics, - CancellationToken cancellationToken = default) - { - await _evaluationSemaphore.WaitAsync(cancellationToken); - try - { - var activeRules = await GetActiveRulesAsync(); - - foreach (var rule in activeRules) - { - try - { - await EvaluateRuleAsync(rule, metrics, cancellationToken); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error evaluating alert rule {RuleId}", rule.Id); - } - } - } - finally - { - _evaluationSemaphore.Release(); - } - } - - private async Task EvaluateRuleAsync( - AudioAlertRule rule, - AudioMetricsSnapshot metrics, - CancellationToken cancellationToken) - { - // Check cooldown period - if (_lastAlertTimes.TryGetValue(rule.Id, out var lastAlert)) - { - if (DateTime.UtcNow - lastAlert < rule.CooldownPeriod) - { - return; // Still in cooldown - } - } - - // Extract metric value - var metricValue = ExtractMetricValue(rule.MetricType, metrics); - - // Evaluate condition - if (!EvaluateCondition(rule.Condition, metricValue)) - { - return; // Condition not met - } - - // Create triggered alert - var alert = new TriggeredAlert - { - Rule = rule, - MetricValue = metricValue, - Message = FormatAlertMessage(rule, metricValue), - Details = new Dictionary - { - ["metric_type"] = rule.MetricType.ToString(), - ["threshold"] = rule.Condition.Threshold, - ["actual_value"] = metricValue, - ["timestamp"] = metrics.Timestamp - }, - State = AlertState.Active - }; - - // Add to history - lock (_historyLock) - { - _alertHistory.Add(alert); - - // Trim old history - if (_alertHistory.Count() > _options.MaxHistorySize) - { - _alertHistory.RemoveAt(0); - } - } - - // Update last alert time - _lastAlertTimes[rule.Id] = DateTime.UtcNow; - - // Send notifications - await SendNotificationsAsync(alert, cancellationToken); - - _logger.LogWarning( - "Alert triggered: {AlertName} - {Message}", - rule.Name, alert.Message); - } - - private double ExtractMetricValue(AudioMetricType metricType, AudioMetricsSnapshot metrics) - { - return metricType switch - { - AudioMetricType.ErrorRate => metrics.CurrentErrorRate, - AudioMetricType.Latency => 0, // Would need historical data - AudioMetricType.ProviderAvailability => metrics.ProviderHealth.Count(p => p.Value) / (double)Math.Max(1, metrics.ProviderHealth.Count()), - AudioMetricType.CacheHitRate => 0, // Would need cache metrics - AudioMetricType.ActiveSessions => metrics.ActiveRealtimeSessions, - AudioMetricType.RequestRate => metrics.RequestsPerSecond, - AudioMetricType.CostRate => 0, // Would need cost data - AudioMetricType.ConnectionPoolUtilization => metrics.Resources.ActiveConnections / 100.0, - AudioMetricType.QueueLength => 0, // Would need queue metrics - _ => 0 - }; - } - - private bool EvaluateCondition(AlertCondition condition, double value) - { - var result = condition.Operator switch - { - ComparisonOperator.GreaterThan => value > condition.Threshold, - ComparisonOperator.LessThan => value < condition.Threshold, - ComparisonOperator.Equals => Math.Abs(value - condition.Threshold) < 0.001, - ComparisonOperator.NotEquals => Math.Abs(value - condition.Threshold) >= 0.001, - ComparisonOperator.GreaterThanOrEqual => value >= condition.Threshold, - ComparisonOperator.LessThanOrEqual => value <= condition.Threshold, - _ => false - }; - - return result; - } - - private string FormatAlertMessage(AudioAlertRule rule, double metricValue) - { - return $"{rule.Name}: {rule.MetricType} is {metricValue:F2} " + - $"(threshold: {rule.Condition.Operator} {rule.Condition.Threshold:F2})"; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioAlertingService.Management.cs b/ConduitLLM.Core/Services/AudioAlertingService.Management.cs deleted file mode 100644 index b65acf764..000000000 --- a/ConduitLLM.Core/Services/AudioAlertingService.Management.cs +++ /dev/null @@ -1,139 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class AudioAlertingService - { - /// - public Task> GetAlertHistoryAsync( - DateTime startTime, - DateTime endTime, - AlertSeverity? severity = null) - { - lock (_historyLock) - { - var filtered = _alertHistory - .Where(a => a.TriggeredAt >= startTime && a.TriggeredAt <= endTime) - .Where(a => severity == null || a.Rule.Severity == severity) - .OrderByDescending(a => a.TriggeredAt) - .ToList(); - - return Task.FromResult(filtered); - } - } - - /// - public Task AcknowledgeAlertAsync( - string alertId, - string acknowledgedBy, - string? notes = null) - { - lock (_historyLock) - { - var alert = _alertHistory.FirstOrDefault(a => a.Id == alertId); - if (alert != null) - { - alert.State = AlertState.Acknowledged; - alert.AcknowledgedBy = acknowledgedBy; - alert.AcknowledgedAt = DateTime.UtcNow; - alert.AcknowledgmentNotes = notes; - - _logger.LogInformation( - "Alert {AlertId} acknowledged by {User}", - alertId, acknowledgedBy); - } - } - - return Task.CompletedTask; - } - - /// - public async Task TestAlertRuleAsync(AudioAlertRule rule) - { - var result = new AlertTestResult - { - Success = true, - Message = "Alert rule test completed" - }; - - try - { - // Simulate metric value - var testMetrics = CreateTestMetrics(rule.MetricType); - var metricValue = ExtractMetricValue(rule.MetricType, testMetrics); - - result.SimulatedMetricValue = metricValue; - result.WouldTrigger = EvaluateCondition(rule.Condition, metricValue); - - // Test notification channels - foreach (var channel in rule.NotificationChannels) - { - var notificationTest = await TestNotificationChannelAsync(channel); - result.NotificationTests.Add(notificationTest); - } - - if (result.WouldTrigger) - { - result.Message = $"Alert would trigger: {rule.Name} (value: {metricValue})"; - } - } - catch (Exception ex) - { - result.Success = false; - result.Message = $"Test failed: {ex.Message}"; - } - - return result; - } - - private AudioMetricsSnapshot CreateTestMetrics(AudioMetricType metricType) - { - return new AudioMetricsSnapshot - { - Timestamp = DateTime.UtcNow, - ActiveTranscriptions = 5, - ActiveTtsOperations = 3, - ActiveRealtimeSessions = 10, - RequestsPerSecond = 25.5, - CurrentErrorRate = 0.02, - ProviderHealth = new Dictionary - { - ["openai"] = true, - ["elevenlabs"] = true, - ["deepgram"] = false - }, - Resources = new SystemResources - { - CpuUsagePercent = 45.2, - MemoryUsageMb = 2048, - ActiveConnections = 75, - CacheSizeMb = 512 - } - }; - } - } - - /// - /// Options for audio alerting service. - /// - public class AudioAlertingOptions - { - /// - /// Gets or sets the maximum alert history size. - /// - public int MaxHistorySize { get; set; } = 1000; - - /// - /// Gets or sets the default cooldown period. - /// - public TimeSpan DefaultCooldownPeriod { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// Gets or sets the evaluation interval. - /// - public TimeSpan EvaluationInterval { get; set; } = TimeSpan.FromMinutes(1); - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioAlertingService.Notifications.cs b/ConduitLLM.Core/Services/AudioAlertingService.Notifications.cs deleted file mode 100644 index 4927814ce..000000000 --- a/ConduitLLM.Core/Services/AudioAlertingService.Notifications.cs +++ /dev/null @@ -1,237 +0,0 @@ -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class AudioAlertingService - { - private async Task SendNotificationsAsync( - TriggeredAlert alert, - CancellationToken cancellationToken) - { - var tasks = alert.Rule.NotificationChannels - .Select(channel => SendNotificationAsync(channel, alert, cancellationToken)) - .ToList(); - - await Task.WhenAll(tasks); - } - - private async Task SendNotificationAsync( - NotificationChannel channel, - TriggeredAlert alert, - CancellationToken cancellationToken) - { - try - { - switch (channel.Type) - { - case NotificationChannelType.Email: - await SendEmailNotificationAsync(channel, alert, cancellationToken); - break; - - case NotificationChannelType.Webhook: - await SendWebhookNotificationAsync(channel, alert, cancellationToken); - break; - - case NotificationChannelType.Slack: - await SendSlackNotificationAsync(channel, alert, cancellationToken); - break; - - case NotificationChannelType.Teams: - await SendTeamsNotificationAsync(channel, alert, cancellationToken); - break; - - default: - _logger.LogWarning("Unsupported notification channel type: {Type}", channel.Type); - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Failed to send {ChannelType} notification for alert {AlertId}", - channel.Type, alert.Id); - } - } - - private async Task SendWebhookNotificationAsync( - NotificationChannel channel, - TriggeredAlert alert, - CancellationToken cancellationToken) - { - var payload = new - { - alert_id = alert.Id, - rule_name = alert.Rule.Name, - severity = alert.Rule.Severity.ToString(), - metric_type = alert.Rule.MetricType.ToString(), - metric_value = alert.MetricValue, - threshold = alert.Rule.Condition.Threshold, - message = alert.Message, - triggered_at = alert.TriggeredAt, - details = alert.Details - }; - - var json = JsonSerializer.Serialize(payload); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(channel.Target, content, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - _logger.LogError( - "Webhook notification failed: {StatusCode} - {Reason}", - response.StatusCode, response.ReasonPhrase); - } - } - - private async Task SendEmailNotificationAsync( - NotificationChannel channel, - TriggeredAlert alert, - CancellationToken cancellationToken) - { - // In production, this would integrate with an email service - _logger.LogInformation( - "Email notification would be sent to {Target} for alert {AlertId}", - channel.Target, alert.Id); - - await Task.CompletedTask; - } - - private async Task SendSlackNotificationAsync( - NotificationChannel channel, - TriggeredAlert alert, - CancellationToken cancellationToken) - { - var color = alert.Rule.Severity switch - { - AlertSeverity.Critical => "danger", - AlertSeverity.Error => "warning", - AlertSeverity.Warning => "warning", - _ => "good" - }; - - var payload = new - { - attachments = new[] - { - new - { - color, - title = $"{alert.Rule.Severity}: {alert.Rule.Name}", - text = alert.Message, - fields = new[] - { - new { title = "Metric", value = alert.Rule.MetricType.ToString(), @short = true }, - new { title = "Value", value = alert.MetricValue.ToString("F2"), @short = true }, - new { title = "Threshold", value = alert.Rule.Condition.Threshold.ToString("F2"), @short = true }, - new { title = "Time", value = alert.TriggeredAt.ToString("yyyy-MM-dd HH:mm:ss UTC"), @short = true } - }, - footer = "Conduit Audio Alerting", - ts = ((DateTimeOffset)alert.TriggeredAt).ToUnixTimeSeconds() - } - } - }; - - var json = JsonSerializer.Serialize(payload); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - await _httpClient.PostAsync(channel.Target, content, cancellationToken); - } - - private async Task SendTeamsNotificationAsync( - NotificationChannel channel, - TriggeredAlert alert, - CancellationToken cancellationToken) - { - var themeColor = alert.Rule.Severity switch - { - AlertSeverity.Critical => "FF0000", - AlertSeverity.Error => "FF8C00", - AlertSeverity.Warning => "FFD700", - _ => "00FF00" - }; - - var payload = new - { - @type = "MessageCard", - @context = "https://schema.org/extensions", - summary = alert.Message, - themeColor, - sections = new[] - { - new - { - activityTitle = alert.Rule.Name, - activitySubtitle = $"Severity: {alert.Rule.Severity}", - facts = new[] - { - new { name = "Metric", value = alert.Rule.MetricType.ToString() }, - new { name = "Current Value", value = alert.MetricValue.ToString("F2") }, - new { name = "Threshold", value = $"{alert.Rule.Condition.Operator} {alert.Rule.Condition.Threshold:F2}" }, - new { name = "Triggered At", value = alert.TriggeredAt.ToString("yyyy-MM-dd HH:mm:ss UTC") } - } - } - } - }; - - var json = JsonSerializer.Serialize(payload); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - await _httpClient.PostAsync(channel.Target, content, cancellationToken); - } - - private async Task TestNotificationChannelAsync(NotificationChannel channel) - { - var result = new NotificationTestResult - { - ChannelType = channel.Type, - Success = true - }; - - try - { - // Test connectivity - switch (channel.Type) - { - case NotificationChannelType.Webhook: - case NotificationChannelType.Slack: - case NotificationChannelType.Teams: - var testPayload = new { test = true, timestamp = DateTime.UtcNow }; - var json = JsonSerializer.Serialize(testPayload); - var content = new StringContent(json, Encoding.UTF8, "application/json"); - - var response = await _httpClient.PostAsync(channel.Target, content); - result.Success = response.IsSuccessStatusCode; - if (!result.Success) - { - result.ErrorMessage = $"HTTP {response.StatusCode}: {response.ReasonPhrase}"; - } - break; - - case NotificationChannelType.Email: - // Would test SMTP connectivity - result.Success = true; - break; - - default: - result.Success = false; - result.ErrorMessage = $"Unsupported channel type: {channel.Type}"; - break; - } - } - catch (Exception ex) - { - result.Success = false; - result.ErrorMessage = ex.Message; - } - - return result; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioAlertingService.cs b/ConduitLLM.Core/Services/AudioAlertingService.cs deleted file mode 100644 index a0720553f..000000000 --- a/ConduitLLM.Core/Services/AudioAlertingService.cs +++ /dev/null @@ -1,5 +0,0 @@ -// This file has been split into partial classes for better maintainability: -// - AudioAlertingService.Core.cs: Constructor and alert rule management -// - AudioAlertingService.Evaluation.cs: Metrics evaluation and alert triggering -// - AudioAlertingService.Notifications.cs: Notification sending functionality -// - AudioAlertingService.Management.cs: Alert history, testing, and options diff --git a/ConduitLLM.Core/Services/AudioAuditLogger.cs b/ConduitLLM.Core/Services/AudioAuditLogger.cs deleted file mode 100644 index 8bb4ca0ce..000000000 --- a/ConduitLLM.Core/Services/AudioAuditLogger.cs +++ /dev/null @@ -1,196 +0,0 @@ -using System.Text.Json; -using ConduitLLM.Core.Extensions; -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of audio audit logging. - /// - public class AudioAuditLogger : IAudioAuditLogger - { - private readonly ILogger _logger; - private readonly IRequestLogRepository _requestLogRepository; - private readonly INotificationRepository _notificationRepository; - - /// - /// Initializes a new instance of the class. - /// - public AudioAuditLogger( - ILogger logger, - IRequestLogRepository requestLogRepository, - INotificationRepository notificationRepository) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _requestLogRepository = requestLogRepository ?? throw new ArgumentNullException(nameof(requestLogRepository)); - _notificationRepository = notificationRepository ?? throw new ArgumentNullException(nameof(notificationRepository)); - } - - /// - public async Task LogTranscriptionAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default) - { - entry.Operation = AudioOperation.Transcription; - await LogAudioOperationAsync(entry, cancellationToken); - } - - /// - public async Task LogTextToSpeechAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default) - { - entry.Operation = AudioOperation.TextToSpeech; - await LogAudioOperationAsync(entry, cancellationToken); - } - - /// - public async Task LogRealtimeSessionAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken = default) - { - entry.Operation = AudioOperation.Realtime; - await LogAudioOperationAsync(entry, cancellationToken); - } - - /// - public async Task LogContentFilteringAsync( - ContentFilterAuditEntry entry, - CancellationToken cancellationToken = default) - { - // Add specific metadata for content filtering - entry.Metadata["FilterType"] = "Content"; - entry.Metadata["WasBlocked"] = entry.WasBlocked.ToString(); - entry.Metadata["WasModified"] = entry.WasModified.ToString(); - entry.Metadata["ViolationCount"] = entry.ViolationCategories.Count.ToString(); - - if (entry.ViolationCategories.Count() > 0) - { - entry.Metadata["ViolationCategories"] = string.Join(",", entry.ViolationCategories); - } - - await LogAudioOperationAsync(entry, cancellationToken); - - // Create notification if content was blocked - if (entry.WasBlocked) - { - await CreateSecurityNotificationAsync( - "Content Blocked", - $"Audio {entry.Operation} request blocked due to inappropriate content", - entry.VirtualKey, - cancellationToken); - } - } - - /// - public async Task LogPiiDetectionAsync( - PiiAuditEntry entry, - CancellationToken cancellationToken = default) - { - // Add specific metadata for PII detection - entry.Metadata["FilterType"] = "PII"; - entry.Metadata["PiiDetected"] = entry.PiiDetected.ToString(); - entry.Metadata["EntityCount"] = entry.EntityCount.ToString(); - entry.Metadata["RiskScore"] = entry.RiskScore.ToString("F2"); - - if (entry.PiiTypes.Count() > 0) - { - entry.Metadata["PiiTypes"] = string.Join(",", entry.PiiTypes); - } - - if (entry.WasRedacted) - { - entry.Metadata["Redacted"] = "true"; - entry.Metadata["RedactionMethod"] = entry.RedactionMethod?.ToString() ?? "Unknown"; - } - - await LogAudioOperationAsync(entry, cancellationToken); - - // Create notification if high-risk PII was detected - if (entry.RiskScore > 0.7) - { - await CreateSecurityNotificationAsync( - "High-Risk PII Detected", - $"Audio {entry.Operation} request contained high-risk PII (score: {entry.RiskScore:F2})", - entry.VirtualKey, - cancellationToken); - } - } - - private Task LogAudioOperationAsync( - AudioAuditEntry entry, - CancellationToken cancellationToken) - { - try - { - // For now, just log to the logger - // In a real implementation, you would create a proper audio audit table - _logger.LogInformation( - "Audio Operation: {Operation} | Key: {VirtualKey} | Provider: {Provider} | Model: {Model} | " + - "Duration: {Duration}ms | Success: {Success} | Size: {Size} bytes | Language: {Language}", - entry.Operation, - entry.VirtualKey, - entry.Provider, - entry.Model, - entry.DurationMs, - entry.Success, - entry.SizeBytes, - entry.Language); - - if (!entry.Success && !string.IsNullOrEmpty(entry.ErrorMessage)) - { - _logger.LogError( - "Audio operation failed: {ErrorMessage}", - entry.ErrorMessage); - } - - // Log metadata as structured data - if (entry.Metadata.Count() > 0) - { - _logger.LogDebug( - "Audio operation metadata: {Metadata}", - JsonSerializer.Serialize(entry.Metadata)); - } - } - catch (Exception ex) - { - _logger.LogError( - ex, - "Failed to log audio audit entry {Id}", - entry.Id); - } - - return Task.CompletedTask; - } - - private async Task CreateSecurityNotificationAsync( - string title, - string message, - string virtualKey, - CancellationToken cancellationToken) - { - try - { - // For now, just log the security notification - // In a real implementation, you would use a proper notification system - _logger.LogWarningSecure( - "SECURITY NOTIFICATION - {Title}: {Message} | VirtualKey: {VirtualKey}", - title, - message, - virtualKey); - - await Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogErrorSecure( - ex, - "Failed to create security notification for key {VirtualKey}", - virtualKey); - } - } - } -} diff --git a/ConduitLLM.Core/Services/AudioCapabilityDetector.cs b/ConduitLLM.Core/Services/AudioCapabilityDetector.cs deleted file mode 100644 index 37bc935f9..000000000 --- a/ConduitLLM.Core/Services/AudioCapabilityDetector.cs +++ /dev/null @@ -1,354 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Default implementation of the audio capability detector. - /// Uses Provider IDs and ProviderType to determine capabilities. - /// - public class AudioCapabilityDetector : IAudioCapabilityDetector - { - private readonly ILogger _logger; - private readonly IModelCapabilityService _capabilityService; - private readonly IProviderService _providerService; - - /// - /// Initializes a new instance of the AudioCapabilityDetector class. - /// - /// Logger for diagnostics - /// Service for retrieving model capabilities from configuration - /// Service for retrieving provider information - public AudioCapabilityDetector( - ILogger logger, - IModelCapabilityService capabilityService, - IProviderService providerService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _capabilityService = capabilityService ?? throw new ArgumentNullException(nameof(capabilityService)); - _providerService = providerService ?? throw new ArgumentNullException(nameof(providerService)); - } - - /// - /// Determines if a provider supports audio transcription. - /// - public bool SupportsTranscription(int providerId, string? model = null) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return false; - } - - // TODO: Query ModelCapabilities from database through ModelProviderMapping - // For now, check if the provider has any models that support transcription - // This should be replaced with proper database capability checks - - // Use capability service if available and model is specified - if (_capabilityService != null && !string.IsNullOrEmpty(model)) - { - try - { - var supportsTranscription = _capabilityService.SupportsAudioTranscriptionAsync(model).GetAwaiter().GetResult(); - return supportsTranscription; - } - catch (Exception capEx) - { - _logger.LogWarning(capEx, "Failed to get transcription capability for model {Model}", model); - } - } - - // Fallback: Return false - require explicit capability in database - _logger.LogWarning("No transcription capability found in database for provider {ProviderId}", providerId); - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking transcription capability for provider {ProviderId}", providerId); - return false; - } - } - - /// - /// Determines if a provider supports text-to-speech synthesis. - /// - public bool SupportsTextToSpeech(int providerId, string? model = null) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return false; - } - - // TODO: Query ModelCapabilities from database through ModelProviderMapping - // For now, check if the provider has any models that support TTS - // This should be replaced with proper database capability checks - - // Use capability service if available and model is specified - if (_capabilityService != null && !string.IsNullOrEmpty(model)) - { - try - { - var supportsTTS = _capabilityService.SupportsTextToSpeechAsync(model).GetAwaiter().GetResult(); - return supportsTTS; - } - catch (Exception capEx) - { - _logger.LogWarning(capEx, "Failed to get TTS capability for model {Model}", model); - } - } - - // Fallback: Return false - require explicit capability in database - _logger.LogWarning("No TTS capability found in database for provider {ProviderId}", providerId); - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking text-to-speech capability for provider {ProviderId}", providerId); - return false; - } - } - - /// - /// Determines if a provider supports real-time conversational audio. - /// - public bool SupportsRealtime(int providerId, string? model = null) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return false; - } - - // TODO: Query ModelCapabilities from database through ModelProviderMapping - // For now, check if the provider has any models that support realtime - // This should be replaced with proper database capability checks - - // Use capability service if available and model is specified - if (_capabilityService != null && !string.IsNullOrEmpty(model)) - { - try - { - var supportsRealtime = _capabilityService.SupportsRealtimeAudioAsync(model).GetAwaiter().GetResult(); - return supportsRealtime; - } - catch (Exception capEx) - { - _logger.LogWarning(capEx, "Failed to get realtime capability for model {Model}", model); - } - } - - // Fallback: Return false - require explicit capability in database - _logger.LogWarning("No realtime capability found in database for provider {ProviderId}", providerId); - return false; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking realtime capability for provider {ProviderId}", providerId); - return false; - } - } - - /// - /// Checks if a specific voice is available for a provider. - /// - public bool SupportsVoice(int providerId, string voiceId) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return false; - } - - // Basic implementation - could be enhanced with provider-specific voice validation - return SupportsTextToSpeech(providerId) || SupportsRealtime(providerId); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking voice support for provider {ProviderId}, voice {VoiceId}", providerId, voiceId); - return false; - } - } - - /// - /// Gets the audio formats supported by a provider for a specific operation. - /// - public AudioFormat[] GetSupportedFormats(int providerId, AudioOperation operation) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return Array.Empty(); - } - - // Basic implementation - return common formats - return provider.ProviderType switch - { - ProviderType.OpenAI => new[] { AudioFormat.Mp3, AudioFormat.Wav, AudioFormat.Flac, AudioFormat.Ogg }, - ProviderType.Groq => new[] { AudioFormat.Mp3, AudioFormat.Wav, AudioFormat.Flac }, - ProviderType.ElevenLabs => new[] { AudioFormat.Mp3, AudioFormat.Wav }, - _ => Array.Empty() - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting supported formats for provider {ProviderId}, operation {Operation}", providerId, operation); - return Array.Empty(); - } - } - - /// - /// Gets the languages supported by a provider for a specific audio operation. - /// - public IEnumerable GetSupportedLanguages(int providerId, AudioOperation operation) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null || !provider.IsEnabled) - { - return Enumerable.Empty(); - } - - // Basic implementation - return common languages - return new[] { "en", "es", "fr", "de", "it", "pt", "ru", "ja", "ko", "zh" }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting supported languages for provider {ProviderId}, operation {Operation}", providerId, operation); - return Enumerable.Empty(); - } - } - - /// - /// Validates that an audio request can be processed by the specified provider. - /// - public bool ValidateAudioRequest(AudioRequestBase request, int providerId, out string errorMessage) - { - errorMessage = string.Empty; - - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null) - { - errorMessage = $"Provider with ID {providerId} not found"; - return false; - } - - if (!provider.IsEnabled) - { - errorMessage = $"Provider {provider.ProviderName} is disabled"; - return false; - } - - // Basic validation - could be enhanced with more specific checks - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error validating audio request for provider {ProviderId}", providerId); - errorMessage = "Internal error validating request"; - return false; - } - } - - /// - /// Gets a list of all provider IDs that support a specific audio capability. - /// - public IEnumerable GetProvidersWithCapability(AudioCapability capability) - { - try - { - var allProviders = _providerService.GetAllEnabledProvidersAsync().GetAwaiter().GetResult(); - - return capability switch - { - AudioCapability.BasicTranscription => allProviders.Where(p => SupportsTranscription(p.Id)).Select(p => p.Id), - AudioCapability.BasicTTS => allProviders.Where(p => SupportsTextToSpeech(p.Id)).Select(p => p.Id), - AudioCapability.RealtimeConversation => allProviders.Where(p => SupportsRealtime(p.Id)).Select(p => p.Id), - _ => Enumerable.Empty() - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting providers with capability {Capability}", capability); - return Enumerable.Empty(); - } - } - - /// - /// Gets detailed capability information for a specific provider. - /// - public AudioProviderCapabilities GetProviderCapabilities(int providerId) - { - try - { - var provider = _providerService.GetByIdAsync(providerId).GetAwaiter().GetResult(); - if (provider == null) - { - return new AudioProviderCapabilities(); - } - - return new AudioProviderCapabilities - { - Provider = providerId.ToString(), - DisplayName = provider.ProviderName, - SupportedCapabilities = new List(), - TextToSpeech = new TextToSpeechCapabilities - { - SupportedFormats = GetSupportedFormats(providerId, AudioOperation.TextToSpeech).ToList(), - SupportedLanguages = GetSupportedLanguages(providerId, AudioOperation.TextToSpeech).ToList() - }, - Transcription = new TranscriptionCapabilities - { - SupportedLanguages = GetSupportedLanguages(providerId, AudioOperation.Transcription).ToList() - } - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting capabilities for provider {ProviderId}", providerId); - return new AudioProviderCapabilities(); - } - } - - /// - /// Determines the best provider for a specific audio request based on capabilities and requirements. - /// - public int? RecommendProvider(AudioRequestBase request, IEnumerable availableProviderIds) - { - try - { - var candidates = availableProviderIds.ToList(); - if (candidates.Count() == 0) - { - return null; - } - - // Simple recommendation logic - return first capable provider - // Could be enhanced with more sophisticated selection criteria - return candidates.FirstOrDefault(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error recommending provider for request"); - return null; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioCdnService.cs b/ConduitLLM.Core/Services/AudioCdnService.cs deleted file mode 100644 index 4a2585ccc..000000000 --- a/ConduitLLM.Core/Services/AudioCdnService.cs +++ /dev/null @@ -1,376 +0,0 @@ -using System.Security.Cryptography; -using System.Text; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of CDN service for audio content delivery. - /// Note: This is a simplified implementation. In production, this would integrate - /// with actual CDN providers like CloudFront, Cloudflare, or Azure CDN. - /// - public class AudioCdnService : IAudioCdnService - { - private readonly ILogger _logger; - private readonly AudioCdnOptions _options; - private readonly Dictionary _contentStore = new(); - private readonly CdnMetrics _metrics = new(); - private readonly SemaphoreSlim _uploadSemaphore; - - /// - /// Initializes a new instance of the class. - /// - public AudioCdnService( - ILogger logger, - IOptions options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _uploadSemaphore = new SemaphoreSlim(_options.MaxConcurrentUploads); - } - - /// - public async Task UploadAudioAsync( - byte[] audioData, - string contentType, - CdnMetadata? metadata = null, - CancellationToken cancellationToken = default) - { - await _uploadSemaphore.WaitAsync(cancellationToken); - try - { - var contentKey = GenerateContentKey(audioData); - var contentHash = ComputeHash(audioData); - - // Check if already exists - if (_contentStore.ContainsKey(contentKey)) - { - _logger.LogDebug("Content already exists in CDN: {Key}", contentKey); - _metrics.IncrementDuplicateUploads(); - return CreateUploadResult(contentKey, contentHash, audioData.Length); - } - - // Simulate upload to CDN - await SimulateCdnUpload(audioData.Length, cancellationToken); - - // Store content metadata - var entry = new CdnContentEntry - { - ContentKey = contentKey, - ContentHash = contentHash, - ContentType = contentType, - SizeBytes = audioData.Length, - Metadata = metadata, - UploadedAt = DateTime.UtcNow, - EdgeLocations = DetermineEdgeLocations() - }; - - _contentStore[contentKey] = entry; - _metrics.AddUploadedBytes(audioData.Length); - - _logger.LogInformation( - "Uploaded audio to CDN: {Key} ({Size} bytes) to {EdgeCount} edge locations", - contentKey, audioData.Length, entry.EdgeLocations.Count); - - return CreateUploadResult(contentKey, contentHash, audioData.Length); - } - finally - { - _uploadSemaphore.Release(); - } - } - - /// - public async Task StreamUploadAsync( - Stream audioStream, - string contentType, - CdnMetadata? metadata = null, - CancellationToken cancellationToken = default) - { - await _uploadSemaphore.WaitAsync(cancellationToken); - try - { - // Read stream in chunks and compute hash - using var memoryStream = new MemoryStream(); - await audioStream.CopyToAsync(memoryStream, cancellationToken); - var audioData = memoryStream.ToArray(); - - return await UploadAudioAsync(audioData, contentType, metadata, cancellationToken); - } - finally - { - _uploadSemaphore.Release(); - } - } - - /// - public Task GetCdnUrlAsync( - string contentKey, - TimeSpan? expiresIn = null) - { - if (!_contentStore.TryGetValue(contentKey, out var entry)) - { - return Task.FromResult(null); - } - - _metrics.IncrementRequests(entry.ContentType); - - // Generate CDN URL (in production, this would include signing for security) - var baseUrl = _options.CdnBaseUrl.TrimEnd('/'); - var expires = expiresIn ?? _options.DefaultUrlExpiration; - var expiryTimestamp = DateTimeOffset.UtcNow.Add(expires).ToUnixTimeSeconds(); - - var url = $"{baseUrl}/{contentKey}?expires={expiryTimestamp}"; - - // In production, add signature for URL authentication - var signature = GenerateUrlSignature(contentKey, expiryTimestamp); - url += $"&sig={signature}"; - - return Task.FromResult(url); - } - - /// - public Task InvalidateCacheAsync( - string contentKey, - CancellationToken cancellationToken = default) - { - if (_contentStore.Remove(contentKey)) - { - _logger.LogInformation("Invalidated CDN cache for key: {Key}", contentKey); - - // In production, this would trigger CDN invalidation API - return SimulateCdnInvalidation(contentKey, cancellationToken); - } - - return Task.CompletedTask; - } - - /// - public Task GetUsageStatisticsAsync( - DateTime? startDate = null, - DateTime? endDate = null) - { - var start = startDate ?? DateTime.UtcNow.AddDays(-30); - var end = endDate ?? DateTime.UtcNow; - - var stats = new CdnUsageStatistics - { - TotalBandwidthBytes = _metrics.TotalBandwidthBytes, - TotalRequests = _metrics.TotalRequests, - CacheHitRate = _metrics.CalculateHitRate(), - AverageResponseTimeMs = _metrics.AverageResponseTimeMs, - BandwidthByRegion = _metrics.BandwidthByRegion.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), - RequestsByContentType = _metrics.RequestsByContentType.ToDictionary(kvp => kvp.Key, kvp => kvp.Value), - TopContent = GetTopContent(10) - }; - - return Task.FromResult(stats); - } - - /// - public Task ConfigureEdgeLocationsAsync( - CdnEdgeConfiguration config, - CancellationToken cancellationToken = default) - { - _logger.LogInformation( - "Configuring CDN edge locations: {Count} priority regions, auto-scaling: {AutoScale}", - config.PriorityRegions.Count, - config.EnableAutoScaling); - - // In production, this would configure actual CDN edge locations - // For now, just log the configuration - foreach (var rule in config.RoutingRules) - { - _logger.LogDebug( - "Routing rule: {Source} -> {Target} (weight: {Weight})", - rule.SourceRegion, - rule.TargetEdgeLocation, - rule.Weight); - } - - return Task.CompletedTask; - } - - private string GenerateContentKey(byte[] audioData) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(audioData); - return Convert.ToBase64String(hash) - .Replace("+", "-") - .Replace("/", "_") - .TrimEnd('='); - } - - private string ComputeHash(byte[] data) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(data); - return Convert.ToHexString(hash).ToLowerInvariant(); - } - - private List DetermineEdgeLocations() - { - // In production, this would be based on actual CDN configuration - return new List - { - "us-east-1", - "us-west-2", - "eu-west-1", - "ap-southeast-1" - }; - } - - private CdnUploadResult CreateUploadResult(string contentKey, string contentHash, long sizeBytes) - { - var url = GetCdnUrlAsync(contentKey).Result ?? string.Empty; - - return new CdnUploadResult - { - Url = url, - ContentKey = contentKey, - ContentHash = contentHash, - UploadedAt = DateTime.UtcNow, - SizeBytes = sizeBytes, - EdgeLocations = _contentStore.TryGetValue(contentKey, out var entry) - ? entry.EdgeLocations - : new List() - }; - } - - private string GenerateUrlSignature(string contentKey, long expiryTimestamp) - { - // In production, use HMAC with secret key - var signatureData = $"{contentKey}:{expiryTimestamp}:{_options.SignatureSecret}"; - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(Encoding.UTF8.GetBytes(signatureData)); - return Convert.ToBase64String(hash) - .Replace("+", "-") - .Replace("/", "_") - .TrimEnd('='); - } - - private async Task SimulateCdnUpload(long sizeBytes, CancellationToken cancellationToken) - { - // Simulate upload time based on size - var uploadTimeMs = (int)Math.Min(100 + (sizeBytes / 1024), 5000); // Max 5 seconds - await Task.Delay(uploadTimeMs, cancellationToken); - } - - private async Task SimulateCdnInvalidation(string contentKey, CancellationToken cancellationToken) - { - // Simulate CDN invalidation propagation - await Task.Delay(1000, cancellationToken); - } - - private List GetTopContent(int count) - { - return _contentStore.Values - .OrderByDescending(e => _metrics.GetContentRequests(e.ContentKey)) - .Take(count) - .Select(e => new TopContent - { - ContentKey = e.ContentKey, - Requests = _metrics.GetContentRequests(e.ContentKey), - BandwidthBytes = e.SizeBytes * _metrics.GetContentRequests(e.ContentKey) - }) - .ToList(); - } - } - - /// - /// Internal CDN content entry. - /// - internal class CdnContentEntry - { - public string ContentKey { get; set; } = string.Empty; - public string ContentHash { get; set; } = string.Empty; - public string ContentType { get; set; } = string.Empty; - public long SizeBytes { get; set; } - public CdnMetadata? Metadata { get; set; } - public DateTime UploadedAt { get; set; } - public List EdgeLocations { get; set; } = new(); - } - - /// - /// Internal CDN metrics. - /// - internal class CdnMetrics - { - private long _totalBandwidthBytes; - private long _totalRequests; - private long _cacheHits = 0; - private long _duplicateUploads; - private readonly Dictionary _requestsByContentType = new(); - private readonly Dictionary _contentRequests = new(); - - public long TotalBandwidthBytes => _totalBandwidthBytes; - public long TotalRequests => _totalRequests; - public double AverageResponseTimeMs => 25; // Simulated - public Dictionary BandwidthByRegion => new() - { - ["us-east-1"] = _totalBandwidthBytes * 40 / 100, - ["us-west-2"] = _totalBandwidthBytes * 30 / 100, - ["eu-west-1"] = _totalBandwidthBytes * 20 / 100, - ["ap-southeast-1"] = _totalBandwidthBytes * 10 / 100 - }; - public Dictionary RequestsByContentType => _requestsByContentType; - - public void AddUploadedBytes(long bytes) => Interlocked.Add(ref _totalBandwidthBytes, bytes); - public void IncrementDuplicateUploads() => Interlocked.Increment(ref _duplicateUploads); - - public void IncrementRequests(string contentType) - { - Interlocked.Increment(ref _totalRequests); - lock (_requestsByContentType) - { - _requestsByContentType.TryGetValue(contentType, out var count); - _requestsByContentType[contentType] = count + 1; - } - } - - public long GetContentRequests(string contentKey) - { - lock (_contentRequests) - { - _contentRequests.TryGetValue(contentKey, out var count); - return count; - } - } - - public double CalculateHitRate() - { - var total = _totalRequests + _duplicateUploads; - return total > 0 ? (double)_cacheHits / total : 0; - } - } - - /// - /// Options for audio CDN service. - /// - public class AudioCdnOptions - { - /// - /// Gets or sets the CDN base URL. - /// - public string CdnBaseUrl { get; set; } = "https://cdn.example.com"; - - /// - /// Gets or sets the signature secret. - /// - public string SignatureSecret { get; set; } = "default-secret"; - - /// - /// Gets or sets the default URL expiration. - /// - public TimeSpan DefaultUrlExpiration { get; set; } = TimeSpan.FromHours(24); - - /// - /// Gets or sets the maximum concurrent uploads. - /// - public int MaxConcurrentUploads { get; set; } = 10; - } -} diff --git a/ConduitLLM.Core/Services/AudioConnectionPool.cs b/ConduitLLM.Core/Services/AudioConnectionPool.cs deleted file mode 100644 index efefb2be9..000000000 --- a/ConduitLLM.Core/Services/AudioConnectionPool.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of connection pooling for audio providers. - /// - public class AudioConnectionPool : IAudioConnectionPool, IDisposable - { - private readonly ILogger _logger; - private readonly IHttpClientFactory _httpClientFactory; - private readonly AudioConnectionPoolOptions _options; - private readonly ConcurrentDictionary _pools = new(); - private readonly Timer _cleanupTimer; - private bool _disposed; - - /// - /// Initializes a new instance of the class. - /// - public AudioConnectionPool( - ILogger logger, - IHttpClientFactory httpClientFactory, - IOptions options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _httpClientFactory = httpClientFactory ?? throw new ArgumentNullException(nameof(httpClientFactory)); - ArgumentNullException.ThrowIfNull(options); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - - // Start cleanup timer - _cleanupTimer = new Timer( - CleanupCallback, - null, - TimeSpan.FromMinutes(1), - TimeSpan.FromMinutes(1)); - } - - /// - public async Task GetConnectionAsync( - string provider, - CancellationToken cancellationToken = default) - { - var pool = _pools.GetOrAdd(provider, p => new ProviderConnectionPool(p, _options)); - - // Try to get an existing healthy connection - if (pool.TryGetConnection(out var connection) && connection?.IsHealthy == true) - { - _logger.LogDebug("Reusing connection {ConnectionId} for {Provider}", connection.ConnectionId, provider); - return connection; - } - - // Create a new connection - connection = await CreateConnectionAsync(provider, cancellationToken); - pool.AddConnection(connection); - - _logger.LogInformation("Created new connection {ConnectionId} for {Provider}", connection.ConnectionId, provider); - return connection; - } - - /// - public Task ReturnConnectionAsync(IAudioProviderConnection connection) - { - if (connection == null) - { - return Task.CompletedTask; - } - - if (_pools.TryGetValue(connection.Provider, out var pool)) - { - pool.ReturnConnection(connection); - _logger.LogDebug("Returned connection {ConnectionId} to pool", connection.ConnectionId); - } - - return Task.CompletedTask; - } - - /// - public Task GetStatisticsAsync(string? provider = null) - { - var stats = new ConnectionPoolStatistics(); - - var pools = provider != null && _pools.TryGetValue(provider, out var pool) - ? new[] { pool } - : _pools.Values.ToArray(); - - foreach (var p in pools) - { - var poolStats = p.GetStatistics(); - stats.TotalCreated += poolStats.TotalCreated; - stats.ActiveConnections += poolStats.ActiveConnections; - stats.IdleConnections += poolStats.IdleConnections; - stats.UnhealthyConnections += poolStats.UnhealthyConnections; - stats.TotalRequests += poolStats.TotalRequests; - - stats.ProviderStats[p.Provider] = new ProviderPoolStatistics - { - Provider = p.Provider, - ConnectionCount = poolStats.TotalCreated, - ActiveCount = poolStats.ActiveConnections, - AverageAge = poolStats.AverageAge, - RequestsPerConnection = poolStats.TotalCreated > 0 - ? (double)poolStats.TotalRequests / poolStats.TotalCreated - : 0 - }; - } - - stats.HitRate = stats.TotalRequests > 0 && stats.TotalCreated > 0 - ? 1.0 - ((double)stats.TotalCreated / stats.TotalRequests) - : 0; - - return Task.FromResult(stats); - } - - /// - public async Task ClearIdleConnectionsAsync(TimeSpan maxIdleTime) - { - var totalCleared = 0; - - foreach (var pool in _pools.Values) - { - var cleared = await pool.ClearIdleConnectionsAsync(maxIdleTime); - totalCleared += cleared; - } - - if (totalCleared > 0) - { - _logger.LogInformation("Cleared {Count} idle connections", totalCleared); - } - - return totalCleared; - } - - /// - public async Task WarmupAsync( - string provider, - int connectionCount, - CancellationToken cancellationToken = default) - { - _logger.LogInformation("Warming up {Count} connections for {Provider}", connectionCount, provider); - - var pool = _pools.GetOrAdd(provider, p => new ProviderConnectionPool(p, _options)); - var tasks = new List(); - - for (int i = 0; i < connectionCount; i++) - { - tasks.Add(Task.Run(async () => - { - try - { - var connection = await CreateConnectionAsync(provider, cancellationToken); - pool.AddConnection(connection); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create warmup connection for {Provider}", provider); - } - }, cancellationToken)); - } - - await Task.WhenAll(tasks); - _logger.LogInformation("Warmup completed for {Provider}", provider); - } - - private async Task CreateConnectionAsync( - string provider, - CancellationToken cancellationToken) - { - var httpClient = _httpClientFactory.CreateClient($"AudioProvider_{provider}"); - - // Configure HTTP client - httpClient.Timeout = TimeSpan.FromSeconds(_options.ConnectionTimeout); - httpClient.DefaultRequestHeaders.Add("X-Provider", provider); - - var connection = new AudioProviderConnection(provider, httpClient); - - // Validate the connection - if (!await connection.ValidateAsync(cancellationToken)) - { - throw new InvalidOperationException($"Failed to create healthy connection for {provider}"); - } - - return connection; - } - - private void CleanupCallback(object? state) - { - try - { - var task = ClearIdleConnectionsAsync(_options.MaxIdleTime); - task.Wait(TimeSpan.FromSeconds(30)); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during connection pool cleanup"); - } - } - - public void Dispose() - { - if (!_disposed) - { - _cleanupTimer?.Dispose(); - - foreach (var pool in _pools.Values) - { - pool.Dispose(); - } - - _pools.Clear(); - _disposed = true; - } - } - } - - /// - /// Connection pool for a specific provider. - /// - internal class ProviderConnectionPool : IDisposable - { - private readonly string _provider; - private readonly AudioConnectionPoolOptions _options; - private readonly ConcurrentBag _connections = new(); - private readonly ConcurrentDictionary _activeConnections = new(); - private long _totalCreated; - private long _totalRequests; - - public string Provider => _provider; - - public ProviderConnectionPool(string provider, AudioConnectionPoolOptions options) - { - _provider = provider; - _options = options; - } - - public bool TryGetConnection(out AudioProviderConnection? connection) - { - Interlocked.Increment(ref _totalRequests); - - while (_connections.TryTake(out connection)) - { - if (connection.IsHealthy && !IsExpired(connection)) - { - _activeConnections[connection.ConnectionId] = connection; - return true; - } - - connection.Dispose(); - } - - connection = null; - return false; - } - - public void AddConnection(AudioProviderConnection connection) - { - Interlocked.Increment(ref _totalCreated); - _activeConnections[connection.ConnectionId] = connection; - } - - public void ReturnConnection(IAudioProviderConnection connection) - { - if (connection is AudioProviderConnection conn && - _activeConnections.TryRemove(conn.ConnectionId, out _)) - { - if (conn.IsHealthy && !IsExpired(conn) && _connections.Count < _options.MaxConnectionsPerProvider) - { - _connections.Add(conn); - } - else - { - conn.Dispose(); - } - } - } - - public PoolStatistics GetStatistics() - { - var connections = _connections.ToArray(); - var now = DateTime.UtcNow; - - return new PoolStatistics - { - TotalCreated = (int)_totalCreated, - ActiveConnections = _activeConnections.Count, - IdleConnections = connections.Length, - UnhealthyConnections = connections.Count(c => !c.IsHealthy), - TotalRequests = _totalRequests, - AverageAge = connections.Length > 0 - ? TimeSpan.FromMilliseconds(connections.Average(c => (now - c.CreatedAt).TotalMilliseconds)) - : TimeSpan.Zero - }; - } - - public Task ClearIdleConnectionsAsync(TimeSpan maxIdleTime) - { - var cleared = 0; - var now = DateTime.UtcNow; - var toDispose = new List(); - - // Check idle connections - var connections = _connections.ToArray(); - foreach (var conn in connections) - { - if (now - conn.LastUsedAt > maxIdleTime) - { - if (_connections.TryTake(out var removed) && removed.ConnectionId == conn.ConnectionId) - { - toDispose.Add(removed); - cleared++; - } - } - } - - // Dispose connections - foreach (var conn in toDispose) - { - conn.Dispose(); - } - - return Task.FromResult(cleared); - } - - private bool IsExpired(AudioProviderConnection connection) - { - return DateTime.UtcNow - connection.CreatedAt > _options.MaxConnectionAge; - } - - public void Dispose() - { - foreach (var conn in _connections) - { - conn.Dispose(); - } - - foreach (var conn in _activeConnections.Values) - { - conn.Dispose(); - } - - _connections.Clear(); - _activeConnections.Clear(); - } - } - - /// - /// Implementation of audio provider connection. - /// - internal class AudioProviderConnection : IAudioProviderConnection - { - private readonly HttpClient _httpClient; - private bool _disposed; - - public string Provider { get; } - public string ConnectionId { get; } - public bool IsHealthy { get; private set; } - public DateTime CreatedAt { get; } - public DateTime LastUsedAt { get; private set; } - public HttpClient HttpClient => _httpClient; - - public AudioProviderConnection(string provider, HttpClient httpClient) - { - Provider = provider; - ConnectionId = Guid.NewGuid().ToString(); - _httpClient = httpClient; - CreatedAt = DateTime.UtcNow; - LastUsedAt = DateTime.UtcNow; - IsHealthy = true; - } - - public async Task ValidateAsync(CancellationToken cancellationToken = default) - { - try - { - // Simple health check - adjust based on provider - var response = await _httpClient.GetAsync("/health", cancellationToken); - IsHealthy = response.IsSuccessStatusCode; - LastUsedAt = DateTime.UtcNow; - return IsHealthy; - } - catch - { - IsHealthy = false; - return false; - } - } - - public void Dispose() - { - if (!_disposed) - { - _httpClient?.Dispose(); - _disposed = true; - } - } - } - - /// - /// Internal pool statistics. - /// - internal class PoolStatistics - { - public int TotalCreated { get; set; } - public int ActiveConnections { get; set; } - public int IdleConnections { get; set; } - public int UnhealthyConnections { get; set; } - public long TotalRequests { get; set; } - public TimeSpan AverageAge { get; set; } - } - - /// - /// Options for audio connection pooling. - /// - public class AudioConnectionPoolOptions - { - /// - /// Gets or sets the maximum connections per provider. - /// - public int MaxConnectionsPerProvider { get; set; } = 10; - - /// - /// Gets or sets the maximum connection age. - /// - public TimeSpan MaxConnectionAge { get; set; } = TimeSpan.FromHours(1); - - /// - /// Gets or sets the maximum idle time before cleanup. - /// - public TimeSpan MaxIdleTime { get; set; } = TimeSpan.FromMinutes(15); - - /// - /// Gets or sets the connection timeout in seconds. - /// - public int ConnectionTimeout { get; set; } = 30; - } -} diff --git a/ConduitLLM.Core/Services/AudioContentFilter.cs b/ConduitLLM.Core/Services/AudioContentFilter.cs deleted file mode 100644 index 5d5b533b1..000000000 --- a/ConduitLLM.Core/Services/AudioContentFilter.cs +++ /dev/null @@ -1,215 +0,0 @@ -using System.Text.RegularExpressions; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of audio content filtering. - /// - public class AudioContentFilter : IAudioContentFilter - { - private readonly ILogger _logger; - private readonly IAudioAuditLogger _auditLogger; - - // Simple patterns for demo - in production, use ML models or external services - private readonly Dictionary> _inappropriatePatterns = new() - { - ["profanity"] = new() { @"\b(badword1|badword2|badword3)\b" }, - ["violence"] = new() { @"\b(threat|kill|hurt|violence)\b" }, - ["harassment"] = new() { @"\b(harass|bully|intimidate)\b" }, - ["hate_speech"] = new() { @"\b(hate|discriminate)\b" } - }; - - /// - /// Initializes a new instance of the class. - /// - public AudioContentFilter( - ILogger logger, - IAudioAuditLogger auditLogger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _auditLogger = auditLogger ?? throw new ArgumentNullException(nameof(auditLogger)); - } - - /// - public async Task FilterTranscriptionAsync( - string text, - string virtualKey, - CancellationToken cancellationToken = default) - { - var startTime = DateTime.UtcNow; - var result = await FilterTextInternalAsync(text, AudioOperation.Transcription); - - // Audit the filtering operation - await _auditLogger.LogContentFilteringAsync( - new ContentFilterAuditEntry - { - Operation = AudioOperation.Transcription, - VirtualKey = virtualKey, - WasBlocked = !result.IsApproved, - WasModified = result.WasModified, - ViolationCategories = result.ViolationCategories, - ConfidenceScore = result.ConfidenceScore, - Success = true, - DurationMs = (long)(DateTime.UtcNow - startTime).TotalMilliseconds - }, - cancellationToken); - - return result; - } - - /// - public async Task FilterTextToSpeechAsync( - string text, - string virtualKey, - CancellationToken cancellationToken = default) - { - var startTime = DateTime.UtcNow; - var result = await FilterTextInternalAsync(text, AudioOperation.TextToSpeech); - - // Audit the filtering operation - await _auditLogger.LogContentFilteringAsync( - new ContentFilterAuditEntry - { - Operation = AudioOperation.TextToSpeech, - VirtualKey = virtualKey, - WasBlocked = !result.IsApproved, - WasModified = result.WasModified, - ViolationCategories = result.ViolationCategories, - ConfidenceScore = result.ConfidenceScore, - Success = true, - DurationMs = (long)(DateTime.UtcNow - startTime).TotalMilliseconds - }, - cancellationToken); - - return result; - } - - /// - public async Task ValidateAudioContentAsync( - byte[] audioData, - AudioFormat format, - string virtualKey, - CancellationToken cancellationToken = default) - { - // In a real implementation, this might: - // 1. Use speech recognition to convert to text - // 2. Analyze audio characteristics for inappropriate content - // 3. Check against audio fingerprinting databases - - _logger.LogDebug( - "Validating audio content for format {Format}, size {Size} bytes", - format, - audioData.Length); - - // For now, just check file size limits - const int maxSizeMb = 100; - if (audioData.Length > maxSizeMb * 1024 * 1024) - { - _logger.LogWarning("Audio file too large: {Size} MB", audioData.Length / 1024 / 1024); - return false; - } - - return await Task.FromResult(true); - } - - private Task FilterTextInternalAsync( - string text, - AudioOperation operation) - { - var result = new ContentFilterResult - { - FilteredText = text, - IsApproved = true, - WasModified = false, - ConfidenceScore = 1.0 - }; - - if (string.IsNullOrWhiteSpace(text)) - { - return Task.FromResult(result); - } - - var filteredText = text; - var details = new List(); - - // Check each category of inappropriate content - foreach (var category in _inappropriatePatterns) - { - foreach (var pattern in category.Value) - { - var regex = new Regex(pattern, RegexOptions.IgnoreCase); - var matches = regex.Matches(text); - - foreach (Match match in matches) - { - result.ViolationCategories.Add(category.Key); - - var detail = new ContentFilterDetail - { - Type = category.Key, - Severity = DetermineSeverity(category.Key), - OriginalText = match.Value, - ReplacementText = new string('*', match.Value.Length), - StartIndex = match.Index, - EndIndex = match.Index + match.Length - }; - - details.Add(detail); - - // Replace with asterisks - filteredText = filteredText.Replace( - match.Value, - detail.ReplacementText, - StringComparison.OrdinalIgnoreCase); - } - } - } - - if (details.Count() > 0) - { - result.WasModified = true; - result.FilteredText = filteredText; - result.Details = details; - - // Determine if content should be blocked entirely - var criticalViolations = details.Count(d => d.Severity == FilterSeverity.Critical); - var highViolations = details.Count(d => d.Severity == FilterSeverity.High); - - if (criticalViolations > 0 || highViolations > 2) - { - result.IsApproved = false; - result.ConfidenceScore = 0.1; - } - else - { - result.ConfidenceScore = 1.0 - (details.Count * 0.1); - } - - _logger.LogWarning( - "Content filtering detected {Count} issues in {Operation}: {Categories}", - details.Count, - operation, - string.Join(", ", result.ViolationCategories.Distinct())); - } - - return Task.FromResult(result); - } - - private FilterSeverity DetermineSeverity(string category) - { - return category switch - { - "profanity" => FilterSeverity.Medium, - "violence" => FilterSeverity.High, - "harassment" => FilterSeverity.High, - "hate_speech" => FilterSeverity.Critical, - _ => FilterSeverity.Low - }; - } - } -} diff --git a/ConduitLLM.Core/Services/AudioEncryptionService.cs b/ConduitLLM.Core/Services/AudioEncryptionService.cs deleted file mode 100644 index 7c96f104a..000000000 --- a/ConduitLLM.Core/Services/AudioEncryptionService.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Collections.Concurrent; -using System.Security.Cryptography; -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of audio encryption service using AES-256-GCM. - /// - public class AudioEncryptionService : IAudioEncryptionService - { - private readonly ILogger _logger; - private readonly ConcurrentDictionary _keyStore = new(); // In production, use secure key management - - /// - /// Initializes a new instance of the class. - /// - public AudioEncryptionService(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task EncryptAudioAsync( - byte[] audioData, - AudioEncryptionMetadata? metadata = null, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - { - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - } - - try - { - using var aesGcm = new AesGcm(await GetOrCreateKeyAsync(), 16); // 16 byte tag size - - // Generate nonce/IV - var nonce = new byte[12]; // AES-GCM nonce is 12 bytes - RandomNumberGenerator.Fill(nonce); - - // Prepare ciphertext and tag - var ciphertext = new byte[audioData.Length]; - var tag = new byte[16]; // AES-GCM tag is 16 bytes - - // Encrypt metadata if provided - byte[]? associatedData = null; - string? encryptedMetadata = null; - if (metadata != null) - { - var metadataJson = JsonSerializer.Serialize(metadata); - associatedData = Encoding.UTF8.GetBytes(metadataJson); - encryptedMetadata = Convert.ToBase64String(associatedData); - } - - // Perform encryption - aesGcm.Encrypt(nonce, audioData, ciphertext, tag, associatedData); - - var result = new EncryptedAudioData - { - EncryptedBytes = ciphertext, - IV = nonce, - AuthTag = tag, - KeyId = "default", // In production, use proper key rotation - Algorithm = "AES-256-GCM", - EncryptedMetadata = encryptedMetadata, - EncryptedAt = DateTime.UtcNow - }; - - _logger.LogDebug( - "Encrypted audio data: {Size} bytes -> {EncryptedSize} bytes", - audioData.Length, - ciphertext.Length); - - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to encrypt audio data"); - throw new InvalidOperationException("Audio encryption failed", ex); - } - } - - /// - public async Task DecryptAudioAsync( - EncryptedAudioData encryptedData, - CancellationToken cancellationToken = default) - { - if (encryptedData == null) - { - throw new ArgumentNullException(nameof(encryptedData)); - } - - try - { - var key = await GetKeyAsync(encryptedData.KeyId); - if (key == null) - { - throw new InvalidOperationException($"Key not found: {encryptedData.KeyId}"); - } - - using var aesGcm = new AesGcm(key, 16); // 16 byte tag size - - // Prepare plaintext buffer - var plaintext = new byte[encryptedData.EncryptedBytes.Length]; - - // Prepare associated data if metadata exists - byte[]? associatedData = null; - if (!string.IsNullOrEmpty(encryptedData.EncryptedMetadata)) - { - associatedData = Convert.FromBase64String(encryptedData.EncryptedMetadata); - } - - // Perform decryption - aesGcm.Decrypt( - encryptedData.IV, - encryptedData.EncryptedBytes, - encryptedData.AuthTag, - plaintext, - associatedData); - - _logger.LogDebug( - "Decrypted audio data: {EncryptedSize} bytes -> {Size} bytes", - encryptedData.EncryptedBytes.Length, - plaintext.Length); - - return plaintext; - } - catch (CryptographicException ex) - { - _logger.LogError(ex, "Failed to decrypt audio data - authentication failed"); - throw new InvalidOperationException("Audio decryption failed - data may be tampered", ex); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to decrypt audio data"); - throw new InvalidOperationException("Audio decryption failed", ex); - } - } - - /// - public Task GenerateKeyAsync() - { - var key = new byte[32]; // 256 bits - RandomNumberGenerator.Fill(key); - - var keyId = Guid.NewGuid().ToString(); - _keyStore.TryAdd(keyId, key); - - _logger.LogInformation("Generated new encryption key: {KeyId}", keyId); - - return Task.FromResult(keyId); - } - - /// - public async Task ValidateIntegrityAsync(EncryptedAudioData encryptedData) - { - if (encryptedData == null) - { - return false; - } - - try - { - // Attempt to decrypt - if it fails, integrity is compromised - var decrypted = await DecryptAudioAsync(encryptedData); - return decrypted != null && decrypted.Length > 0; - } - catch (Exception ex) - { - _logger.LogDebug(ex, "Audio integrity validation failed - decryption unsuccessful"); - return false; - } - } - - private Task GetOrCreateKeyAsync() - { - // In production, this would retrieve from secure key management - var key = _keyStore.GetOrAdd("default", keyId => - { - var newKey = new byte[32]; - RandomNumberGenerator.Fill(newKey); - return newKey; - }); - - return Task.FromResult(key); - } - - private Task GetKeyAsync(string keyId) - { - _keyStore.TryGetValue(keyId, out var key); - return Task.FromResult(key); - } - } -} diff --git a/ConduitLLM.Core/Services/AudioMetricsCollector.Recording.cs b/ConduitLLM.Core/Services/AudioMetricsCollector.Recording.cs deleted file mode 100644 index 8a876e4d4..000000000 --- a/ConduitLLM.Core/Services/AudioMetricsCollector.Recording.cs +++ /dev/null @@ -1,185 +0,0 @@ -using Microsoft.Extensions.Logging; -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Partial class containing metric recording functionality. - /// - public partial class AudioMetricsCollector - { - /// - public Task RecordTranscriptionMetricAsync(TranscriptionMetric metric) - { - try - { - var bucket = GetOrCreateBucket(DateTime.UtcNow); - - bucket.TranscriptionMetrics.Add(metric); - bucket.UpdateOperation(AudioOperation.Transcription, metric.Success, metric.DurationMs); - - if (metric.ServedFromCache) - { - Interlocked.Increment(ref bucket.CacheHits); - } - - _logger.LogDebug("Recorded transcription metric: Provider={Provider}, Duration={Duration}ms, Success={Success}", - metric.Provider.Replace(Environment.NewLine, ""), - metric.DurationMs, - metric.Success); - - // Check for anomalies - if (metric.DurationMs > _options.TranscriptionLatencyThreshold) - { - _logger.LogWarning( - "High transcription latency detected: {Duration}ms (threshold: {Threshold}ms)", - metric.DurationMs, _options.TranscriptionLatencyThreshold); - } - - return Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error recording transcription metric"); - return Task.CompletedTask; - } - } - - /// - public Task RecordTtsMetricAsync(TtsMetric metric) - { - try - { - var bucket = GetOrCreateBucket(DateTime.UtcNow); - - bucket.TtsMetrics.Add(metric); - bucket.UpdateOperation(AudioOperation.TextToSpeech, metric.Success, metric.DurationMs); - - if (metric.ServedFromCache) - { - Interlocked.Increment(ref bucket.CacheHits); - } - - if (metric.UploadedToCdn) - { - Interlocked.Increment(ref bucket.CdnUploads); - } - - _logger.LogDebug("Recorded TTS metric: Provider={Provider}, Voice={Voice}, Duration={Duration}ms", - metric.Provider.Replace(Environment.NewLine, ""), - metric.Voice.Replace(Environment.NewLine, ""), - metric.DurationMs); - - return Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error recording TTS metric"); - return Task.CompletedTask; - } - } - - /// - public Task RecordRealtimeMetricAsync(RealtimeMetric metric) - { - try - { - var bucket = GetOrCreateBucket(DateTime.UtcNow); - - bucket.RealtimeMetrics.Add(metric); - bucket.UpdateOperation(AudioOperation.Realtime, metric.Success, metric.DurationMs); - - // Track session statistics - Interlocked.Add(ref bucket.TotalRealtimeSeconds, (long)metric.SessionDurationSeconds); - Interlocked.Add(ref bucket.TotalRealtimeTurns, metric.TurnCount); - - _logger.LogDebug("Recorded realtime metric: Provider={Provider}, Session={SessionId}, Duration={Duration}s", - metric.Provider.Replace(Environment.NewLine, ""), - metric.SessionId, - metric.SessionDurationSeconds); - - // Check for high latency - if (metric.AverageLatencyMs > _options.RealtimeLatencyThreshold) - { - _logger.LogWarning( - "High realtime latency detected: {Latency}ms (threshold: {Threshold}ms)", - metric.AverageLatencyMs, _options.RealtimeLatencyThreshold); - } - - return Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error recording realtime metric"); - return Task.CompletedTask; - } - } - - /// - public Task RecordRoutingMetricAsync(RoutingMetric metric) - { - try - { - var bucket = GetOrCreateBucket(DateTime.UtcNow); - - bucket.RoutingMetrics.Add(metric); - - // Track routing decisions - bucket.TrackRoutingDecision(metric.SelectedProvider, metric.RoutingStrategy); - - _logger.LogDebug("Recorded routing metric: Operation={Operation}, Provider={Provider}, Strategy={Strategy}", - metric.Operation.ToString(), - metric.SelectedProvider.Replace(Environment.NewLine, ""), - metric.RoutingStrategy.Replace(Environment.NewLine, "")); - - return Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error recording routing metric"); - return Task.CompletedTask; - } - } - - /// - public Task RecordProviderHealthMetricAsync(ProviderHealthMetric metric) - { - try - { - var bucket = GetOrCreateBucket(DateTime.UtcNow); - - bucket.ProviderHealthMetrics.Add(metric); - - // Update provider statistics - bucket.UpdateProviderHealth(metric.Provider, metric.IsHealthy, metric.ErrorRate); - - _logger.LogDebug("Recorded provider health: Provider={Provider}, Healthy={Healthy}, ErrorRate={ErrorRate}%", - metric.Provider.Replace(Environment.NewLine, ""), - metric.IsHealthy, - metric.ErrorRate * 100); - - // Alert on provider issues - if (!metric.IsHealthy && _alertingService != null) - { - _ = Task.Run(async () => - { - await _alertingService.EvaluateMetricsAsync( - await GetCurrentSnapshotAsync()); - }); - } - - return Task.CompletedTask; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error recording provider health metric"); - return Task.CompletedTask; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioMetricsCollector.Retrieval.cs b/ConduitLLM.Core/Services/AudioMetricsCollector.Retrieval.cs deleted file mode 100644 index 51b4ea1f4..000000000 --- a/ConduitLLM.Core/Services/AudioMetricsCollector.Retrieval.cs +++ /dev/null @@ -1,358 +0,0 @@ -using Microsoft.Extensions.Logging; -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Partial class containing metric retrieval and aggregation functionality. - /// - public partial class AudioMetricsCollector - { - /// - public Task GetAggregatedMetricsAsync( - DateTime startTime, - DateTime endTime, - string? provider = null) - { - _aggregationLock.EnterReadLock(); - try - { - var relevantBuckets = _metricsBuckets.Values - .Where(b => b.Timestamp >= startTime && b.Timestamp <= endTime) - .ToList(); - - var aggregated = new AggregatedAudioMetrics - { - Period = new DateTimeRange { Start = startTime, End = endTime } - }; - - // Aggregate transcription metrics - var transcriptionMetrics = relevantBuckets - .SelectMany(b => b.TranscriptionMetrics) - .Where(m => provider == null || m.Provider == provider) - .ToList(); - - aggregated.Transcription = AggregateOperationMetrics(transcriptionMetrics); - - // Aggregate TTS metrics - var ttsMetrics = relevantBuckets - .SelectMany(b => b.TtsMetrics) - .Where(m => provider == null || m.Provider == provider) - .ToList(); - - aggregated.TextToSpeech = AggregateOperationMetrics(ttsMetrics); - - // Aggregate realtime metrics - var realtimeMetrics = relevantBuckets - .SelectMany(b => b.RealtimeMetrics) - .Where(m => provider == null || m.Provider == provider) - .ToList(); - - aggregated.Realtime = AggregateRealtimeMetrics(realtimeMetrics); - - // Aggregate provider statistics - aggregated.ProviderStats = AggregateProviderStats(relevantBuckets, provider); - - // Calculate costs - aggregated.Costs = CalculateCosts(relevantBuckets, provider); - - return Task.FromResult(aggregated); - } - finally - { - _aggregationLock.ExitReadLock(); - } - } - - /// - public async Task GetCurrentSnapshotAsync() - { - var now = DateTime.UtcNow; - var recentBuckets = _metricsBuckets.Values - .Where(b => b.Timestamp >= now.AddMinutes(-5)) - .ToList(); - - var snapshot = new AudioMetricsSnapshot - { - Timestamp = now, - ActiveTranscriptions = CountActiveOperations(recentBuckets, AudioOperation.Transcription), - ActiveTtsOperations = CountActiveOperations(recentBuckets, AudioOperation.TextToSpeech), - ActiveRealtimeSessions = CountActiveOperations(recentBuckets, AudioOperation.Realtime), - RequestsPerSecond = CalculateRequestRate(recentBuckets), - CurrentErrorRate = CalculateErrorRate(recentBuckets), - ProviderHealth = GetProviderHealthStatus(recentBuckets), - Resources = await GetSystemResourcesAsync() - }; - - return snapshot; - } - - private MetricsBucket GetOrCreateBucket(DateTime timestamp) - { - var bucketKey = GetBucketKey(timestamp); - return _metricsBuckets.GetOrAdd(bucketKey, _ => new MetricsBucket { Timestamp = timestamp }); - } - - private string GetBucketKey(DateTime timestamp) - { - // Round to nearest minute - var rounded = new DateTime( - timestamp.Year, - timestamp.Month, - timestamp.Day, - timestamp.Hour, - timestamp.Minute, - 0, - DateTimeKind.Utc); - return rounded.ToString("yyyy-MM-dd-HH-mm"); - } - - private void AggregateMetrics(object? state) - { - try - { - _aggregationLock.EnterWriteLock(); - - // Clean up old buckets - var cutoff = DateTime.UtcNow.Subtract(_options.RetentionPeriod); - var keysToRemove = _metricsBuckets - .Where(kvp => kvp.Value.Timestamp < cutoff) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in keysToRemove) - { - _metricsBuckets.TryRemove(key, out _); - } - - _logger.LogDebug("Cleaned up {Count} old metric buckets", - keysToRemove.Count()); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error during metrics aggregation"); - } - finally - { - _aggregationLock.ExitWriteLock(); - } - } - - private OperationStatistics AggregateOperationMetrics(List metrics) where T : AudioMetricBase - { - if (metrics.Count() == 0) - { - return new OperationStatistics(); - } - - var successful = metrics.Where(m => m.Success).ToList(); - var durations = metrics.Select(m => m.DurationMs).OrderBy(d => d).ToList(); - - return new OperationStatistics - { - TotalRequests = metrics.Count(), - SuccessfulRequests = successful.Count(), - FailedRequests = metrics.Count() - successful.Count(), - AverageDurationMs = durations.Average(), - P95DurationMs = GetPercentile(durations, 0.95), - P99DurationMs = GetPercentile(durations, 0.99), - CacheHitRate = CalculateCacheHitRate(metrics), - TotalDataBytes = CalculateTotalDataBytes(metrics) - }; - } - - private RealtimeStatistics AggregateRealtimeMetrics(List metrics) - { - if (metrics.Count() == 0) - { - return new RealtimeStatistics(); - } - - var disconnectReasons = metrics - .Where(m => !string.IsNullOrEmpty(m.DisconnectReason)) - .GroupBy(m => m.DisconnectReason!) - .ToDictionary(g => g.Key, g => (long)g.Count()); - - return new RealtimeStatistics - { - TotalSessions = metrics.Count(), - AverageSessionDurationSeconds = metrics.Average(m => m.SessionDurationSeconds), - TotalAudioMinutes = metrics.Sum(m => (m.TotalAudioSentSeconds + m.TotalAudioReceivedSeconds) / 60), - AverageLatencyMs = metrics.Average(m => m.AverageLatencyMs), - DisconnectReasons = disconnectReasons - }; - } - - private Dictionary AggregateProviderStats( - List buckets, - string? provider) - { - var allMetrics = buckets - .SelectMany(b => b.TranscriptionMetrics.Cast() - .Concat(b.TtsMetrics) - .Concat(b.RealtimeMetrics)) - .Where(m => provider == null || m.Provider == provider) - .GroupBy(m => m.Provider) - .ToList(); - - var result = new Dictionary(); - - foreach (var group in allMetrics) - { - var providerMetrics = group.ToList(); - var successful = providerMetrics.Count(m => m.Success); - var errorGroups = providerMetrics - .Where(m => !m.Success && !string.IsNullOrEmpty(m.ErrorCode)) - .GroupBy(m => m.ErrorCode!) - .ToDictionary(g => g.Key, g => (long)g.Count()); - - result[group.Key] = new ProviderStatistics - { - Provider = group.Key, - RequestCount = providerMetrics.Count(), - SuccessRate = providerMetrics.Count() > 0 ? (double)successful / providerMetrics.Count() : 0, - AverageLatencyMs = providerMetrics.Average(m => m.DurationMs), - UptimePercentage = CalculateUptime(buckets, group.Key), - ErrorBreakdown = errorGroups - }; - } - - return result; - } - - private CostAnalysis CalculateCosts(List buckets, string? provider) - { - // This is a simplified cost calculation - // In production, this would integrate with actual billing data - var costs = new CostAnalysis(); - - var transcriptionMinutes = buckets - .SelectMany(b => b.TranscriptionMetrics) - .Where(m => provider == null || m.Provider == provider) - .Sum(m => m.AudioDurationSeconds / 60); - - var ttsCharacters = buckets - .SelectMany(b => b.TtsMetrics) - .Where(m => provider == null || m.Provider == provider) - .Sum(m => m.CharacterCount); - - var realtimeMinutes = buckets - .SelectMany(b => b.RealtimeMetrics) - .Where(m => provider == null || m.Provider == provider) - .Sum(m => m.SessionDurationSeconds / 60); - - // Example cost rates (would come from configuration) - costs.TranscriptionCost = (decimal)(transcriptionMinutes * 0.006); // $0.006/minute - costs.TextToSpeechCost = (decimal)(ttsCharacters * 0.000016); // $16/1M chars - costs.RealtimeCost = (decimal)(realtimeMinutes * 0.06); // $0.06/minute - costs.TotalCost = costs.TranscriptionCost + costs.TextToSpeechCost + costs.RealtimeCost; - - // Calculate cache savings - var cacheHits = buckets.Sum(b => b.CacheHits); - costs.CachingSavings = (decimal)(cacheHits * 0.001); // Estimated savings per cache hit - - return costs; - } - - private double GetPercentile(List sortedValues, double percentile) - { - if (sortedValues.Count() == 0) return 0; - - var index = (int)Math.Ceiling(percentile * sortedValues.Count()) - 1; - return sortedValues[Math.Max(0, Math.Min(index, sortedValues.Count() - 1))]; - } - - private double CalculateCacheHitRate(List metrics) where T : AudioMetricBase - { - if (metrics is List transcriptions) - { - var cached = transcriptions.Count(m => m.ServedFromCache); - return transcriptions.Count() > 0 ? (double)cached / transcriptions.Count() : 0; - } - - if (metrics is List ttsMetrics) - { - var cached = ttsMetrics.Count(m => m.ServedFromCache); - return ttsMetrics.Count() > 0 ? (double)cached / ttsMetrics.Count() : 0; - } - - return 0; - } - - private long CalculateTotalDataBytes(List metrics) where T : AudioMetricBase - { - if (metrics is List transcriptions) - { - return transcriptions.Sum(m => m.FileSizeBytes); - } - - if (metrics is List ttsMetrics) - { - return ttsMetrics.Sum(m => m.OutputSizeBytes); - } - - return 0; - } - - private int CountActiveOperations(List buckets, AudioOperation operation) - { - return buckets.Sum(b => b.ActiveOperations.GetValueOrDefault(operation, 0)); - } - - private double CalculateRequestRate(List buckets) - { - if (buckets.Count() == 0) return 0; - - var totalRequests = buckets.Sum(b => b.TotalRequests); - var timeSpan = buckets.Max(b => b.Timestamp) - buckets.Min(b => b.Timestamp); - - return timeSpan.TotalSeconds > 0 ? totalRequests / timeSpan.TotalSeconds : 0; - } - - private double CalculateErrorRate(List buckets) - { - var total = buckets.Sum(b => b.TotalRequests); - var errors = buckets.Sum(b => b.FailedRequests); - - return total > 0 ? (double)errors / total : 0; - } - - private Dictionary GetProviderHealthStatus(List buckets) - { - var latestHealth = buckets - .SelectMany(b => b.ProviderHealthMetrics) - .GroupBy(h => h.Provider) - .ToDictionary( - g => g.Key, - g => g.OrderByDescending(h => h.Timestamp).First().IsHealthy); - - return latestHealth; - } - - private double CalculateUptime(List buckets, string provider) - { - var healthMetrics = buckets - .SelectMany(b => b.ProviderHealthMetrics) - .Where(h => h.Provider == provider) - .ToList(); - - if (healthMetrics.Count() == 0) return 100; - - var healthyCount = healthMetrics.Count(h => h.IsHealthy); - return (double)healthyCount / healthMetrics.Count() * 100; - } - - private async Task GetSystemResourcesAsync() - { - // This would integrate with actual system monitoring - return await Task.FromResult(new SystemResources - { - CpuUsagePercent = 45, - MemoryUsageMb = 2048, - ActiveConnections = 50, - CacheSizeMb = 512 - }); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioMetricsCollector.Support.cs b/ConduitLLM.Core/Services/AudioMetricsCollector.Support.cs deleted file mode 100644 index 04b39591f..000000000 --- a/ConduitLLM.Core/Services/AudioMetricsCollector.Support.cs +++ /dev/null @@ -1,83 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Metrics bucket for time-based aggregation. - /// - internal class MetricsBucket - { - public DateTime Timestamp { get; set; } - public ConcurrentBag TranscriptionMetrics { get; } = new(); - public ConcurrentBag TtsMetrics { get; } = new(); - public ConcurrentBag RealtimeMetrics { get; } = new(); - public ConcurrentBag RoutingMetrics { get; } = new(); - public ConcurrentBag ProviderHealthMetrics { get; } = new(); - - public ConcurrentDictionary ActiveOperations { get; } = new(); - public ConcurrentDictionary ProviderRequests { get; } = new(); - public ConcurrentDictionary RoutingStrategies { get; } = new(); - - public long TotalRequests; - public long SuccessfulRequests; - public long FailedRequests; - public long CacheHits; - public long CdnUploads; - public long TotalRealtimeSeconds; - public long TotalRealtimeTurns; - - public void UpdateOperation(AudioOperation operation, bool success, double durationMs) - { - Interlocked.Increment(ref TotalRequests); - if (success) - { - Interlocked.Increment(ref SuccessfulRequests); - } - else - { - Interlocked.Increment(ref FailedRequests); - } - - ActiveOperations.AddOrUpdate(operation, 1, (_, count) => count + 1); - } - - public void TrackRoutingDecision(string provider, string strategy) - { - ProviderRequests.AddOrUpdate(provider, 1, (_, count) => count + 1); - RoutingStrategies.AddOrUpdate(strategy, 1, (_, count) => count + 1); - } - - public void UpdateProviderHealth(string provider, bool healthy, double errorRate) - { - // Provider health is tracked in the ProviderHealthMetrics collection - } - } - - /// - /// Options for audio metrics collection. - /// - public class AudioMetricsOptions - { - /// - /// Gets or sets the aggregation interval. - /// - public TimeSpan AggregationInterval { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// Gets or sets the retention period for metrics. - /// - public TimeSpan RetentionPeriod { get; set; } = TimeSpan.FromDays(7); - - /// - /// Gets or sets the transcription latency threshold. - /// - public double TranscriptionLatencyThreshold { get; set; } = 5000; // 5 seconds - - /// - /// Gets or sets the realtime latency threshold. - /// - public double RealtimeLatencyThreshold { get; set; } = 200; // 200ms - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioMetricsCollector.cs b/ConduitLLM.Core/Services/AudioMetricsCollector.cs deleted file mode 100644 index 7d4898adb..000000000 --- a/ConduitLLM.Core/Services/AudioMetricsCollector.cs +++ /dev/null @@ -1,56 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Collects and aggregates audio operation metrics. - /// - public partial class AudioMetricsCollector : IAudioMetricsCollector - { - private readonly ILogger _logger; - private readonly AudioMetricsOptions _options; - private readonly ConcurrentDictionary _metricsBuckets = new(); - private readonly ReaderWriterLockSlim _aggregationLock = new(); - private readonly Timer _aggregationTimer; - private readonly IAudioAlertingService? _alertingService; - - /// - /// Initializes a new instance of the class. - /// - public AudioMetricsCollector( - ILogger logger, - IOptions options, - IAudioAlertingService? alertingService = null) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _alertingService = alertingService; - - // Start aggregation timer - _aggregationTimer = new Timer( - AggregateMetrics, - null, - _options.AggregationInterval, - _options.AggregationInterval); - } - - public void Dispose() - { - try - { - _aggregationTimer?.Dispose(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error disposing aggregation timer"); - } - - _aggregationLock?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioPiiDetector.cs b/ConduitLLM.Core/Services/AudioPiiDetector.cs deleted file mode 100644 index 5ba0e0413..000000000 --- a/ConduitLLM.Core/Services/AudioPiiDetector.cs +++ /dev/null @@ -1,261 +0,0 @@ -using System.Text.RegularExpressions; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of PII detection and redaction for audio content. - /// - public class AudioPiiDetector : IAudioPiiDetector - { - private readonly ILogger _logger; - private readonly IAudioAuditLogger _auditLogger; - - // Regex patterns for common PII types - private readonly Dictionary _piiPatterns = new() - { - [PiiType.SSN] = @"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b", - [PiiType.CreditCard] = @"\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b", - [PiiType.Email] = @"\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b", - [PiiType.Phone] = @"\b(?:\+?1[-.\s]?)?\(?([0-9]{3})\)?[-.\s]?([0-9]{3})[-.\s]?([0-9]{4})\b", - [PiiType.DateOfBirth] = @"\b(?:0[1-9]|1[0-2])[-/](?:0[1-9]|[12][0-9]|3[01])[-/](?:19|20)\d{2}\b", - [PiiType.BankAccount] = @"\b\d{8,17}\b", - [PiiType.DriversLicense] = @"\b[A-Z]{1,2}\d{5,8}\b", - [PiiType.Passport] = @"\b[A-Z][0-9]{8}\b" - }; - - /// - /// Initializes a new instance of the class. - /// - public AudioPiiDetector( - ILogger logger, - IAudioAuditLogger auditLogger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _auditLogger = auditLogger ?? throw new ArgumentNullException(nameof(auditLogger)); - } - - /// - public async Task DetectPiiAsync( - string text, - CancellationToken cancellationToken = default) - { - var result = new PiiDetectionResult(); - - if (string.IsNullOrWhiteSpace(text)) - { - return result; - } - - var detectedEntities = new List(); - - // Check for each PII type - foreach (var (piiType, pattern) in _piiPatterns) - { - var regex = new Regex(pattern, RegexOptions.IgnoreCase); - var matches = regex.Matches(text); - - foreach (Match match in matches) - { - var entity = new PiiEntity - { - Type = piiType, - Text = match.Value, - StartIndex = match.Index, - EndIndex = match.Index + match.Length, - Confidence = CalculateConfidence(piiType, match.Value) - }; - - detectedEntities.Add(entity); - } - } - - // Also check for names using simple heuristics - await DetectNamesAsync(text, detectedEntities); - - // Check for addresses using pattern matching - DetectAddresses(text, detectedEntities); - - result.Entities = detectedEntities.OrderBy(e => e.StartIndex).ToList(); - result.ContainsPii = detectedEntities.Count() > 0; - result.RiskScore = CalculateRiskScore(detectedEntities); - - if (result.ContainsPii) - { - _logger.LogWarning( - "Detected {Count} PII entities with risk score {Score:F2}", - result.Entities.Count(), - result.RiskScore); - } - - return result; - } - - /// - public Task RedactPiiAsync( - string text, - PiiDetectionResult detectionResult, - PiiRedactionOptions? redactionOptions = null) - { - if (!detectionResult.ContainsPii || string.IsNullOrWhiteSpace(text)) - { - return Task.FromResult(text); - } - - var options = redactionOptions ?? new PiiRedactionOptions(); - var redactedText = text; - - // Process entities in reverse order to maintain indices - foreach (var entity in detectionResult.Entities.OrderByDescending(e => e.StartIndex)) - { - var replacement = GetRedactionReplacement(entity, options); - - redactedText = redactedText.Remove(entity.StartIndex, entity.EndIndex - entity.StartIndex); - redactedText = redactedText.Insert(entity.StartIndex, replacement); - } - - _logger.LogInformation( - "Redacted {Count} PII entities using {Method} method", - detectionResult.Entities.Count(), - options.Method); - - return Task.FromResult(redactedText); - } - - private string GetRedactionReplacement(PiiEntity entity, PiiRedactionOptions options) - { - switch (options.Method) - { - case RedactionMethod.Mask: - return options.PreserveLength - ? new string(options.MaskCharacter, entity.Text.Length) - : "****"; - - case RedactionMethod.Placeholder: - return $"[{entity.Type}]"; - - case RedactionMethod.Remove: - return string.Empty; - - case RedactionMethod.Custom: - if (options.CustomReplacements.TryGetValue(entity.Type, out var custom)) - return custom; - goto case RedactionMethod.Placeholder; - - default: - return "[REDACTED]"; - } - } - - private async Task DetectNamesAsync(string text, List entities) - { - // Simple name detection using capitalization patterns - // In production, use NER (Named Entity Recognition) models - var namePattern = @"\b[A-Z][a-z]+\s+[A-Z][a-z]+(?:\s+[A-Z][a-z]+)?\b"; - var regex = new Regex(namePattern); - var matches = regex.Matches(text); - - foreach (Match match in matches) - { - // Skip if already detected as another PII type - if (entities.Any(e => e.StartIndex <= match.Index && e.EndIndex >= match.Index + match.Length)) - continue; - - // Simple heuristic: check if it looks like a name - var parts = match.Value.Split(' '); - if (parts.Length >= 2 && parts.Length <= 4) - { - entities.Add(new PiiEntity - { - Type = PiiType.Name, - Text = match.Value, - StartIndex = match.Index, - EndIndex = match.Index + match.Length, - Confidence = 0.7 // Lower confidence for name detection - }); - } - } - - await Task.CompletedTask; - } - - private void DetectAddresses(string text, List entities) - { - // Simple address pattern - in production, use more sophisticated methods - var addressPattern = @"\b\d+\s+[A-Za-z\s]+(?:Street|St|Avenue|Ave|Road|Rd|Boulevard|Blvd|Lane|Ln|Drive|Dr|Court|Ct|Plaza|Pl)\b"; - var regex = new Regex(addressPattern, RegexOptions.IgnoreCase); - var matches = regex.Matches(text); - - foreach (Match match in matches) - { - entities.Add(new PiiEntity - { - Type = PiiType.Address, - Text = match.Value, - StartIndex = match.Index, - EndIndex = match.Index + match.Length, - Confidence = 0.8 - }); - } - } - - private double CalculateConfidence(PiiType type, string value) - { - // More structured patterns have higher confidence - return type switch - { - PiiType.SSN => 0.95, - PiiType.CreditCard => ValidateCreditCard(value) ? 0.99 : 0.7, - PiiType.Email => 0.95, - PiiType.Phone => 0.9, - PiiType.DateOfBirth => 0.85, - _ => 0.8 - }; - } - - private bool ValidateCreditCard(string number) - { - // Luhn algorithm validation - var digits = number.Where(char.IsDigit).Select(c => c - '0').ToArray(); - if (digits.Length < 13 || digits.Length > 19) - return false; - - var sum = 0; - var alternate = false; - - for (var i = digits.Length - 1; i >= 0; i--) - { - var digit = digits[i]; - if (alternate) - { - digit *= 2; - if (digit > 9) - digit -= 9; - } - sum += digit; - alternate = !alternate; - } - - return sum % 10 == 0; - } - - private double CalculateRiskScore(List entities) - { - if (entities.Count() == 0) - return 0; - - var highRiskTypes = new[] { PiiType.SSN, PiiType.CreditCard, PiiType.BankAccount, PiiType.MedicalRecord }; - var mediumRiskTypes = new[] { PiiType.DateOfBirth, PiiType.DriversLicense, PiiType.Passport }; - - var highRiskCount = entities.Count(e => highRiskTypes.Contains(e.Type)); - var mediumRiskCount = entities.Count(e => mediumRiskTypes.Contains(e.Type)); - var lowRiskCount = entities.Count() - highRiskCount - mediumRiskCount; - - var score = (highRiskCount * 0.4) + (mediumRiskCount * 0.3) + (lowRiskCount * 0.1); - return Math.Min(1.0, score / entities.Count()); - } - } -} diff --git a/ConduitLLM.Core/Services/AudioProcessingService.Caching.cs b/ConduitLLM.Core/Services/AudioProcessingService.Caching.cs deleted file mode 100644 index 53987242f..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.Caching.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Security.Cryptography; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Caching functionality for the audio processing service. - /// - public partial class AudioProcessingService - { - /// - public Task CacheAudioAsync( - string key, - byte[] audioData, - Dictionary? metadata = null, - int expiration = 3600, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Cache key cannot be null or empty", nameof(key)); - - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - try - { - var cacheData = new CachedAudio - { - Data = audioData, - Format = metadata?.GetValueOrDefault("format", "unknown") ?? "unknown", - Metadata = metadata ?? new Dictionary(), - CachedAt = DateTime.UtcNow, - ExpiresAt = DateTime.UtcNow.AddSeconds(expiration) - }; - - var serialized = System.Text.Json.JsonSerializer.Serialize(cacheData); - _cacheService.Set($"audio:{key}", serialized, TimeSpan.FromSeconds(expiration)); - - _logger.LogDebug("Cached audio with key {Key} for {Expiration} seconds", key.Replace(Environment.NewLine, ""), expiration); - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to cache audio, continuing without cache"); - } - - return Task.CompletedTask; - } - - /// - public Task GetCachedAudioAsync(string key, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentException("Cache key cannot be null or empty", nameof(key)); - - try - { - var cached = _cacheService.Get($"audio:{key}"); - if (!string.IsNullOrEmpty(cached)) - { - var cacheData = System.Text.Json.JsonSerializer.Deserialize(cached); - if (cacheData != null && cacheData.ExpiresAt > DateTime.UtcNow) - { - _logger.LogDebug("Retrieved cached audio with key {Key}", key.Replace(Environment.NewLine, "")); - return Task.FromResult(cacheData); - } - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to retrieve cached audio"); - } - - return Task.FromResult(null); - } - - private string GenerateCacheKey(byte[] audioData, string operation) - { - using var sha256 = SHA256.Create(); - var hash = sha256.ComputeHash(audioData); - var hashString = Convert.ToBase64String(hash); - return $"{operation}:{hashString}"; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioProcessingService.Core.cs b/ConduitLLM.Core/Services/AudioProcessingService.Core.cs deleted file mode 100644 index dc139e35f..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.Core.cs +++ /dev/null @@ -1,81 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Core functionality for audio processing service. - /// - public partial class AudioProcessingService : IAudioProcessingService - { - private readonly ILogger _logger; - private readonly ICacheService _cacheService; - - // Supported formats matrix - private readonly Dictionary> _conversionMatrix = new() - { - ["mp3"] = new HashSet { "wav", "flac", "ogg", "webm", "m4a" }, - ["wav"] = new HashSet { "mp3", "flac", "ogg", "webm", "m4a" }, - ["flac"] = new HashSet { "mp3", "wav", "ogg", "webm", "m4a" }, - ["ogg"] = new HashSet { "mp3", "wav", "flac", "webm", "m4a" }, - ["webm"] = new HashSet { "mp3", "wav", "flac", "ogg", "m4a" }, - ["m4a"] = new HashSet { "mp3", "wav", "flac", "ogg", "webm" } - }; - - private readonly List _supportedFormats = new() - { - "mp3", "wav", "flac", "ogg", "webm", "m4a", "opus", "aac" - }; - - /// - /// Initializes a new instance of the class. - /// - /// The logger instance. - /// The cache service for audio caching. - public AudioProcessingService( - ILogger logger, - ICacheService cacheService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); - } - - /// - public bool IsConversionSupported(string sourceFormat, string targetFormat) - { - sourceFormat = sourceFormat?.ToLowerInvariant() ?? string.Empty; - targetFormat = targetFormat?.ToLowerInvariant() ?? string.Empty; - - return sourceFormat == targetFormat || - (_conversionMatrix.ContainsKey(sourceFormat) && - _conversionMatrix[sourceFormat].Contains(targetFormat)); - } - - /// - public List GetSupportedFormats() - { - return new List(_supportedFormats); - } - - /// - public double EstimateProcessingTime(long audioSizeBytes, string operation) - { - // Simple estimation based on file size and operation type - var baseFactor = audioSizeBytes / 1024.0 / 1024.0; // MB - - return operation?.ToLowerInvariant() switch - { - "convert" => baseFactor * 100, // 100ms per MB - "compress" => baseFactor * 150, // 150ms per MB - "noise-reduce" => baseFactor * 200, // 200ms per MB - "normalize" => baseFactor * 50, // 50ms per MB - "split" => baseFactor * 20, // 20ms per MB - "merge" => baseFactor * 30, // 30ms per MB - _ => baseFactor * 100 // Default - }; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioProcessingService.Helpers.cs b/ConduitLLM.Core/Services/AudioProcessingService.Helpers.cs deleted file mode 100644 index f00dcfd7f..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.Helpers.cs +++ /dev/null @@ -1,108 +0,0 @@ -namespace ConduitLLM.Core.Services -{ - /// - /// Helper methods and utilities for the audio processing service. - /// - public partial class AudioProcessingService - { - private async Task SimulateFormatConversion( - byte[] audioData, - string sourceFormat, - string targetFormat, - CancellationToken cancellationToken) - { - // Simulate processing delay - var processingTime = EstimateProcessingTime(audioData.Length, "convert"); - await Task.Delay(TimeSpan.FromMilliseconds(Math.Min(processingTime, 100)), cancellationToken); - - // In production, use FFmpeg or similar - // For now, return slightly modified data to simulate conversion - var sizeMultiplier = GetFormatSizeMultiplier(sourceFormat, targetFormat); - var newSize = (int)(audioData.Length * sizeMultiplier); - var result = new byte[newSize]; - - if (newSize <= audioData.Length) - { - Array.Copy(audioData, result, newSize); - } - else - { - Array.Copy(audioData, result, audioData.Length); - // Fill remaining with simulated data - } - - return result; - } - - private async Task SimulateCompression( - byte[] audioData, - string format, - double quality, - CancellationToken cancellationToken) - { - await Task.Delay(10, cancellationToken); - - // Simulate compression by reducing size based on quality - var compressionRatio = 0.3 + (0.7 * quality); // 30% to 100% of original - var newSize = (int)(audioData.Length * compressionRatio); - var result = new byte[newSize]; - - // Simple sampling to simulate compression - var step = audioData.Length / (double)newSize; - for (int i = 0; i < newSize; i++) - { - var sourceIndex = (int)(i * step); - result[i] = audioData[Math.Min(sourceIndex, audioData.Length - 1)]; - } - - return result; - } - - private async Task SimulateNoiseReduction( - byte[] audioData, - string format, - double aggressiveness, - CancellationToken cancellationToken) - { - await Task.Delay(10, cancellationToken); - - // In production, apply actual noise reduction algorithms - // For simulation, return the same data - return audioData; - } - - private async Task SimulateNormalization( - byte[] audioData, - string format, - double targetLevel, - CancellationToken cancellationToken) - { - await Task.Delay(10, cancellationToken); - - // In production, apply actual normalization - // For simulation, return the same data - return audioData; - } - - private double GetFormatSizeMultiplier(string sourceFormat, string targetFormat) - { - // Approximate size differences between formats - var formatSizes = new Dictionary - { - ["wav"] = 10.0, - ["flac"] = 5.0, - ["mp3"] = 1.0, - ["ogg"] = 0.9, - ["webm"] = 0.8, - ["m4a"] = 1.1, - ["opus"] = 0.7, - ["aac"] = 1.0 - }; - - var sourceSize = formatSizes.GetValueOrDefault(sourceFormat, 1.0); - var targetSize = formatSizes.GetValueOrDefault(targetFormat, 1.0); - - return targetSize / sourceSize; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioProcessingService.Metadata.cs b/ConduitLLM.Core/Services/AudioProcessingService.Metadata.cs deleted file mode 100644 index 52082645f..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.Metadata.cs +++ /dev/null @@ -1,162 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Metadata and audio manipulation functionality for the audio processing service. - /// - public partial class AudioProcessingService - { - /// - public async Task GetAudioMetadataAsync( - byte[] audioData, - string format, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - - await Task.Delay(10, cancellationToken); // Simulate async processing - - // Simulate metadata extraction - // In production, use audio processing libraries - var metadata = new AudioMetadata - { - FileSizeBytes = audioData.Length, - DurationSeconds = EstimateDuration(audioData.Length, format), - Bitrate = EstimateBitrate(format), - SampleRate = 44100, // Standard CD quality - Channels = 2, // Stereo - AverageVolume = -12.0, // dB - PeakVolume = -3.0, // dB - ContainsSpeech = true, // Assume speech for now - ContainsMusic = false, - NoiseLevel = -40.0, // dB - LanguageHints = new List() // Could be populated by analysis - }; - - _logger.LogDebug("Extracted metadata for {Format} audio: {Duration}s, {Bitrate}bps", - format, metadata.DurationSeconds, metadata.Bitrate); - - return metadata; - } - - /// - public async Task> SplitAudioAsync( - byte[] audioData, - string format, - double segmentDuration = 30.0, - double overlap = 0.5, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - - var metadata = await GetAudioMetadataAsync(audioData, format, cancellationToken); - var totalDuration = metadata.DurationSeconds; - var segments = new List(); - - var bytesPerSecond = audioData.Length / totalDuration; - var segmentBytes = (int)(segmentDuration * bytesPerSecond); - var overlapBytes = (int)(overlap * bytesPerSecond); - - var position = 0; - var index = 0; - - while (position < audioData.Length) - { - var start = Math.Max(0, position - overlapBytes); - var length = Math.Min(segmentBytes + overlapBytes, audioData.Length - start); - - var segmentData = new byte[length]; - Array.Copy(audioData, start, segmentData, 0, length); - - segments.Add(new AudioSegment - { - Index = index++, - Data = segmentData, - StartTime = start / bytesPerSecond, - EndTime = (start + length) / bytesPerSecond, - HasOverlap = position > 0 && overlap > 0 - }); - - position += segmentBytes; - } - - _logger.LogDebug("Split audio into {Count} segments of {Duration}s each", segments.Count(), segmentDuration); - return segments; - } - - /// - public async Task MergeAudioAsync( - List segments, - string format, - CancellationToken cancellationToken = default) - { - if (segments == null || segments.Count() == 0) - throw new ArgumentException("Segments cannot be null or empty", nameof(segments)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - - await Task.Delay(10, cancellationToken); // Simulate async processing - - // Sort segments by index - segments = segments.OrderBy(s => s.Index).ToList(); - - // Simple merge without handling overlaps - // In production, use proper audio mixing for overlapping segments - using var stream = new MemoryStream(); - foreach (var segment in segments) - { - await stream.WriteAsync(segment.Data, 0, segment.Data.Length, cancellationToken); - } - - var mergedData = stream.ToArray(); - _logger.LogDebug("Merged {Count} audio segments into {Size} bytes", segments.Count(), mergedData.Length); - - return mergedData; - } - - private double EstimateDuration(long fileSize, string format) - { - // Rough estimation based on typical bitrates - var bitrates = new Dictionary - { - ["mp3"] = 128000, // 128 kbps - ["wav"] = 1411000, // 1411 kbps (CD quality) - ["flac"] = 700000, // ~700 kbps - ["ogg"] = 96000, // 96 kbps - ["webm"] = 64000, // 64 kbps - ["m4a"] = 128000, // 128 kbps - ["opus"] = 64000, // 64 kbps - ["aac"] = 128000 // 128 kbps - }; - - var bitrate = bitrates.GetValueOrDefault(format, 128000); - var bits = fileSize * 8; - return bits / (double)bitrate; - } - - private int EstimateBitrate(string format) - { - return format switch - { - "mp3" => 128000, - "wav" => 1411000, - "flac" => 700000, - "ogg" => 96000, - "webm" => 64000, - "m4a" => 128000, - "opus" => 64000, - "aac" => 128000, - _ => 128000 - }; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioProcessingService.Processing.cs b/ConduitLLM.Core/Services/AudioProcessingService.Processing.cs deleted file mode 100644 index b6f697976..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.Processing.cs +++ /dev/null @@ -1,204 +0,0 @@ -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Audio processing operations for the audio processing service. - /// - public partial class AudioProcessingService - { - /// - public async Task ConvertFormatAsync( - byte[] audioData, - string sourceFormat, - string targetFormat, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - sourceFormat = sourceFormat?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(sourceFormat)); - targetFormat = targetFormat?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(targetFormat)); - - if (sourceFormat == targetFormat) - return audioData; - - if (!IsConversionSupported(sourceFormat, targetFormat)) - throw new NotSupportedException($"Conversion from {sourceFormat} to {targetFormat} is not supported"); - - _logger.LogDebug("Converting audio from {SourceFormat} to {TargetFormat}", sourceFormat, targetFormat); - - try - { - // Check cache first - var cacheKey = GenerateCacheKey(audioData, $"convert_{sourceFormat}_to_{targetFormat}"); - var cached = await GetCachedAudioAsync(cacheKey, cancellationToken); - if (cached != null) - { - _logger.LogDebug("Retrieved converted audio from cache"); - return cached.Data; - } - - // Simulate format conversion - // In production, use FFmpeg or similar library - var convertedData = await SimulateFormatConversion(audioData, sourceFormat, targetFormat, cancellationToken); - - // Cache the result - await CacheAudioAsync(cacheKey, convertedData, new Dictionary - { - ["sourceFormat"] = sourceFormat, - ["targetFormat"] = targetFormat, - ["originalSize"] = audioData.Length.ToString(), - ["convertedSize"] = convertedData.Length.ToString() - }, cancellationToken: cancellationToken); - - return convertedData; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error converting audio format"); - throw; - } - } - - /// - public async Task CompressAudioAsync( - byte[] audioData, - string format, - double quality = 0.8, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - quality = Math.Clamp(quality, 0.0, 1.0); - - _logger.LogDebug("Compressing {Format} audio with quality {Quality}", format.Replace(Environment.NewLine, ""), quality); - - try - { - var cacheKey = GenerateCacheKey(audioData, $"compress_{format}_{quality}"); - var cached = await GetCachedAudioAsync(cacheKey, cancellationToken); - if (cached != null) - { - _logger.LogDebug("Retrieved compressed audio from cache"); - return cached.Data; - } - - // Simulate compression - var compressedData = await SimulateCompression(audioData, format, quality, cancellationToken); - - var compressionRatio = (double)compressedData.Length / audioData.Length; - _logger.LogInformation("Compressed audio from {Original} to {Compressed} bytes (ratio: {Ratio:P})", - audioData.Length, compressedData.Length, compressionRatio); - - // Cache the result - await CacheAudioAsync(cacheKey, compressedData, new Dictionary - { - ["format"] = format, - ["quality"] = quality.ToString("F2"), - ["originalSize"] = audioData.Length.ToString(), - ["compressedSize"] = compressedData.Length.ToString(), - ["compressionRatio"] = compressionRatio.ToString("F3") - }, cancellationToken: cancellationToken); - - return compressedData; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error compressing audio"); - throw; - } - } - - /// - public async Task ReduceNoiseAsync( - byte[] audioData, - string format, - double aggressiveness = 0.5, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - aggressiveness = Math.Clamp(aggressiveness, 0.0, 1.0); - - _logger.LogDebug("Applying noise reduction to {Format} audio with aggressiveness {Level}", format, aggressiveness); - - try - { - var cacheKey = GenerateCacheKey(audioData, $"denoise_{format}_{aggressiveness}"); - var cached = await GetCachedAudioAsync(cacheKey, cancellationToken); - if (cached != null) - { - _logger.LogDebug("Retrieved denoised audio from cache"); - return cached.Data; - } - - // Simulate noise reduction - var denoisedData = await SimulateNoiseReduction(audioData, format, aggressiveness, cancellationToken); - - // Cache the result - await CacheAudioAsync(cacheKey, denoisedData, new Dictionary - { - ["format"] = format, - ["aggressiveness"] = aggressiveness.ToString("F2"), - ["processing"] = "noise-reduction" - }, cancellationToken: cancellationToken); - - return denoisedData; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error reducing noise in audio"); - throw; - } - } - - /// - public async Task NormalizeAudioAsync( - byte[] audioData, - string format, - double targetLevel = -3.0, - CancellationToken cancellationToken = default) - { - if (audioData == null || audioData.Length == 0) - throw new ArgumentException("Audio data cannot be null or empty", nameof(audioData)); - - format = format?.ToLowerInvariant() ?? throw new ArgumentNullException(nameof(format)); - - _logger.LogDebug("Normalizing {Format} audio to {Target}dB", format, targetLevel); - - try - { - var cacheKey = GenerateCacheKey(audioData, $"normalize_{format}_{targetLevel}"); - var cached = await GetCachedAudioAsync(cacheKey, cancellationToken); - if (cached != null) - { - _logger.LogDebug("Retrieved normalized audio from cache"); - return cached.Data; - } - - // Simulate normalization - var normalizedData = await SimulateNormalization(audioData, format, targetLevel, cancellationToken); - - // Cache the result - await CacheAudioAsync(cacheKey, normalizedData, new Dictionary - { - ["format"] = format, - ["targetLevel"] = targetLevel.ToString("F1"), - ["processing"] = "normalization" - }, cancellationToken: cancellationToken); - - return normalizedData; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error normalizing audio"); - throw; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioProcessingService.cs b/ConduitLLM.Core/Services/AudioProcessingService.cs deleted file mode 100644 index a40706253..000000000 --- a/ConduitLLM.Core/Services/AudioProcessingService.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ConduitLLM.Core.Services -{ - /// - /// Implements audio processing capabilities including format conversion, compression, and enhancement. - /// - /// - /// This implementation provides basic audio processing functionality. For production use, - /// consider integrating with specialized audio processing libraries like FFmpeg or NAudio. - /// - /// This class is split into multiple partial files: - /// - AudioProcessingService.Core.cs: Core functionality, dependencies, and configuration - /// - AudioProcessingService.Processing.cs: Main processing operations - /// - AudioProcessingService.Caching.cs: Audio caching functionality - /// - AudioProcessingService.Metadata.cs: Metadata extraction and audio manipulation - /// - AudioProcessingService.Helpers.cs: Private helper methods and utilities - /// - /// - public partial class AudioProcessingService - { - // All implementation is in partial class files - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioQualityTracker.cs b/ConduitLLM.Core/Services/AudioQualityTracker.cs deleted file mode 100644 index 379f04944..000000000 --- a/ConduitLLM.Core/Services/AudioQualityTracker.cs +++ /dev/null @@ -1,509 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Tracks and analyzes audio quality metrics including confidence scores and accuracy. - /// - public class AudioQualityTracker : IAudioQualityTracker - { - private readonly ILogger _logger; - private readonly IAudioMetricsCollector _metricsCollector; - private readonly ConcurrentDictionary _providerQualityMetrics = new(); - private readonly ConcurrentDictionary _modelQualityMetrics = new(); - private readonly ConcurrentDictionary _languageQualityMetrics = new(); - private readonly Timer _analysisTimer; - - /// - /// Initializes a new instance of the class. - /// - public AudioQualityTracker( - ILogger logger, - IAudioMetricsCollector metricsCollector) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _metricsCollector = metricsCollector ?? throw new ArgumentNullException(nameof(metricsCollector)); - - // Start periodic analysis - _analysisTimer = new Timer( - AnalyzeQualityTrends, - null, - TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(5)); - } - - /// - public Task TrackTranscriptionQualityAsync(AudioQualityMetric metric) - { - try - { - // Update provider quality metrics - var providerMetrics = _providerQualityMetrics.GetOrAdd( - metric.Provider, - _ => new QualityMetrics()); - providerMetrics.UpdateMetrics(metric.Confidence, metric.AccuracyScore); - - // Update model quality metrics - if (!string.IsNullOrEmpty(metric.Model)) - { - var modelMetrics = _modelQualityMetrics.GetOrAdd( - metric.Model, - _ => new QualityMetrics()); - modelMetrics.UpdateMetrics(metric.Confidence, metric.AccuracyScore); - } - - // Update language quality metrics - if (!string.IsNullOrEmpty(metric.Language)) - { - var languageMetrics = _languageQualityMetrics.GetOrAdd( - metric.Language, - _ => new LanguageQualityMetrics()); - languageMetrics.UpdateMetrics(metric.Confidence, metric.WordErrorRate); - } - - // Check for quality issues - if (metric.Confidence < 0.7) - { - _logger.LogWarning( - "Low confidence transcription: Provider={Provider}, Confidence={Confidence}, Language={Language}", - metric.Provider, metric.Confidence, metric.Language); - } - - if (metric.WordErrorRate > 0.15) // 15% WER threshold - { - _logger.LogWarning( - "High word error rate: Provider={Provider}, WER={WER}%, Language={Language}", - metric.Provider, metric.WordErrorRate * 100, metric.Language); - } - - // Record to main metrics collector as well - var transcriptionMetric = new TranscriptionMetric - { - Provider = metric.Provider, - VirtualKey = metric.VirtualKey, - Success = true, - DurationMs = metric.ProcessingDurationMs, - Confidence = metric.Confidence, - DetectedLanguage = metric.Language, - AudioDurationSeconds = metric.AudioDurationSeconds, - FileSizeBytes = 0, // Not tracked in quality metric - WordCount = 0, // Not tracked in quality metric - Tags = new Dictionary - { - ["quality.tracked"] = "true", - ["quality.wer"] = metric.WordErrorRate?.ToString("F3") ?? "unknown", - ["quality.accuracy"] = metric.AccuracyScore?.ToString("F3") ?? "unknown" - } - }; - - return _metricsCollector.RecordTranscriptionMetricAsync(transcriptionMetric); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error tracking transcription quality"); - return Task.CompletedTask; - } - } - - /// - public Task GetQualityReportAsync( - DateTime startTime, - DateTime endTime, - string? provider = null) - { - var report = new AudioQualityReport - { - Period = new DateTimeRange { Start = startTime, End = endTime }, - ProviderQuality = GetProviderQualityStats(provider), - ModelQuality = GetModelQualityStats(), - LanguageQuality = GetLanguageQualityStats(), - QualityTrends = CalculateQualityTrends(), - Recommendations = GenerateRecommendations() - }; - - return Task.FromResult(report); - } - - /// - public Task GetQualityThresholdsAsync(string provider) - { - // These thresholds could be configured per provider - return Task.FromResult(new QualityThresholds - { - MinimumConfidence = 0.8, - MaximumWordErrorRate = 0.1, // 10% - MinimumAccuracy = 0.9, - OptimalConfidence = 0.95, - OptimalWordErrorRate = 0.05, // 5% - OptimalAccuracy = 0.97 - }); - } - - /// - public Task IsQualityAcceptableAsync( - string provider, - double confidence, - double? wordErrorRate = null) - { - var thresholds = GetQualityThresholdsAsync(provider).Result; - - var confidenceOk = confidence >= thresholds.MinimumConfidence; - var werOk = !wordErrorRate.HasValue || wordErrorRate.Value <= thresholds.MaximumWordErrorRate; - - return Task.FromResult(confidenceOk && werOk); - } - - private Dictionary GetProviderQualityStats(string? provider) - { - var stats = new Dictionary(); - - var providers = provider != null - ? new[] { provider } - : _providerQualityMetrics.Keys.ToArray(); - - foreach (var p in providers) - { - if (_providerQualityMetrics.TryGetValue(p, out var metrics)) - { - stats[p] = new ProviderQualityStats - { - Provider = p, - AverageConfidence = metrics.GetAverageConfidence(), - MinimumConfidence = metrics.MinConfidence, - MaximumConfidence = metrics.MaxConfidence, - ConfidenceStdDev = metrics.GetConfidenceStdDev(), - AverageAccuracy = metrics.GetAverageAccuracy(), - SampleCount = metrics.SampleCount, - LowConfidenceRate = metrics.GetLowConfidenceRate(0.7), - HighConfidenceRate = metrics.GetHighConfidenceRate(0.95) - }; - } - } - - return stats; - } - - private Dictionary GetModelQualityStats() - { - var stats = new Dictionary(); - - foreach (var kvp in _modelQualityMetrics) - { - var metrics = kvp.Value; - stats[kvp.Key] = new ModelQualityStats - { - Model = kvp.Key, - AverageConfidence = metrics.GetAverageConfidence(), - AverageAccuracy = metrics.GetAverageAccuracy(), - SampleCount = metrics.SampleCount, - PerformanceRating = CalculatePerformanceRating(metrics) - }; - } - - return stats; - } - - private Dictionary GetLanguageQualityStats() - { - var stats = new Dictionary(); - - foreach (var kvp in _languageQualityMetrics) - { - var metrics = kvp.Value; - stats[kvp.Key] = new LanguageQualityStats - { - Language = kvp.Key, - AverageConfidence = metrics.GetAverageConfidence(), - AverageWordErrorRate = metrics.GetAverageWordErrorRate(), - SampleCount = metrics.SampleCount, - QualityScore = CalculateLanguageQualityScore(metrics) - }; - } - - return stats; - } - - private List CalculateQualityTrends() - { - var trends = new List(); - - foreach (var kvp in _providerQualityMetrics) - { - var trend = kvp.Value.CalculateTrend(); - if (trend != AudioQualityTrendDirection.Stable) - { - trends.Add(new QualityTrend - { - Provider = kvp.Key, - Metric = "Confidence", - Direction = trend, - ChangePercent = kvp.Value.GetTrendChangePercent() - }); - } - } - - return trends; - } - - private List GenerateRecommendations() - { - var recommendations = new List(); - - // Check for providers with consistently low confidence - foreach (var kvp in _providerQualityMetrics) - { - var avgConfidence = kvp.Value.GetAverageConfidence(); - if (avgConfidence < 0.8) - { - recommendations.Add(new QualityRecommendation - { - Type = RecommendationType.ProviderSwitch, - Severity = RecommendationSeverity.High, - Provider = kvp.Key, - Message = $"Provider {kvp.Key} has low average confidence ({avgConfidence:P1}). Consider switching to a higher quality provider.", - Impact = "Improved transcription accuracy" - }); - } - } - - // Check for languages with high error rates - foreach (var kvp in _languageQualityMetrics) - { - var avgWer = kvp.Value.GetAverageWordErrorRate(); - if (avgWer > 0.15) - { - recommendations.Add(new QualityRecommendation - { - Type = RecommendationType.ModelUpgrade, - Severity = RecommendationSeverity.Medium, - Language = kvp.Key, - Message = $"Language {kvp.Key} has high error rate ({avgWer:P1}). Consider using a specialized model for this language.", - Impact = "Reduced word error rate" - }); - } - } - - return recommendations; - } - - private double CalculatePerformanceRating(QualityMetrics metrics) - { - var confidenceScore = metrics.GetAverageConfidence(); - var accuracyScore = metrics.GetAverageAccuracy(); - var consistencyScore = 1.0 - (metrics.GetConfidenceStdDev() / 0.5); // Normalize std dev - - return (confidenceScore * 0.4 + accuracyScore * 0.4 + consistencyScore * 0.2); - } - - private double CalculateLanguageQualityScore(LanguageQualityMetrics metrics) - { - var confidenceScore = metrics.GetAverageConfidence(); - var werScore = 1.0 - metrics.GetAverageWordErrorRate(); // Invert WER - - return (confidenceScore * 0.6 + werScore * 0.4); - } - - private void AnalyzeQualityTrends(object? state) - { - try - { - // Clean up old metrics - var cutoff = DateTime.UtcNow.AddHours(-24); - foreach (var metrics in _providerQualityMetrics.Values) - { - metrics.CleanupOldSamples(cutoff); - } - - // Log significant quality changes - foreach (var kvp in _providerQualityMetrics) - { - var trend = kvp.Value.CalculateTrend(); - if (trend == AudioQualityTrendDirection.Declining) - { - _logger.LogWarning( - "Quality declining for provider {Provider}: Confidence dropped {Change:P1}", - kvp.Key, Math.Abs(kvp.Value.GetTrendChangePercent())); - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error analyzing quality trends"); - } - } - - /// - /// Disposes the quality tracker. - /// - public void Dispose() - { - _analysisTimer?.Dispose(); - } - } - - /// - /// Internal class for tracking quality metrics. - /// - internal class QualityMetrics - { - private readonly ConcurrentBag _confidenceSamples = new(); - private readonly ConcurrentBag _accuracySamples = new(); - private readonly object _lock = new(); - - public double MinConfidence { get; private set; } = 1.0; - public double MaxConfidence { get; private set; } = 0.0; - public long SampleCount => _confidenceSamples.Count(); - - public void UpdateMetrics(double? confidence, double? accuracy) - { - var timestamp = DateTime.UtcNow; - - if (confidence.HasValue) - { - _confidenceSamples.Add(new TimestampedSample { Value = confidence.Value, Timestamp = timestamp }); - - lock (_lock) - { - MinConfidence = Math.Min(MinConfidence, confidence.Value); - MaxConfidence = Math.Max(MaxConfidence, confidence.Value); - } - } - - if (accuracy.HasValue) - { - _accuracySamples.Add(new TimestampedSample { Value = accuracy.Value, Timestamp = timestamp }); - } - } - - public double GetAverageConfidence() - { - var samples = _confidenceSamples.ToList(); - return samples.Count() > 0 ? samples.Average(s => s.Value) : 0; - } - - public double GetAverageAccuracy() - { - var samples = _accuracySamples.ToList(); - return samples.Count() > 0 ? samples.Average(s => s.Value) : 0; - } - - public double GetConfidenceStdDev() - { - var samples = _confidenceSamples.Select(s => s.Value).ToList(); - if (samples.Count() < 2) return 0; - - var avg = samples.Average(); - var sum = samples.Sum(d => Math.Pow(d - avg, 2)); - return Math.Sqrt(sum / (samples.Count() - 1)); - } - - public double GetLowConfidenceRate(double threshold) - { - var samples = _confidenceSamples.ToList(); - if (samples.Count() == 0) return 0; - - var lowCount = samples.Count(s => s.Value < threshold); - return (double)lowCount / samples.Count(); - } - - public double GetHighConfidenceRate(double threshold) - { - var samples = _confidenceSamples.ToList(); - if (samples.Count() == 0) return 0; - - var highCount = samples.Count(s => s.Value >= threshold); - return (double)highCount / samples.Count(); - } - - public AudioQualityTrendDirection CalculateTrend() - { - var samples = _confidenceSamples - .OrderBy(s => s.Timestamp) - .ToList(); - - if (samples.Count() < 10) return AudioQualityTrendDirection.Stable; - - var recentAvg = samples.TakeLast(5).Average(s => s.Value); - var olderAvg = samples.Take(5).Average(s => s.Value); - - var change = (recentAvg - olderAvg) / olderAvg; - - if (change > 0.05) return AudioQualityTrendDirection.Improving; - if (change < -0.05) return AudioQualityTrendDirection.Declining; - return AudioQualityTrendDirection.Stable; - } - - public double GetTrendChangePercent() - { - var samples = _confidenceSamples - .OrderBy(s => s.Timestamp) - .ToList(); - - if (samples.Count() < 10) return 0; - - var recentAvg = samples.TakeLast(5).Average(s => s.Value); - var olderAvg = samples.Take(5).Average(s => s.Value); - - return (recentAvg - olderAvg) / olderAvg; - } - - public void CleanupOldSamples(DateTime cutoff) - { - var toKeep = _confidenceSamples.Where(s => s.Timestamp >= cutoff).ToList(); - _confidenceSamples.Clear(); - foreach (var sample in toKeep) - { - _confidenceSamples.Add(sample); - } - - var accuracyToKeep = _accuracySamples.Where(s => s.Timestamp >= cutoff).ToList(); - _accuracySamples.Clear(); - foreach (var sample in accuracyToKeep) - { - _accuracySamples.Add(sample); - } - } - } - - /// - /// Language-specific quality metrics. - /// - internal class LanguageQualityMetrics : QualityMetrics - { - private readonly ConcurrentBag _werSamples = new(); - - public new void UpdateMetrics(double? confidence, double? wordErrorRate) - { - base.UpdateMetrics(confidence, null); - - if (wordErrorRate.HasValue) - { - _werSamples.Add(new TimestampedSample - { - Value = wordErrorRate.Value, - Timestamp = DateTime.UtcNow - }); - } - } - - public double GetAverageWordErrorRate() - { - var samples = _werSamples.ToList(); - return samples.Count() > 0 ? samples.Average(s => s.Value) : 0; - } - } - - /// - /// Timestamped sample for trend analysis. - /// - internal class TimestampedSample - { - public double Value { get; set; } - public DateTime Timestamp { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioStreamCache.cs b/ConduitLLM.Core/Services/AudioStreamCache.cs deleted file mode 100644 index 110f0a069..000000000 --- a/ConduitLLM.Core/Services/AudioStreamCache.cs +++ /dev/null @@ -1,380 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Security.Cryptography; -using System.Text; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of audio stream caching. - /// - public class AudioStreamCache : IAudioStreamCache - { - private readonly ILogger _logger; - private readonly IMemoryCache _memoryCache; - private readonly ICacheService _distributedCache; - private readonly AudioCacheOptions _options; - private readonly AudioCacheMetrics _metrics = new(); - - /// - /// Initializes a new instance of the class. - /// - public AudioStreamCache( - ILogger logger, - IMemoryCache memoryCache, - ICacheService distributedCache, - IOptions options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _memoryCache = memoryCache ?? throw new ArgumentNullException(nameof(memoryCache)); - _distributedCache = distributedCache ?? throw new ArgumentNullException(nameof(distributedCache)); - ArgumentNullException.ThrowIfNull(options); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - } - - /// - public Task CacheTranscriptionAsync( - AudioTranscriptionRequest request, - AudioTranscriptionResponse response, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default) - { - var cacheKey = GenerateTranscriptionCacheKey(request); - var effectiveTtl = ttl ?? _options.DefaultTranscriptionTtl; - - // Cache in memory for fast access - _memoryCache.Set(cacheKey, response, effectiveTtl); - - // Cache in distributed cache for sharing across instances - _distributedCache.Set( - cacheKey, - response, - effectiveTtl); - - _metrics.IncrementCachedItems(); - _logger.LogDebug("Cached transcription with key {Key} for {Duration}", cacheKey, effectiveTtl); - - return Task.CompletedTask; - } - - /// - public Task GetCachedTranscriptionAsync( - AudioTranscriptionRequest request, - CancellationToken cancellationToken = default) - { - var cacheKey = GenerateTranscriptionCacheKey(request); - - // Try memory cache first - if (_memoryCache.TryGetValue(cacheKey, out var cached)) - { - _metrics.IncrementTranscriptionHit(); - _logger.LogDebug("Transcription cache hit (memory) for key {Key}", cacheKey); - return Task.FromResult(cached); - } - - // Try distributed cache - var distributedResult = _distributedCache.Get(cacheKey); - - if (distributedResult != null) - { - // Populate memory cache for next time - _memoryCache.Set(cacheKey, distributedResult, _options.MemoryCacheTtl); - _metrics.IncrementTranscriptionHit(); - _logger.LogDebug("Transcription cache hit (distributed) for key {Key}", cacheKey); - return Task.FromResult(distributedResult); - } - - _metrics.IncrementTranscriptionMiss(); - _logger.LogDebug("Transcription cache miss for key {Key}", cacheKey); - return Task.FromResult(null); - } - - /// - public Task CacheTtsAudioAsync( - TextToSpeechRequest request, - TextToSpeechResponse response, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default) - { - var cacheKey = GenerateTtsCacheKey(request); - var effectiveTtl = ttl ?? _options.DefaultTtsTtl; - - // For large audio, only cache metadata in memory - var cacheEntry = new TtsCacheEntry - { - Response = response, - CachedAt = DateTime.UtcNow, - SizeBytes = response.AudioData.Length - }; - - if (response.AudioData.Length <= _options.MaxMemoryCacheSizeBytes) - { - _memoryCache.Set(cacheKey, cacheEntry, effectiveTtl); - } - - // Always cache in distributed cache - _distributedCache.Set( - cacheKey, - cacheEntry, - effectiveTtl); - - _metrics.IncrementCachedItems(); - _metrics.AddCachedBytes(response.AudioData.Length); - _logger.LogDebug("Cached TTS audio with key {Key} ({Size} bytes) for {Duration}", - cacheKey, response.AudioData.Length, effectiveTtl); - - return Task.CompletedTask; - } - - /// - public Task GetCachedTtsAudioAsync( - TextToSpeechRequest request, - CancellationToken cancellationToken = default) - { - var cacheKey = GenerateTtsCacheKey(request); - - // Try memory cache first - if (_memoryCache.TryGetValue(cacheKey, out var cached)) - { - _metrics.IncrementTtsHit(); - _logger.LogDebug("TTS cache hit (memory) for key {Key}", cacheKey); - return Task.FromResult(cached?.Response); - } - - // Try distributed cache - var distributedResult = _distributedCache.Get(cacheKey); - - if (distributedResult != null) - { - // Populate memory cache if small enough - if (distributedResult.SizeBytes <= _options.MaxMemoryCacheSizeBytes) - { - _memoryCache.Set(cacheKey, distributedResult, _options.MemoryCacheTtl); - } - - _metrics.IncrementTtsHit(); - _logger.LogDebug("TTS cache hit (distributed) for key {Key}", cacheKey); - return Task.FromResult(distributedResult.Response); - } - - _metrics.IncrementTtsMiss(); - _logger.LogDebug("TTS cache miss for key {Key}", cacheKey); - return Task.FromResult(null); - } - - /// - public async IAsyncEnumerable StreamCachedAudioAsync( - string cacheKey, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // Get the cached audio - var cached = _distributedCache.Get(cacheKey); - if (cached?.Response.AudioData == null) - { - yield break; - } - - var audioData = cached.Response.AudioData; - var chunkSize = _options.StreamingChunkSizeBytes; - var totalChunks = (int)Math.Ceiling((double)audioData.Length / chunkSize); - - for (int i = 0; i < totalChunks; i++) - { - cancellationToken.ThrowIfCancellationRequested(); - - var offset = i * chunkSize; - var length = Math.Min(chunkSize, audioData.Length - offset); - var chunkData = new byte[length]; - Array.Copy(audioData, offset, chunkData, 0, length); - - yield return new AudioChunk - { - Data = chunkData, - ChunkIndex = i, - IsFinal = i == totalChunks - 1, - Timestamp = new ChunkTimestamp - { - Start = (double)offset / audioData.Length * (cached.Response.Duration ?? 0), - End = (double)(offset + length) / audioData.Length * (cached.Response.Duration ?? 0) - } - }; - - // Small delay to simulate streaming - await Task.Delay(10, cancellationToken); - } - } - - /// - public Task GetStatisticsAsync() - { - var stats = new AudioCacheStatistics - { - TotalEntries = _metrics.TotalEntries, - TotalSizeBytes = _metrics.TotalSizeBytes, - TranscriptionHits = _metrics.TranscriptionHits, - TranscriptionMisses = _metrics.TranscriptionMisses, - TtsHits = _metrics.TtsHits, - TtsMisses = _metrics.TtsMisses, - AverageEntrySizeBytes = _metrics.TotalEntries > 0 - ? _metrics.TotalSizeBytes / _metrics.TotalEntries - : 0, - OldestEntryAge = _metrics.OldestEntryTime.HasValue - ? DateTime.UtcNow - _metrics.OldestEntryTime.Value - : TimeSpan.Zero - }; - - return Task.FromResult(stats); - } - - /// - public async Task ClearExpiredAsync() - { - // Memory cache handles expiration automatically - // For distributed cache, we'd need to track keys separately - - _logger.LogInformation("Clearing expired cache entries"); - - // Reset metrics for expired entries - var cleared = await Task.FromResult(0); - - return cleared; - } - - /// - public async Task PreloadContentAsync( - PreloadContent content, - CancellationToken cancellationToken = default) - { - _logger.LogInformation("Preloading {TtsCount} TTS items and {TranscriptionCount} transcriptions", - content.CommonPhrases.Count, content.CommonAudioFiles.Count); - - // Note: Actual implementation would need access to the audio services - // This is a placeholder showing the caching logic - - foreach (var phrase in content.CommonPhrases) - { - var request = new TextToSpeechRequest - { - Input = phrase.Text, - Voice = phrase.Voice, - Language = phrase.Language - }; - - // Check if already cached - var existing = await GetCachedTtsAudioAsync(request, cancellationToken); - if (existing != null) - { - continue; - } - - _logger.LogDebug("Preloading TTS for: {Text}", phrase.Text); - // In real implementation, would call TTS service and cache result - } - - await Task.CompletedTask; - } - - private string GenerateTranscriptionCacheKey(AudioTranscriptionRequest request) - { - using var sha256 = SHA256.Create(); - var dataHash = Convert.ToBase64String(sha256.ComputeHash(request.AudioData ?? Array.Empty())); - - var keyParts = new[] - { - "transcription", - request.Model ?? "default", - request.Language ?? "auto", - request.ResponseFormat?.ToString() ?? "json", - dataHash - }; - - return string.Join(":", keyParts); - } - - private string GenerateTtsCacheKey(TextToSpeechRequest request) - { - using var sha256 = SHA256.Create(); - var textHash = Convert.ToBase64String( - sha256.ComputeHash(Encoding.UTF8.GetBytes(request.Input))); - - var keyParts = new[] - { - "tts", - request.Model ?? "default", - request.Voice, - request.Language ?? "auto", - request.Speed?.ToString() ?? "1.0", - request.ResponseFormat?.ToString() ?? "mp3", - textHash - }; - - return string.Join(":", keyParts); - } - } - - - /// - /// Internal metrics tracking. - /// - internal class AudioCacheMetrics - { - private long _totalEntries; - private long _totalSizeBytes; - private long _transcriptionHits; - private long _transcriptionMisses; - private long _ttsHits; - private long _ttsMisses; - - public long TotalEntries => _totalEntries; - public long TotalSizeBytes => _totalSizeBytes; - public long TranscriptionHits => _transcriptionHits; - public long TranscriptionMisses => _transcriptionMisses; - public long TtsHits => _ttsHits; - public long TtsMisses => _ttsMisses; - public DateTime? OldestEntryTime { get; set; } - - public void IncrementCachedItems() => Interlocked.Increment(ref _totalEntries); - public void AddCachedBytes(long bytes) => Interlocked.Add(ref _totalSizeBytes, bytes); - public void IncrementTranscriptionHit() => Interlocked.Increment(ref _transcriptionHits); - public void IncrementTranscriptionMiss() => Interlocked.Increment(ref _transcriptionMisses); - public void IncrementTtsHit() => Interlocked.Increment(ref _ttsHits); - public void IncrementTtsMiss() => Interlocked.Increment(ref _ttsMisses); - } - - /// - /// Options for audio caching. - /// - public class AudioCacheOptions - { - /// - /// Gets or sets the default TTL for transcriptions. - /// - public TimeSpan DefaultTranscriptionTtl { get; set; } = TimeSpan.FromHours(24); - - /// - /// Gets or sets the default TTL for TTS audio. - /// - public TimeSpan DefaultTtsTtl { get; set; } = TimeSpan.FromHours(48); - - /// - /// Gets or sets the memory cache TTL. - /// - public TimeSpan MemoryCacheTtl { get; set; } = TimeSpan.FromMinutes(15); - - /// - /// Gets or sets the maximum size for memory caching. - /// - public long MaxMemoryCacheSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB - - /// - /// Gets or sets the streaming chunk size. - /// - public int StreamingChunkSizeBytes { get; set; } = 64 * 1024; // 64KB - } -} diff --git a/ConduitLLM.Core/Services/AudioTracingService.Contexts.cs b/ConduitLLM.Core/Services/AudioTracingService.Contexts.cs deleted file mode 100644 index 34d8f573f..000000000 --- a/ConduitLLM.Core/Services/AudioTracingService.Contexts.cs +++ /dev/null @@ -1,192 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implementation of audio trace context. - /// - internal class AudioTraceContext : IAudioTraceContext - { - private readonly AudioTrace _trace; - private readonly Action _onDispose; - - public Activity Activity { get; } - public string TraceId => _trace.TraceId; - public string SpanId => Activity.SpanId.ToString(); - - public AudioTraceContext(AudioTrace trace, Activity activity, Action onDispose) - { - _trace = trace; - Activity = activity; - _onDispose = onDispose; - } - - public void AddTag(string key, string value) - { - _trace.Tags[key] = value; - Activity.SetTag(key, value); - } - - public void AddEvent(string eventName, Dictionary? attributes = null) - { - var evt = new TraceEvent - { - Name = eventName, - Timestamp = DateTime.UtcNow, - Attributes = attributes ?? new Dictionary() - }; - - _trace.Events.Add(evt); - - var activityEvent = new ActivityEvent( - eventName, - DateTimeOffset.UtcNow, - new ActivityTagsCollection(evt.Attributes.Select(kvp => - new KeyValuePair(kvp.Key, kvp.Value)))); - - Activity.AddEvent(activityEvent); - } - - public void SetStatus(TraceStatus status, string? description = null) - { - _trace.Status = status; - _trace.StatusDescription = description; - - var activityStatus = status switch - { - TraceStatus.Ok => ActivityStatusCode.Ok, - TraceStatus.Error => ActivityStatusCode.Error, - _ => ActivityStatusCode.Unset - }; - - Activity.SetStatus(activityStatus, description); - } - - public void RecordException(Exception exception) - { - _trace.Error = new TraceError - { - Type = exception.GetType().FullName ?? "Unknown", - Message = exception.Message, - StackTrace = exception.StackTrace, - Timestamp = DateTime.UtcNow - }; - - AddEvent("exception", new Dictionary - { - ["exception.type"] = _trace.Error.Type, - ["exception.message"] = _trace.Error.Message, - ["exception.stacktrace"] = _trace.Error.StackTrace ?? string.Empty - }); - - SetStatus(TraceStatus.Error, exception.Message); - } - - public Dictionary GetPropagationHeaders() - { - var headers = new Dictionary(); - - // W3C Trace Context headers - headers["traceparent"] = $"00-{Activity.TraceId}-{Activity.SpanId}-01"; // Always set as sampled - - if (Activity.TraceStateString != null) - { - headers["tracestate"] = Activity.TraceStateString; - } - - // Add correlation ID from activity baggage - var correlationId = Activity.GetBaggageItem("correlation.id"); - if (!string.IsNullOrEmpty(correlationId)) - { - headers["X-Correlation-ID"] = correlationId; - headers["X-Request-ID"] = correlationId; - } - - // Add any other context baggage items - foreach (var baggage in Activity.Baggage) - { - if (baggage.Key.StartsWith("context.") && !string.IsNullOrEmpty(baggage.Value)) - { - headers[$"X-Context-{baggage.Key}"] = baggage.Value; - } - } - - return headers; - } - - public void Dispose() - { - Activity?.Dispose(); - _onDispose(); - } - } - - /// - /// Implementation of audio span context. - /// - internal class AudioSpanContext : AudioTraceContext, IAudioSpanContext - { - public string? ParentSpanId { get; } - - public AudioSpanContext( - AudioSpan span, - Activity activity, - string traceId, - string parentSpanId, - Action onDispose) - : base(new AudioTrace - { - TraceId = traceId, - Tags = span.Tags, - Events = span.Events - }, activity, onDispose) - { - ParentSpanId = parentSpanId; - } - } - - /// - /// No-op trace context for when tracing is disabled. - /// - internal class NoOpTraceContext : IAudioSpanContext - { - public string TraceId => "00000000000000000000000000000000"; - public string SpanId => "0000000000000000"; - public string? ParentSpanId => null; - - public void AddTag(string key, string value) { } - public void AddEvent(string eventName, Dictionary? attributes = null) { } - public void SetStatus(TraceStatus status, string? description = null) { } - public void RecordException(Exception exception) { } - public Dictionary GetPropagationHeaders() => new(); - public void Dispose() { } - } - - /// - /// Options for audio tracing service. - /// - public class AudioTracingOptions - { - /// - /// Gets or sets the trace retention period. - /// - public TimeSpan RetentionPeriod { get; set; } = TimeSpan.FromDays(7); - - /// - /// Gets or sets the cleanup interval. - /// - public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromHours(1); - - /// - /// Gets or sets the sampling rate (0.0 to 1.0). - /// - public double SamplingRate { get; set; } = 1.0; - - /// - /// Gets or sets whether to export traces to external systems. - /// - public bool EnableExport { get; set; } = true; - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioTracingService.Core.cs b/ConduitLLM.Core/Services/AudioTracingService.Core.cs deleted file mode 100644 index 6d61a3cee..000000000 --- a/ConduitLLM.Core/Services/AudioTracingService.Core.cs +++ /dev/null @@ -1,203 +0,0 @@ -using System.Collections.Concurrent; -using System.Diagnostics; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Provides distributed tracing for audio operations. - /// - public partial class AudioTracingService : IAudioTracingService - { - private readonly ILogger _logger; - private readonly AudioTracingOptions _options; - private readonly ConcurrentDictionary _activeTraces = new(); - private readonly ConcurrentDictionary> _completedTraces = new(); - private readonly Timer _cleanupTimer; - private readonly ActivitySource _activitySource; - private readonly ICorrelationContextService? _correlationService; - - /// - /// Initializes a new instance of the class. - /// - public AudioTracingService( - ILogger logger, - IOptions options, - ICorrelationContextService? correlationService = null) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - _correlationService = correlationService; - - // Initialize OpenTelemetry activity source - _activitySource = new ActivitySource("ConduitLLM.Audio", "1.0.0"); - - // Start cleanup timer - _cleanupTimer = new Timer( - CleanupOldTraces, - null, - _options.CleanupInterval, - _options.CleanupInterval); - } - - /// - public IAudioTraceContext StartTrace( - string operationName, - AudioOperation operationType, - Dictionary? tags = null) - { - var activity = _activitySource.StartActivity( - operationName, - ActivityKind.Server); - - if (activity == null) - { - // Tracing is disabled or sampled out - return new NoOpTraceContext(); - } - - var trace = new AudioTrace - { - TraceId = activity.TraceId.ToString(), - OperationName = operationName, - OperationType = operationType, - StartTime = DateTime.UtcNow, - Status = TraceStatus.Unset, - Tags = tags ?? new Dictionary() - }; - - // Add default tags - trace.Tags["operation.type"] = operationType.ToString(); - trace.Tags["service.name"] = "conduit.audio"; - trace.Tags["service.version"] = "1.0.0"; - - // Add correlation ID if available - if (_correlationService != null) - { - var correlationId = _correlationService.CorrelationId; - if (!string.IsNullOrEmpty(correlationId)) - { - trace.Tags["correlation.id"] = correlationId; - activity.SetTag("correlation.id", correlationId); - activity.SetBaggage("correlation.id", correlationId); - } - } - - _activeTraces[trace.TraceId] = trace; - - var context = new AudioTraceContext( - trace, - activity, - () => CompleteTrace(trace)); - - _logger.LogDebug( - "Started trace {TraceId} for operation {OperationName} ({OperationType})", - trace.TraceId, operationName, operationType); - - return context; - } - - /// - public IAudioSpanContext CreateSpan( - IAudioTraceContext parentContext, - string spanName, - Dictionary? tags = null) - { - if (parentContext is NoOpTraceContext) - { - return new NoOpTraceContext(); - } - - var traceContext = (AudioTraceContext)parentContext; - var parentActivity = traceContext.Activity; - - var activity = _activitySource.StartActivity( - spanName, - ActivityKind.Internal, - parentActivity.Context); - - if (activity == null) - { - return new NoOpTraceContext(); - } - - var span = new AudioSpan - { - SpanId = activity.SpanId.ToString(), - ParentSpanId = parentActivity.SpanId.ToString(), - Name = spanName, - StartTime = DateTime.UtcNow, - Status = TraceStatus.Unset, - Tags = tags ?? new Dictionary() - }; - - // Add to parent trace - if (_activeTraces.TryGetValue(traceContext.TraceId, out var trace)) - { - trace.Spans.Add(span); - } - - var spanContext = new AudioSpanContext( - span, - activity, - traceContext.TraceId, - parentActivity.SpanId.ToString(), - () => CompleteSpan(span)); - - _logger.LogDebug( - "Created span {SpanId} under trace {TraceId}", - span.SpanId, traceContext.TraceId); - - return spanContext; - } - - private void CompleteTrace(AudioTrace trace) - { - trace.EndTime = DateTime.UtcNow; - trace.DurationMs = (trace.EndTime.Value - trace.StartTime).TotalMilliseconds; - - if (trace.Status == TraceStatus.Unset) - { - trace.Status = TraceStatus.Ok; - } - - // Move from active to completed - if (_activeTraces.TryRemove(trace.TraceId, out _)) - { - var list = _completedTraces.GetOrAdd(trace.TraceId, _ => new List()); - lock (list) - { - list.Add(trace); - } - - _logger.LogDebug( - "Completed trace {TraceId} with status {Status} in {Duration}ms", - trace.TraceId, trace.Status, trace.DurationMs); - } - } - - private void CompleteSpan(AudioSpan span) - { - span.EndTime = DateTime.UtcNow; - span.DurationMs = (span.EndTime.Value - span.StartTime).TotalMilliseconds; - - if (span.Status == TraceStatus.Unset) - { - span.Status = TraceStatus.Ok; - } - } - - /// - /// Disposes the tracing service. - /// - public void Dispose() - { - _cleanupTimer?.Dispose(); - _activitySource?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioTracingService.Search.cs b/ConduitLLM.Core/Services/AudioTracingService.Search.cs deleted file mode 100644 index 346de3f0c..000000000 --- a/ConduitLLM.Core/Services/AudioTracingService.Search.cs +++ /dev/null @@ -1,133 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Core.Services -{ - public partial class AudioTracingService - { - /// - public Task GetTraceAsync(string traceId) - { - if (_activeTraces.TryGetValue(traceId, out var activeTrace)) - { - return Task.FromResult(CloneTrace(activeTrace)); - } - - if (_completedTraces.TryGetValue(traceId, out var completedList)) - { - var trace = completedList.FirstOrDefault(); - return Task.FromResult(trace != null ? CloneTrace(trace) : null); - } - - return Task.FromResult(null); - } - - /// - public Task> SearchTracesAsync(TraceSearchQuery query) - { - var allTraces = _completedTraces.Values - .SelectMany(list => list) - .Concat(_activeTraces.Values) - .Where(t => MatchesQuery(t, query)) - .OrderByDescending(t => t.StartTime) - .Take(query.MaxResults) - .Select(CloneTrace) - .ToList(); - - return Task.FromResult(allTraces); - } - - /// - public Task GetStatisticsAsync( - DateTime startTime, - DateTime endTime) - { - var relevantTraces = _completedTraces.Values - .SelectMany(list => list) - .Where(t => t.StartTime >= startTime && t.StartTime <= endTime) - .ToList(); - - var statistics = new TraceStatistics - { - TotalTraces = relevantTraces.Count(), - SuccessfulTraces = relevantTraces.Count(t => t.Status == TraceStatus.Ok), - FailedTraces = relevantTraces.Count(t => t.Status == TraceStatus.Error) - }; - - if (relevantTraces.Count() > 0) - { - var durations = relevantTraces - .Where(t => t.DurationMs.HasValue) - .Select(t => t.DurationMs!.Value) - .OrderBy(d => d) - .ToList(); - - if (durations.Count() > 0) - { - statistics.AverageDurationMs = durations.Average(); - statistics.P95DurationMs = GetPercentile(durations, 0.95); - statistics.P99DurationMs = GetPercentile(durations, 0.99); - } - } - - // Operation breakdown - statistics.OperationBreakdown = relevantTraces - .GroupBy(t => t.OperationType) - .ToDictionary(g => g.Key, g => (long)g.Count()); - - // Provider breakdown - statistics.ProviderBreakdown = relevantTraces - .Where(t => !string.IsNullOrEmpty(t.Provider)) - .GroupBy(t => t.Provider!) - .ToDictionary(g => g.Key, g => (long)g.Count()); - - // Error breakdown - statistics.ErrorBreakdown = relevantTraces - .Where(t => t.Error != null) - .GroupBy(t => t.Error!.Type) - .ToDictionary(g => g.Key, g => (long)g.Count()); - - // Timeline - statistics.Timeline = GenerateTimeline(relevantTraces, startTime, endTime); - - return Task.FromResult(statistics); - } - - private bool MatchesQuery(AudioTrace trace, TraceSearchQuery query) - { - if (query.StartTime.HasValue && trace.StartTime < query.StartTime.Value) - return false; - - if (query.EndTime.HasValue && trace.StartTime > query.EndTime.Value) - return false; - - if (query.OperationType.HasValue && trace.OperationType != query.OperationType.Value) - return false; - - if (query.Status.HasValue && trace.Status != query.Status.Value) - return false; - - if (!string.IsNullOrEmpty(query.Provider) && trace.Provider != query.Provider) - return false; - - if (!string.IsNullOrEmpty(query.VirtualKey) && trace.VirtualKey != query.VirtualKey) - return false; - - if (query.MinDurationMs.HasValue && (!trace.DurationMs.HasValue || trace.DurationMs.Value < query.MinDurationMs.Value)) - return false; - - if (query.MaxDurationMs.HasValue && (!trace.DurationMs.HasValue || trace.DurationMs.Value > query.MaxDurationMs.Value)) - return false; - - if (query.TagFilters.Count() > 0) - { - foreach (var filter in query.TagFilters) - { - if (!trace.Tags.TryGetValue(filter.Key, out var value) || value != filter.Value) - return false; - } - } - - return true; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioTracingService.Utilities.cs b/ConduitLLM.Core/Services/AudioTracingService.Utilities.cs deleted file mode 100644 index 227c3da4f..000000000 --- a/ConduitLLM.Core/Services/AudioTracingService.Utilities.cs +++ /dev/null @@ -1,148 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class AudioTracingService - { - private AudioTrace CloneTrace(AudioTrace trace) - { - return new AudioTrace - { - TraceId = trace.TraceId, - OperationName = trace.OperationName, - OperationType = trace.OperationType, - StartTime = trace.StartTime, - EndTime = trace.EndTime, - DurationMs = trace.DurationMs, - Status = trace.Status, - StatusDescription = trace.StatusDescription, - Tags = new Dictionary(trace.Tags), - Spans = trace.Spans.Select(CloneSpan).ToList(), - Events = trace.Events.Select(CloneEvent).ToList(), - VirtualKey = trace.VirtualKey, - Provider = trace.Provider, - Error = trace.Error != null ? CloneError(trace.Error) : null - }; - } - - private AudioSpan CloneSpan(AudioSpan span) - { - return new AudioSpan - { - SpanId = span.SpanId, - ParentSpanId = span.ParentSpanId, - Name = span.Name, - StartTime = span.StartTime, - EndTime = span.EndTime, - DurationMs = span.DurationMs, - Tags = new Dictionary(span.Tags), - Events = span.Events.Select(CloneEvent).ToList(), - Status = span.Status - }; - } - - private TraceEvent CloneEvent(TraceEvent evt) - { - return new TraceEvent - { - Name = evt.Name, - Timestamp = evt.Timestamp, - Attributes = new Dictionary(evt.Attributes) - }; - } - - private TraceError CloneError(TraceError error) - { - return new TraceError - { - Type = error.Type, - Message = error.Message, - StackTrace = error.StackTrace, - Timestamp = error.Timestamp - }; - } - - private double GetPercentile(List sortedValues, double percentile) - { - if (sortedValues.Count() == 0) return 0; - - var index = (int)Math.Ceiling(percentile * sortedValues.Count()) - 1; - return sortedValues[Math.Max(0, Math.Min(index, sortedValues.Count() - 1))]; - } - - private List GenerateTimeline( - List traces, - DateTime startTime, - DateTime endTime) - { - var timeline = new List(); - var interval = TimeSpan.FromMinutes(5); - - for (var timestamp = startTime; timestamp <= endTime; timestamp = timestamp.Add(interval)) - { - var windowEnd = timestamp.Add(interval); - var windowTraces = traces - .Where(t => t.StartTime >= timestamp && t.StartTime < windowEnd) - .ToList(); - - if (windowTraces.Count() > 0) - { - timeline.Add(new TraceTimelinePoint - { - Timestamp = timestamp, - TraceCount = windowTraces.Count(), - ErrorCount = windowTraces.Count(t => t.Status == TraceStatus.Error), - AverageDurationMs = windowTraces - .Where(t => t.DurationMs.HasValue) - .Select(t => t.DurationMs!.Value) - .DefaultIfEmpty(0) - .Average() - }); - } - } - - return timeline; - } - - private void CleanupOldTraces(object? state) - { - try - { - var cutoff = DateTime.UtcNow.Subtract(_options.RetentionPeriod); - - // Clean up completed traces - foreach (var kvp in _completedTraces.ToList()) - { - lock (kvp.Value) - { - kvp.Value.RemoveAll(t => t.StartTime < cutoff); - if (kvp.Value.Count() == 0) - { - _completedTraces.TryRemove(kvp.Key, out _); - } - } - } - - // Clean up stale active traces - var staleTraces = _activeTraces.Values - .Where(t => t.StartTime < cutoff.AddHours(-1)) - .ToList(); - - foreach (var trace in staleTraces) - { - trace.Status = TraceStatus.Error; - trace.StatusDescription = "Trace timed out"; - CompleteTrace(trace); - } - - _logger.LogDebug("Cleaned up traces older than {Cutoff}", cutoff); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during trace cleanup"); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/AudioTracingService.cs b/ConduitLLM.Core/Services/AudioTracingService.cs deleted file mode 100644 index 79d42ca54..000000000 --- a/ConduitLLM.Core/Services/AudioTracingService.cs +++ /dev/null @@ -1,5 +0,0 @@ -// This file has been split into partial classes for better maintainability: -// - AudioTracingService.Core.cs: Core tracing operations and constructor -// - AudioTracingService.Search.cs: Search and statistics functionality -// - AudioTracingService.Utilities.cs: Helper methods and utilities -// - AudioTracingService.Contexts.cs: Internal context classes and options diff --git a/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs b/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs index a3f23cf58..3f0bdda22 100644 --- a/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs +++ b/ConduitLLM.Core/Services/ConfigurationModelCapabilityService.cs @@ -44,23 +44,6 @@ public async Task SupportsVisionAsync(string model) return capability?.SupportsVision ?? false; } - public async Task SupportsAudioTranscriptionAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportsTranscription ?? false; - } - - public async Task SupportsTextToSpeechAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportsTextToSpeech ?? false; - } - - public async Task SupportsRealtimeAudioAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportsRealtimeAudio ?? false; - } public async Task SupportsVideoGenerationAsync(string model) { @@ -74,23 +57,6 @@ public async Task SupportsVideoGenerationAsync(string model) return capability?.TokenizerType; } - public async Task> GetSupportedVoicesAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportedVoices ?? new List(); - } - - public async Task> GetSupportedLanguagesAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportedLanguages ?? new List(); - } - - public async Task> GetSupportedFormatsAsync(string model) - { - var capability = await GetModelCapabilityAsync(model); - return capability?.SupportedFormats ?? new List(); - } public async Task GetDefaultModelAsync(string provider, string capabilityType) { @@ -118,9 +84,6 @@ public async Task> GetSupportedFormatsAsync(string model) { "chat" => models.FirstOrDefault(m => m.Capabilities.SupportsChat)?.ModelId, "vision" => models.FirstOrDefault(m => m.Capabilities.SupportsVision)?.ModelId, - "transcription" => models.FirstOrDefault(m => m.Capabilities.SupportsTranscription)?.ModelId, - "tts" => models.FirstOrDefault(m => m.Capabilities.SupportsTextToSpeech)?.ModelId, - "realtime" => models.FirstOrDefault(m => m.Capabilities.SupportsRealtimeAudio)?.ModelId, "embeddings" => models.FirstOrDefault(m => m.Capabilities.SupportsEmbeddings)?.ModelId, _ => null }; diff --git a/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs b/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs index 0dcc860bf..83bfc98f9 100644 --- a/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs +++ b/ConduitLLM.Core/Services/CostCalculationService.PricingModels.cs @@ -253,17 +253,5 @@ private Task CalculatePerImageCostAsync(string modelId, ModelCost model return Task.FromResult(cost); } - private async Task CalculatePerMinuteAudioCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - // This pricing model is for audio transcription/realtime - // Delegate to standard calculation which already handles audio costs - return await CalculateStandardCostAsync(modelId, modelCost, usage); - } - - private async Task CalculatePerThousandCharactersCostAsync(string modelId, ModelCost modelCost, Usage usage) - { - // This pricing model is for text-to-speech - // The standard calculation already handles AudioCostPerKCharacters - return await CalculateStandardCostAsync(modelId, modelCost, usage); - } + // Audio calculation methods removed - audio functionality has been removed from the system } \ No newline at end of file diff --git a/ConduitLLM.Core/Services/CostCalculationService.cs b/ConduitLLM.Core/Services/CostCalculationService.cs index e8132b2f5..6d208bb5f 100644 --- a/ConduitLLM.Core/Services/CostCalculationService.cs +++ b/ConduitLLM.Core/Services/CostCalculationService.cs @@ -113,11 +113,13 @@ public async Task CalculateCostAsync(string modelId, Usage usage, Cance case PricingModel.PerImage: calculatedCost = await CalculatePerImageCostAsync(modelId, modelCost, usage); break; + // Audio pricing models have been removed - fallback to standard calculation +#pragma warning disable CS0618 // Type or member is obsolete case PricingModel.PerMinuteAudio: - calculatedCost = await CalculatePerMinuteAudioCostAsync(modelId, modelCost, usage); - break; case PricingModel.PerThousandCharacters: - calculatedCost = await CalculatePerThousandCharactersCostAsync(modelId, modelCost, usage); +#pragma warning restore CS0618 // Type or member is obsolete + _logger.LogWarning("Audio pricing model {PricingModel} is obsolete for model {ModelId}. Using standard calculation.", modelCost.PricingModel, modelId); + calculatedCost = await CalculateStandardCostAsync(modelId, modelCost, usage); break; default: _logger.LogWarning("Unknown pricing model {PricingModel} for model {ModelId}. Using standard calculation.", modelCost.PricingModel, modelId); diff --git a/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs b/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs index 33e2445d5..aba845199 100644 --- a/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs +++ b/ConduitLLM.Core/Services/DatabaseModelCapabilityService.cs @@ -54,74 +54,6 @@ public async Task SupportsVisionAsync(string model) } } - /// - public async Task SupportsAudioTranscriptionAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}AudioTranscription:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = mapping?.SupportsAudioTranscription ?? false; - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking audio transcription capability for model {Model}", model); - return false; - } - } - - /// - public async Task SupportsTextToSpeechAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}TTS:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = mapping?.SupportsTextToSpeech ?? false; - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking TTS capability for model {Model}", model); - return false; - } - } - - /// - public async Task SupportsRealtimeAudioAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}RealtimeAudio:{model}"; - if (_cache.TryGetValue(cacheKey, out var cached)) - { - return cached; - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = mapping?.SupportsRealtimeAudio ?? false; - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking realtime audio capability for model {Model}", model); - return false; - } - } /// public async Task SupportsVideoGenerationAsync(string model) @@ -173,113 +105,6 @@ public async Task SupportsVideoGenerationAsync(string model) } } - /// - public async Task> GetSupportedVoicesAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}Voices:{model}"; - if (_cache.TryGetValue>(cacheKey, out var cached)) - { - return cached ?? new List(); - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = new List(); - - if (!string.IsNullOrEmpty(mapping?.SupportedVoices)) - { - try - { - result = JsonSerializer.Deserialize>(mapping.SupportedVoices) ?? new List(); - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "Invalid JSON in SupportedVoices for model {Model}", model); - } - } - - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting supported voices for model {Model}", model); - return new List(); - } - } - - /// - public async Task> GetSupportedLanguagesAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}Languages:{model}"; - if (_cache.TryGetValue>(cacheKey, out var cached)) - { - return cached ?? new List(); - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = new List(); - - if (!string.IsNullOrEmpty(mapping?.SupportedLanguages)) - { - try - { - result = JsonSerializer.Deserialize>(mapping.SupportedLanguages) ?? new List(); - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "Invalid JSON in SupportedLanguages for model {Model}", model); - } - } - - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting supported languages for model {Model}", model); - return new List(); - } - } - - /// - public async Task> GetSupportedFormatsAsync(string model) - { - var cacheKey = $"{CacheKeyPrefix}Formats:{model}"; - if (_cache.TryGetValue>(cacheKey, out var cached)) - { - return cached ?? new List(); - } - - try - { - var mapping = await GetMappingByModelNameAsync(model); - var result = new List(); - - if (!string.IsNullOrEmpty(mapping?.SupportedFormats)) - { - try - { - result = JsonSerializer.Deserialize>(mapping.SupportedFormats) ?? new List(); - } - catch (JsonException ex) - { - _logger.LogWarning(ex, "Invalid JSON in SupportedFormats for model {Model}", model); - } - } - - _cache.Set(cacheKey, result, _cacheExpiration); - return result; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error getting supported formats for model {Model}", model); - return new List(); - } - } /// public async Task GetDefaultModelAsync(string provider, string capabilityType) diff --git a/ConduitLLM.Core/Services/HybridAudioService.Metrics.cs b/ConduitLLM.Core/Services/HybridAudioService.Metrics.cs deleted file mode 100644 index 5efcddf97..000000000 --- a/ConduitLLM.Core/Services/HybridAudioService.Metrics.cs +++ /dev/null @@ -1,93 +0,0 @@ -using System.Text; - -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Services -{ - public partial class HybridAudioService - { - /// - public Task GetLatencyMetricsAsync(CancellationToken cancellationToken = default) - { - lock (_metricsLock) - { - if (_recentMetrics.Count() == 0) - { - return Task.FromResult(new HybridLatencyMetrics - { - SampleCount = 0, - CalculatedAt = DateTime.UtcNow - }); - } - - var metrics = _recentMetrics.ToList(); - var totalLatencies = metrics.Select(m => m.TotalLatencyMs).OrderBy(l => l).ToList(); - - return Task.FromResult(new HybridLatencyMetrics - { - AverageSttLatencyMs = metrics.Average(m => m.SttLatencyMs), - AverageLlmLatencyMs = metrics.Average(m => m.LlmLatencyMs), - AverageTtsLatencyMs = metrics.Average(m => m.TtsLatencyMs), - AverageTotalLatencyMs = metrics.Average(m => m.TotalLatencyMs), - P95LatencyMs = GetPercentile(totalLatencies, 0.95), - P99LatencyMs = GetPercentile(totalLatencies, 0.99), - SampleCount = metrics.Count(), - CalculatedAt = DateTime.UtcNow - }); - } - } - - private List ExtractCompleteSentences(StringBuilder text) - { - var sentences = new List(); - var currentText = text.ToString(); - var lastSentenceEnd = -1; - - for (int i = 0; i < currentText.Length; i++) - { - if (currentText[i] == '.' || currentText[i] == '!' || currentText[i] == '?') - { - // Check if it's really the end of a sentence (not an abbreviation) - if (i + 1 < currentText.Length && char.IsWhiteSpace(currentText[i + 1])) - { - var sentence = currentText.Substring(lastSentenceEnd + 1, i - lastSentenceEnd).Trim(); - if (!string.IsNullOrWhiteSpace(sentence)) - { - sentences.Add(sentence); - } - lastSentenceEnd = i; - } - } - } - - // Remove extracted sentences from the builder - if (lastSentenceEnd >= 0) - { - text.Remove(0, lastSentenceEnd + 1); - } - - return sentences; - } - - private void RecordMetrics(ProcessingMetrics metrics) - { - lock (_metricsLock) - { - _recentMetrics.Enqueue(metrics); - while (_recentMetrics.Count() > MaxMetricsSamples) - { - _recentMetrics.Dequeue(); - } - } - } - - private double GetPercentile(List sortedValues, double percentile) - { - if (sortedValues.Count() == 0) - return 0; - - var index = (int)Math.Ceiling(percentile * sortedValues.Count()) - 1; - return sortedValues[Math.Max(0, Math.Min(index, sortedValues.Count() - 1))]; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/HybridAudioService.Processing.cs b/ConduitLLM.Core/Services/HybridAudioService.Processing.cs deleted file mode 100644 index 6491ed239..000000000 --- a/ConduitLLM.Core/Services/HybridAudioService.Processing.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.Diagnostics; - -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class HybridAudioService - { - /// - public async Task ProcessAudioAsync( - HybridAudioRequest request, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - - var stopwatch = Stopwatch.StartNew(); - var metrics = new ProcessingMetrics(); - - try - { - _logger.LogDebug("Starting hybrid audio processing"); - - // Pre-process audio: noise reduction and normalization - var processedAudioData = request.AudioData; - if (request.EnableStreaming) // Only process if streaming is enabled (as a quality flag) - { - // Apply noise reduction - processedAudioData = await _audioProcessingService.ReduceNoiseAsync( - processedAudioData, - request.AudioFormat, - 0.7, // Moderate aggressiveness - cancellationToken); - - // Normalize audio levels - processedAudioData = await _audioProcessingService.NormalizeAudioAsync( - processedAudioData, - request.AudioFormat, - -3.0, // Standard target level - cancellationToken); - } - - // Step 1: Speech-to-Text - var sttStart = stopwatch.ElapsedMilliseconds; - // Create a minimal transcription request for routing - var routingRequest = new AudioTranscriptionRequest - { - Language = request.Language - }; - var transcriptionClient = await _audioRouter.GetTranscriptionClientAsync( - routingRequest, - request.VirtualKey ?? string.Empty, - cancellationToken); - - if (transcriptionClient == null) - throw new InvalidOperationException("No STT provider available"); - - // Check if format conversion is needed - var supportedFormats = await transcriptionClient.GetSupportedFormatsAsync(cancellationToken); - var audioDataForStt = processedAudioData; - var audioFormatForStt = request.AudioFormat; - - if (!supportedFormats.Contains(request.AudioFormat)) - { - // Convert to a supported format (prefer wav for quality) - var targetFormat = supportedFormats.Contains("wav") ? "wav" : supportedFormats.FirstOrDefault() ?? "mp3"; - if (_audioProcessingService.IsConversionSupported(request.AudioFormat, targetFormat)) - { - audioDataForStt = await _audioProcessingService.ConvertFormatAsync( - processedAudioData, - request.AudioFormat, - targetFormat, - cancellationToken); - audioFormatForStt = targetFormat; - _logger.LogDebug("Converted audio from {Source} to {Target} for STT", request.AudioFormat, targetFormat); - } - } - - var transcriptionRequest = new AudioTranscriptionRequest - { - AudioData = audioDataForStt, - AudioFormat = audioFormatForStt == "mp3" ? AudioFormat.Mp3 : - audioFormatForStt == "wav" ? AudioFormat.Wav : - audioFormatForStt == "flac" ? AudioFormat.Flac : - audioFormatForStt == "webm" ? AudioFormat.Mp3 : // WebM not in enum - audioFormatForStt == "ogg" ? AudioFormat.Ogg : - AudioFormat.Mp3, - Language = request.Language - }; - - var transcription = await transcriptionClient.TranscribeAudioAsync( - transcriptionRequest, - cancellationToken: cancellationToken); - - metrics.SttLatencyMs = stopwatch.ElapsedMilliseconds - sttStart; - metrics.InputDurationSeconds = transcription.Duration ?? 0; - - _logger.LogDebug("Transcription completed: {Text}", transcription.Text); - - // Step 2: LLM Processing - var llmStart = stopwatch.ElapsedMilliseconds; - var messages = await BuildMessagesAsync( - request.SessionId, - transcription.Text, - request.SystemPrompt); - - var llmRequest = new ChatCompletionRequest - { - Model = "gpt-4o-mini", // Default model, will be routed by ILLMRouter - Messages = messages, - Temperature = request.Temperature, - MaxTokens = request.MaxTokens, - Stream = false - }; - - var llmResponse = await _llmRouter.CreateChatCompletionAsync( - llmRequest, - cancellationToken: cancellationToken); - - var responseText = llmResponse.Choices?.FirstOrDefault()?.Message?.Content?.ToString() ?? ""; - metrics.LlmLatencyMs = stopwatch.ElapsedMilliseconds - llmStart; - metrics.TokensUsed = llmResponse.Usage?.TotalTokens ?? 0; - - _logger.LogDebug("LLM response generated: {Text}", responseText); - - // Update session history if applicable - if (!string.IsNullOrEmpty(request.SessionId) && _sessions.TryGetValue(request.SessionId, out var session)) - { - session.AddTurn(transcription.Text ?? string.Empty, responseText ?? string.Empty); - session.LastActivity = DateTime.UtcNow; - } - - // Step 3: Text-to-Speech - var ttsStart = stopwatch.ElapsedMilliseconds; - // Create a minimal TTS request for routing - var ttsRoutingRequest = new TextToSpeechRequest - { - Voice = request.VoiceId ?? "alloy", - Input = responseText ?? string.Empty - }; - var ttsClient = await _audioRouter.GetTextToSpeechClientAsync( - ttsRoutingRequest, - request.VirtualKey ?? string.Empty, - cancellationToken); - - if (ttsClient == null) - throw new InvalidOperationException("No TTS provider available"); - - var ttsRequest = new TextToSpeechRequest - { - Input = responseText ?? string.Empty, - Voice = request.VoiceId ?? "alloy", // Default voice if not specified - ResponseFormat = request.OutputFormat == "mp3" ? AudioFormat.Mp3 : - request.OutputFormat == "wav" ? AudioFormat.Wav : - request.OutputFormat == "flac" ? AudioFormat.Flac : - request.OutputFormat == "ogg" ? AudioFormat.Ogg : - AudioFormat.Mp3 // Default to MP3 - }; - - var ttsResponse = await ttsClient.CreateSpeechAsync( - ttsRequest, - cancellationToken: cancellationToken); - - metrics.TtsLatencyMs = stopwatch.ElapsedMilliseconds - ttsStart; - metrics.OutputDurationSeconds = ttsResponse.Duration ?? 0; - - _logger.LogDebug("TTS completed, audio size: {Size} bytes", ttsResponse.AudioData.Length); - - // Post-process TTS output: compress if needed - var finalAudioData = ttsResponse.AudioData; - var finalAudioFormat = ttsResponse.Format?.ToString().ToLower() ?? request.OutputFormat; - - // Apply compression for smaller file sizes (except for lossless formats) - if (!new[] { "wav", "flac" }.Contains(finalAudioFormat.ToLower())) - { - finalAudioData = await _audioProcessingService.CompressAudioAsync( - finalAudioData, - finalAudioFormat, - 0.85, // High quality compression - cancellationToken); - _logger.LogDebug("Compressed audio from {Original} to {Compressed} bytes", - ttsResponse.AudioData.Length, finalAudioData.Length); - } - - // Complete metrics - metrics.TotalLatencyMs = stopwatch.ElapsedMilliseconds; - RecordMetrics(metrics); - - // Build response - return new HybridAudioResponse - { - AudioData = finalAudioData, - AudioFormat = finalAudioFormat, - TranscribedText = transcription.Text ?? string.Empty, - ResponseText = responseText!, - DetectedLanguage = transcription.Language ?? request.Language, - VoiceUsed = ttsResponse.VoiceUsed, - DurationSeconds = metrics.OutputDurationSeconds, - Metrics = metrics, - SessionId = request.SessionId, - Metadata = request.Metadata - }; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error in hybrid audio processing"); - throw; - } - } - - /// - public async Task IsAvailableAsync(CancellationToken cancellationToken = default) - { - try - { - // For hybrid audio service, we can't check specific provider availability without a virtual key - // The actual availability will be determined when ProcessConversationAsync is called with a valid key - // For now, just check if the LLM router is available - - // Check LLM availability - var testRequest = new ChatCompletionRequest - { - Model = "gpt-4o-mini", - Messages = new List { new() { Role = "user", Content = "test" } } - }; - try - { - // Try to create a completion to check availability - await _llmRouter.CreateChatCompletionAsync(testRequest, cancellationToken: cancellationToken); - } - catch (Exception llmEx) - { - _logger.LogWarning(llmEx, "LLM availability check failed"); - return false; - } - - // TTS availability will be checked when actually processing with a valid virtual key - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "Error checking hybrid audio availability"); - return false; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/HybridAudioService.Sessions.cs b/ConduitLLM.Core/Services/HybridAudioService.Sessions.cs deleted file mode 100644 index 6ebfc85d2..000000000 --- a/ConduitLLM.Core/Services/HybridAudioService.Sessions.cs +++ /dev/null @@ -1,161 +0,0 @@ -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class HybridAudioService - { - /// - public Task CreateSessionAsync( - HybridSessionConfig config, - CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(config); - - var sessionId = Guid.NewGuid().ToString(); - var session = new HybridSession - { - Id = sessionId, - Config = config, - CreatedAt = DateTime.UtcNow, - LastActivity = DateTime.UtcNow - }; - - _sessions[sessionId] = session; - _logger.LogInformation("Created hybrid audio session: {SessionId}", sessionId); - - return Task.FromResult(sessionId); - } - - /// - public Task CloseSessionAsync(string sessionId, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(sessionId)) - throw new ArgumentNullException(nameof(sessionId)); - - if (_sessions.TryRemove(sessionId, out var session)) - { - _logger.LogInformation("Closed hybrid audio session: {SessionId}", sessionId.Replace(Environment.NewLine, "")); - } - - return Task.CompletedTask; - } - - private Task> BuildMessagesAsync( - string? sessionId, - string userInput, - string? systemPrompt) - { - var messages = new List(); - - // Add system prompt - if (!string.IsNullOrEmpty(systemPrompt)) - { - messages.Add(new Message - { - Role = "system", - Content = systemPrompt - }); - } - else if (!string.IsNullOrEmpty(sessionId) && _sessions.TryGetValue(sessionId, out var session)) - { - // Use session's system prompt - if (!string.IsNullOrEmpty(session.Config.SystemPrompt)) - { - messages.Add(new Message - { - Role = "system", - Content = session.Config.SystemPrompt - }); - } - - // Add conversation history - foreach (var turn in session.GetRecentTurns()) - { - messages.Add(new Message { Role = "user", Content = turn.UserInput }); - messages.Add(new Message { Role = "assistant", Content = turn.AssistantResponse }); - } - } - - // Add current user input - messages.Add(new Message - { - Role = "user", - Content = userInput - }); - - return Task.FromResult(messages); - } - - private void CleanupExpiredSessions(object? state) - { - var now = DateTime.UtcNow; - var expiredSessions = _sessions - .Where(kvp => now - kvp.Value.LastActivity > kvp.Value.Config.SessionTimeout) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var sessionId in expiredSessions) - { - if (_sessions.TryRemove(sessionId, out _)) - { - _logger.LogDebug("Cleaned up expired session: {SessionId}", sessionId); - } - } - } - - /// - /// Disposes of the service and cleans up resources. - /// - public void Dispose() - { - _sessionCleanupTimer?.Dispose(); - _sessions.Clear(); - } - - /// - /// Represents a hybrid audio conversation session. - /// - private class HybridSession - { - public string Id { get; set; } = string.Empty; - public HybridSessionConfig Config { get; set; } = new(); - public DateTime CreatedAt { get; set; } - public DateTime LastActivity { get; set; } - private readonly Queue _history = new(); - - public void AddTurn(string userInput, string assistantResponse) - { - _history.Enqueue(new ConversationTurn - { - UserInput = userInput, - AssistantResponse = assistantResponse, - Timestamp = DateTime.UtcNow - }); - - // Maintain history limit - while (_history.Count() > Config.MaxHistoryTurns) - { - _history.Dequeue(); - } - } - - public IEnumerable GetRecentTurns() - { - return _history.ToList(); - } - } - - /// - /// Represents a single turn in a conversation. - /// - private class ConversationTurn - { - public string UserInput { get; set; } = string.Empty; - public string AssistantResponse { get; set; } = string.Empty; - public DateTime Timestamp { get; set; } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/HybridAudioService.cs b/ConduitLLM.Core/Services/HybridAudioService.cs deleted file mode 100644 index 8ced17ef8..000000000 --- a/ConduitLLM.Core/Services/HybridAudioService.cs +++ /dev/null @@ -1,80 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Implements hybrid audio conversation by chaining STT, LLM, and TTS services. - /// - /// - /// This service provides conversational AI capabilities for providers that don't have - /// native real-time audio support, by orchestrating a pipeline of separate services. - /// - /// This class is split into multiple partial files: - /// - HybridAudioService.cs: Core functionality, dependencies, and initialization - /// - HybridAudioService.Processing.cs: Main audio processing operations and availability checks - /// - HybridAudioService.Sessions.cs: Session management and conversation history - /// - HybridAudioService.Metrics.cs: Metrics collection and latency monitoring - /// - HybridAudioServiceStreaming.cs: Streaming audio processing implementation - /// - /// - public partial class HybridAudioService : IHybridAudioService - { - private readonly ILLMRouter _llmRouter; - private readonly IAudioRouter _audioRouter; - private readonly ILogger _logger; - private readonly ICostCalculationService _costService; - private readonly IContextManager _contextManager; - private readonly IAudioProcessingService _audioProcessingService; - - // Session management - private readonly ConcurrentDictionary _sessions = new(); - private readonly Timer _sessionCleanupTimer; - - // Latency tracking - private readonly Queue _recentMetrics = new(); - private readonly object _metricsLock = new(); - private const int MaxMetricsSamples = 100; - - /// - /// Initializes a new instance of the class. - /// - /// The LLM router for text generation. - /// The audio router for STT and TTS. - /// The logger instance. - /// The cost calculation service. - /// The context manager for conversation history. - /// The audio processing service. - public HybridAudioService( - ILLMRouter llmRouter, - IAudioRouter audioRouter, - ILogger logger, - ICostCalculationService costService, - IContextManager contextManager, - IAudioProcessingService audioProcessingService) - { - _llmRouter = llmRouter ?? throw new ArgumentNullException(nameof(llmRouter)); - _audioRouter = audioRouter ?? throw new ArgumentNullException(nameof(audioRouter)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _costService = costService ?? throw new ArgumentNullException(nameof(costService)); - _contextManager = contextManager ?? throw new ArgumentNullException(nameof(contextManager)); - _audioProcessingService = audioProcessingService ?? throw new ArgumentNullException(nameof(audioProcessingService)); - - // Start session cleanup timer - _sessionCleanupTimer = new Timer( - CleanupExpiredSessions, - null, - TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(5)); - } - - // ProcessAudioAsync is implemented in HybridAudioService.Processing.cs - // StreamProcessAudioAsync is implemented in HybridAudioServiceStreaming.cs - // Session management methods are implemented in HybridAudioService.Sessions.cs - // Metrics and monitoring methods are implemented in HybridAudioService.Metrics.cs - } -} diff --git a/ConduitLLM.Core/Services/HybridAudioServiceStreaming.cs b/ConduitLLM.Core/Services/HybridAudioServiceStreaming.cs deleted file mode 100644 index 5883ec279..000000000 --- a/ConduitLLM.Core/Services/HybridAudioServiceStreaming.cs +++ /dev/null @@ -1,337 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text; - -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - public partial class HybridAudioService - { - /// - public async IAsyncEnumerable StreamProcessAudioAsync( - HybridAudioRequest request, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - ArgumentNullException.ThrowIfNull(request); - - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var sequenceNumber = 0; - - _logger.LogDebug("Starting streaming hybrid audio processing"); - - // Step 1: Process transcription - AudioTranscriptionResponse? transcriptionResult = null; - Exception? transcriptionError = null; - - try - { - transcriptionResult = await ProcessTranscriptionAsync(request, cancellationToken); - } - catch (Exception ex) - { - transcriptionError = ex; - _logger.LogError(ex, "Error in transcription phase"); - } - - if (transcriptionError != null) - throw transcriptionError; - - if (transcriptionResult == null) - throw new InvalidOperationException("Transcription failed"); - - // Yield transcription chunk - yield return new HybridAudioChunk - { - ChunkType = "transcription", - TextContent = transcriptionResult.Text, - SequenceNumber = sequenceNumber++, - IsFinal = false - }; - - _logger.LogDebug("Transcription completed: {Text}", transcriptionResult.Text); - - // Step 2: Process LLM with streaming - var responseBuilder = new StringBuilder(); - var ttsQueue = new Queue(); - var llmError = await ProcessLlmStreamingAsync( - request, - transcriptionResult, - responseBuilder, - ttsQueue, - cancellationToken); - - if (llmError != null) - { - _logger.LogError(llmError, "Error in LLM processing"); - throw llmError; - } - - // Yield LLM text chunks - var textChunks = GetTextChunks(responseBuilder, sequenceNumber); - sequenceNumber += textChunks.Count; - foreach (var chunk in textChunks) - { - yield return chunk; - } - - // Step 3: Process TTS - var ttsChunks = new List(); - var ttsError = await ProcessTtsAsync( - request, - ttsQueue, - ttsChunks, - sequenceNumber, - cancellationToken); - - if (ttsError != null) - { - _logger.LogError(ttsError, "Error in TTS processing"); - throw ttsError; - } - - // Yield TTS chunks - foreach (var chunk in ttsChunks) - { - yield return chunk; - } - - // Final chunk - yield return new HybridAudioChunk - { - ChunkType = "complete", - SequenceNumber = sequenceNumber++, - IsFinal = true - }; - - _logger.LogDebug("Streaming hybrid audio processing completed"); - } - - private async Task ProcessTranscriptionAsync( - HybridAudioRequest request, - CancellationToken cancellationToken) - { - // Create a minimal transcription request for routing - var routingRequest = new AudioTranscriptionRequest - { - Language = request.Language - }; - var transcriptionClient = await _audioRouter.GetTranscriptionClientAsync( - routingRequest, - request.VirtualKey ?? string.Empty, - cancellationToken); - - if (transcriptionClient == null) - throw new InvalidOperationException("No STT provider available"); - - // Check if format conversion is needed - var supportedFormats = await transcriptionClient.GetSupportedFormatsAsync(cancellationToken); - var audioDataForStt = request.AudioData; - var audioFormatForStt = request.AudioFormat; - - if (!supportedFormats.Contains(request.AudioFormat)) - { - // Convert to a supported format (prefer wav for quality) - var targetFormat = supportedFormats.Contains("wav") ? "wav" : supportedFormats.FirstOrDefault() ?? "mp3"; - if (_audioProcessingService.IsConversionSupported(request.AudioFormat, targetFormat)) - { - audioDataForStt = await _audioProcessingService.ConvertFormatAsync( - request.AudioData, - request.AudioFormat, - targetFormat, - cancellationToken); - audioFormatForStt = targetFormat; - _logger.LogDebug("Converted audio from {Source} to {Target} for STT", request.AudioFormat, targetFormat); - } - } - - var transcriptionRequest = new AudioTranscriptionRequest - { - AudioData = audioDataForStt, - AudioFormat = audioFormatForStt == "mp3" ? AudioFormat.Mp3 : - audioFormatForStt == "wav" ? AudioFormat.Wav : - audioFormatForStt == "flac" ? AudioFormat.Flac : - audioFormatForStt == "webm" ? AudioFormat.Mp3 : // WebM not in enum - audioFormatForStt == "ogg" ? AudioFormat.Ogg : - AudioFormat.Mp3, - Language = request.Language - }; - - return await transcriptionClient.TranscribeAudioAsync( - transcriptionRequest, - cancellationToken: cancellationToken); - } - - private async Task ProcessLlmStreamingAsync( - HybridAudioRequest request, - AudioTranscriptionResponse transcription, - StringBuilder responseBuilder, - Queue ttsQueue, - CancellationToken cancellationToken) - { - try - { - var messages = await BuildMessagesAsync( - request.SessionId, - transcription.Text ?? string.Empty, - request.SystemPrompt); - - var llmRequest = new ChatCompletionRequest - { - Model = "gpt-4o-mini", // Default model, will be routed by ILLMRouter - Messages = messages, - Temperature = request.Temperature, - MaxTokens = request.MaxTokens, - Stream = true // Enable streaming - }; - - await foreach (var chunk in _llmRouter.StreamChatCompletionAsync(llmRequest, cancellationToken: cancellationToken)) - { - var content = chunk.Choices?.FirstOrDefault()?.Delta?.Content; - if (!string.IsNullOrEmpty(content)) - { - responseBuilder.Append(content); - - // Queue text for TTS when we have a sentence - if (content.Contains('.') || content.Contains('!') || content.Contains('?')) - { - var sentences = ExtractCompleteSentences(responseBuilder); - foreach (var sentence in sentences) - { - ttsQueue.Enqueue(sentence); - } - } - } - } - - // Process any remaining text - var remainingText = responseBuilder.ToString(); - if (!string.IsNullOrWhiteSpace(remainingText)) - { - ttsQueue.Enqueue(remainingText); - } - - // Update session history - var fullResponse = responseBuilder.ToString(); - if (!string.IsNullOrEmpty(request.SessionId) && _sessions.TryGetValue(request.SessionId, out var session)) - { - session.AddTurn(transcription.Text ?? string.Empty, fullResponse); - session.LastActivity = DateTime.UtcNow; - } - - return null; - } - catch (Exception ex) - { - return ex; - } - } - - private List GetTextChunks(StringBuilder responseBuilder, int startSequenceNumber) - { - var chunks = new List(); - var text = responseBuilder.ToString(); - - if (!string.IsNullOrEmpty(text)) - { - // Split into smaller chunks for progressive display - const int chunkSize = 100; - for (int i = 0; i < text.Length; i += chunkSize) - { - var chunkText = text.Substring(i, Math.Min(chunkSize, text.Length - i)); - chunks.Add(new HybridAudioChunk - { - ChunkType = "text", - TextContent = chunkText, - SequenceNumber = startSequenceNumber + chunks.Count, - IsFinal = false - }); - } - } - - return chunks; - } - - private async Task ProcessTtsAsync( - HybridAudioRequest request, - Queue ttsQueue, - List chunks, - int startSequenceNumber, - CancellationToken cancellationToken) - { - try - { - // Create a minimal TTS request for routing - var ttsRoutingRequest = new TextToSpeechRequest - { - Voice = request.VoiceId ?? "alloy", - Input = "test" // Dummy text for routing only - }; - var ttsClient = await _audioRouter.GetTextToSpeechClientAsync( - ttsRoutingRequest, - request.VirtualKey ?? string.Empty, - cancellationToken); - - if (ttsClient == null) - throw new InvalidOperationException("No TTS provider available"); - - // Process TTS queue - while (ttsQueue.Count() > 0) - { - var textToSpeak = ttsQueue.Dequeue(); - - var ttsRequest = new TextToSpeechRequest - { - Input = textToSpeak, - Voice = request.VoiceId ?? "alloy", - ResponseFormat = request.OutputFormat == "mp3" ? AudioFormat.Mp3 : - request.OutputFormat == "wav" ? AudioFormat.Wav : - request.OutputFormat == "flac" ? AudioFormat.Flac : - request.OutputFormat == "ogg" ? AudioFormat.Ogg : - AudioFormat.Mp3 - }; - - // Check if provider supports streaming TTS - var supportedFormats = await ttsClient.GetSupportedFormatsAsync(cancellationToken); - if (supportedFormats.Contains("stream")) - { - // Stream TTS chunks - await foreach (var audioChunk in ttsClient.StreamSpeechAsync(ttsRequest, cancellationToken: cancellationToken)) - { - chunks.Add(new HybridAudioChunk - { - ChunkType = "audio", - AudioData = audioChunk.Data, - SequenceNumber = startSequenceNumber + chunks.Count, - IsFinal = false - }); - } - } - else - { - // Fall back to non-streaming TTS - var ttsResponse = await ttsClient.CreateSpeechAsync( - ttsRequest, - cancellationToken: cancellationToken); - - chunks.Add(new HybridAudioChunk - { - ChunkType = "audio", - AudioData = ttsResponse.AudioData, - SequenceNumber = startSequenceNumber + chunks.Count, - IsFinal = false - }); - } - } - - return null; - } - catch (Exception ex) - { - return ex; - } - } - } -} diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.Core.cs b/ConduitLLM.Core/Services/MonitoringAudioService.Core.cs deleted file mode 100644 index a4e0ec65a..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.Core.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Core functionality for monitoring audio service wrapper. - /// - public partial class MonitoringAudioService : IAudioTranscriptionClient, ITextToSpeechClient, IRealtimeAudioClient - { - protected readonly IAudioTranscriptionClient _transcriptionClient; - protected readonly ITextToSpeechClient _ttsClient; - protected readonly IRealtimeAudioClient _realtimeClient; - protected readonly IAudioMetricsCollector _metricsCollector; - protected readonly IAudioAlertingService _alertingService; - protected readonly IAudioTracingService _tracingService; - protected readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public MonitoringAudioService( - IAudioTranscriptionClient transcriptionClient, - ITextToSpeechClient ttsClient, - IRealtimeAudioClient realtimeClient, - IAudioMetricsCollector metricsCollector, - IAudioAlertingService alertingService, - IAudioTracingService tracingService, - ILogger logger) - { - _transcriptionClient = transcriptionClient ?? throw new ArgumentNullException(nameof(transcriptionClient)); - _ttsClient = ttsClient ?? throw new ArgumentNullException(nameof(ttsClient)); - _realtimeClient = realtimeClient ?? throw new ArgumentNullException(nameof(realtimeClient)); - _metricsCollector = metricsCollector ?? throw new ArgumentNullException(nameof(metricsCollector)); - _alertingService = alertingService ?? throw new ArgumentNullException(nameof(alertingService)); - _tracingService = tracingService ?? throw new ArgumentNullException(nameof(tracingService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.Realtime.cs b/ConduitLLM.Core/Services/MonitoringAudioService.Realtime.cs deleted file mode 100644 index d0ed511e7..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.Realtime.cs +++ /dev/null @@ -1,129 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Realtime audio monitoring functionality for the monitoring audio service. - /// - public partial class MonitoringAudioService - { - #region IRealtimeAudioClient Implementation - - /// - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - using var trace = _tracingService.StartTrace( - "audio.realtime.create_session", - AudioOperation.Realtime, - new() - { - ["realtime.model"] = config.Model ?? "default", - ["realtime.voice"] = config.Voice ?? "default", - ["api_key"] = apiKey ?? "default" - }); - - try - { - trace.AddEvent("session.create"); - - var session = await _realtimeClient.CreateSessionAsync( - config, apiKey, cancellationToken); - - // Store virtual key in session metadata - if (session.Metadata == null) - { - session.Metadata = new Dictionary(); - } - session.Metadata["VirtualKey"] = apiKey ?? "default"; - - trace.AddTag("session.id", session.Id); - trace.SetStatus(TraceStatus.Ok); - - _logger.LogInformation( - "Realtime session created: {SessionId} for virtual key: {VirtualKey}", - session.Id, apiKey ?? "default"); - - return session; - } - catch (Exception ex) - { - trace.RecordException(ex); - - _logger.LogError(ex, - "Failed to create realtime session"); - - throw; - } - } - - /// - public IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - var stream = _realtimeClient.StreamAudioAsync(session, cancellationToken); - var virtualKey = session.Metadata?.GetValueOrDefault("VirtualKey")?.ToString() ?? "default"; - - return new MonitoredDuplexStream( - stream, - _metricsCollector, - _tracingService, - apiKey: virtualKey, - _realtimeClient.GetType().Name, - session.Id); - } - - /// - public Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - return _realtimeClient.UpdateSessionAsync(session, updates, cancellationToken); - } - - /// - public virtual async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - await _realtimeClient.CloseSessionAsync(session, cancellationToken); - - // Record session completion metrics - var sessionDuration = (DateTime.UtcNow - session.CreatedAt).TotalSeconds; - var virtualKey = session.Metadata?.GetValueOrDefault("VirtualKey")?.ToString() ?? "default"; - - await _metricsCollector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = _realtimeClient.GetType().Name, - VirtualKey = virtualKey, - SessionId = session.Id, - SessionDurationSeconds = sessionDuration, - Success = true, - DurationMs = sessionDuration * 1000 - }); - } - - /// - public Task SupportsRealtimeAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return _realtimeClient.SupportsRealtimeAsync(apiKey, cancellationToken); - } - - /// - public Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) - { - return _realtimeClient.GetCapabilitiesAsync(cancellationToken); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.TextToSpeech.cs b/ConduitLLM.Core/Services/MonitoringAudioService.TextToSpeech.cs deleted file mode 100644 index 3719ccb71..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.TextToSpeech.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Runtime.CompilerServices; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Text-to-speech monitoring functionality for the monitoring audio service. - /// - public partial class MonitoringAudioService - { - #region ITextToSpeechClient Implementation - - /// - public virtual async Task CreateSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - using var trace = _tracingService.StartTrace( - "audio.tts", - AudioOperation.TextToSpeech, - new() - { - ["tts.character_count"] = request.Input.Length.ToString(), - ["tts.voice"] = request.Voice, - ["tts.model"] = request.Model ?? "default", - ["api_key"] = apiKey ?? "default" - }); - - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var metric = new TtsMetric - { - Provider = _ttsClient.GetType().Name, - VirtualKey = apiKey ?? "default", - Voice = request.Voice, - CharacterCount = request.Input.Length, - OutputFormat = request.ResponseFormat?.ToString() ?? "mp3" - }; - - try - { - trace.AddEvent("tts.start"); - - var response = await _ttsClient.CreateSpeechAsync( - request, apiKey, cancellationToken); - - stopwatch.Stop(); - metric.Success = true; - metric.DurationMs = stopwatch.ElapsedMilliseconds; - metric.OutputSizeBytes = response.AudioData?.Length ?? 0; - metric.GeneratedDurationSeconds = response.Duration ?? 0; - - trace.AddTag("tts.output_bytes", metric.OutputSizeBytes.ToString()); - trace.AddTag("tts.duration_ms", metric.DurationMs.ToString()); - trace.SetStatus(TraceStatus.Ok); - - _logger.LogInformation( - "TTS completed: {Characters} chars -> {Bytes} bytes in {Duration}ms", - metric.CharacterCount, metric.OutputSizeBytes, metric.DurationMs); - - return response; - } - catch (Exception ex) - { - stopwatch.Stop(); - metric.Success = false; - metric.DurationMs = stopwatch.ElapsedMilliseconds; - metric.ErrorCode = ex.GetType().Name; - - trace.RecordException(ex); - - _logger.LogError(ex, - "TTS failed after {Duration}ms", - metric.DurationMs); - - throw; - } - finally - { - await _metricsCollector.RecordTtsMetricAsync(metric); - - // Check alerts - var snapshot = await _metricsCollector.GetCurrentSnapshotAsync(); - await _alertingService.EvaluateMetricsAsync(snapshot, CancellationToken.None); - } - } - - /// - public async IAsyncEnumerable StreamSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - using var trace = _tracingService.StartTrace( - "audio.tts.stream", - AudioOperation.TextToSpeech, - new() - { - ["tts.character_count"] = request.Input.Length.ToString(), - ["tts.voice"] = request.Voice, - ["api_key"] = apiKey ?? "default" - }); - - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var totalBytes = 0; - - var chunks = _ttsClient.StreamSpeechAsync(request, apiKey, cancellationToken); - var enumerator = chunks.GetAsyncEnumerator(cancellationToken); - - try - { - while (true) - { - AudioChunk? chunk = null; - try - { - if (!await enumerator.MoveNextAsync()) - break; - chunk = enumerator.Current; - } - catch (Exception ex) - { - trace.RecordException(ex); - _logger.LogError(ex, "TTS streaming failed after {Duration}ms", stopwatch.ElapsedMilliseconds); - throw; - } - - if (chunk != null) - { - totalBytes += chunk.Data?.Length ?? 0; - trace.AddEvent("tts.chunk", new Dictionary - { - ["chunk_size"] = chunk.Data?.Length ?? 0 - }); - - yield return chunk; - } - } - - trace.SetStatus(TraceStatus.Ok); - _logger.LogInformation( - "TTS streaming completed: {TotalBytes} bytes in {Duration}ms", - totalBytes, stopwatch.ElapsedMilliseconds); - } - finally - { - await enumerator.DisposeAsync(); - } - } - - /// - public async Task> ListVoicesAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - using var trace = _tracingService.StartTrace( - "audio.list_voices", - AudioOperation.TextToSpeech, - new() - { - ["api_key"] = apiKey ?? "default" - }); - - try - { - var voices = await _ttsClient.ListVoicesAsync(apiKey, cancellationToken); - - trace.AddTag("voice.count", voices.Count.ToString()); - trace.SetStatus(TraceStatus.Ok); - - return voices; - } - catch (Exception ex) - { - trace.RecordException(ex); - throw; - } - } - - /// - Task> ITextToSpeechClient.GetSupportedFormatsAsync(CancellationToken cancellationToken) - { - return _ttsClient.GetSupportedFormatsAsync(cancellationToken); - } - - /// - public Task SupportsTextToSpeechAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return _ttsClient.SupportsTextToSpeechAsync(apiKey, cancellationToken); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.Transcription.cs b/ConduitLLM.Core/Services/MonitoringAudioService.Transcription.cs deleted file mode 100644 index 876ba5d8e..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.Transcription.cs +++ /dev/null @@ -1,112 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Audio transcription monitoring functionality for the monitoring audio service. - /// - public partial class MonitoringAudioService - { - #region IAudioTranscriptionClient Implementation - - /// - public virtual async Task TranscribeAudioAsync( - AudioTranscriptionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - using var trace = _tracingService.StartTrace( - "audio.transcribe", - AudioOperation.Transcription, - new() - { - ["audio.size_bytes"] = request.AudioData?.Length.ToString() ?? "0", - ["audio.language"] = request.Language ?? "auto", - ["api_key"] = apiKey ?? "default" - }); - - var stopwatch = System.Diagnostics.Stopwatch.StartNew(); - var metric = new TranscriptionMetric - { - Provider = _transcriptionClient.GetType().Name, - VirtualKey = apiKey ?? "default", - AudioFormat = request.ResponseFormat?.ToString() ?? "unknown", - FileSizeBytes = request.AudioData?.Length ?? 0, - DetectedLanguage = request.Language - }; - - try - { - trace.AddEvent("transcription.start"); - - var response = await _transcriptionClient.TranscribeAudioAsync( - request, apiKey, cancellationToken); - - stopwatch.Stop(); - metric.Success = true; - metric.DurationMs = stopwatch.ElapsedMilliseconds; - metric.AudioDurationSeconds = response.Duration ?? 0; - metric.DetectedLanguage = response.Language ?? request.Language; - metric.WordCount = response.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries).Length; - - trace.AddTag("transcription.words", metric.WordCount.ToString()); - trace.AddTag("transcription.duration_ms", metric.DurationMs.ToString()); - trace.SetStatus(TraceStatus.Ok); - - _logger.LogInformation( - "Transcription completed: {Words} words in {Duration}ms", - metric.WordCount, metric.DurationMs); - - return response; - } - catch (Exception ex) - { - stopwatch.Stop(); - metric.Success = false; - metric.DurationMs = stopwatch.ElapsedMilliseconds; - metric.ErrorCode = ex.GetType().Name; - - trace.RecordException(ex); - - _logger.LogError(ex, - "Transcription failed after {Duration}ms", - metric.DurationMs); - - throw; - } - finally - { - await _metricsCollector.RecordTranscriptionMetricAsync(metric); - - // Check alerts - var snapshot = await _metricsCollector.GetCurrentSnapshotAsync(); - await _alertingService.EvaluateMetricsAsync(snapshot, CancellationToken.None); - } - } - - /// - public Task SupportsTranscriptionAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return _transcriptionClient.SupportsTranscriptionAsync(apiKey, cancellationToken); - } - - /// - Task> IAudioTranscriptionClient.GetSupportedFormatsAsync(CancellationToken cancellationToken) - { - return _transcriptionClient.GetSupportedFormatsAsync(cancellationToken); - } - - /// - public Task> GetSupportedLanguagesAsync(CancellationToken cancellationToken = default) - { - return _transcriptionClient.GetSupportedLanguagesAsync(cancellationToken); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.Utilities.cs b/ConduitLLM.Core/Services/MonitoringAudioService.Utilities.cs deleted file mode 100644 index 5ce3558f5..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.Utilities.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System.Runtime.CompilerServices; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Core.Services -{ - /// - /// Utility classes for the monitoring audio service. - /// - public partial class MonitoringAudioService - { - } - - /// - /// Monitored duplex stream wrapper. - /// - internal class MonitoredDuplexStream : IAsyncDuplexStream - { - private readonly IAsyncDuplexStream _innerStream; - private readonly IAudioMetricsCollector _metricsCollector; - private readonly IAudioTracingService _tracingService; - private readonly string _virtualKey; - private readonly string _provider; - private readonly string _sessionId; - private readonly IAudioTraceContext _streamTrace; - private int _framesSent; - private int _framesReceived; - - public bool IsConnected => _innerStream.IsConnected; - - public MonitoredDuplexStream( - IAsyncDuplexStream innerStream, - IAudioMetricsCollector metricsCollector, - IAudioTracingService tracingService, - string apiKey, - string provider, - string sessionId) - { - _innerStream = innerStream; - _metricsCollector = metricsCollector; - _tracingService = tracingService; - _virtualKey = apiKey; - _provider = provider; - _sessionId = sessionId; - - _streamTrace = _tracingService.StartTrace( - $"audio.realtime.stream.{sessionId}", - AudioOperation.Realtime, - new() - { - ["session.id"] = sessionId, - ["virtual_key"] = apiKey, - ["provider"] = provider - }); - } - - public async ValueTask SendAsync(RealtimeAudioFrame item, CancellationToken cancellationToken = default) - { - using var span = _tracingService.CreateSpan(_streamTrace, "stream.send"); - - try - { - await _innerStream.SendAsync(item, cancellationToken); - _framesSent++; - - span.AddTag("frame.type", item.Type.ToString()); - span.AddTag("frame.size", item.AudioData?.Length.ToString() ?? "0"); - span.SetStatus(TraceStatus.Ok); - } - catch (System.Exception ex) - { - span.RecordException(ex); - throw; - } - } - - public async IAsyncEnumerable ReceiveAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var response in _innerStream.ReceiveAsync(cancellationToken)) - { - _framesReceived++; - - using var span = _tracingService.CreateSpan(_streamTrace, "stream.receive"); - span.AddTag("response.type", response.Type.ToString()); - span.SetStatus(TraceStatus.Ok); - - yield return response; - } - } - - public async ValueTask CompleteAsync() - { - await _innerStream.CompleteAsync(); - - _streamTrace.AddTag("frames.sent", _framesSent.ToString()); - _streamTrace.AddTag("frames.received", _framesReceived.ToString()); - _streamTrace.SetStatus(TraceStatus.Ok); - _streamTrace.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/MonitoringAudioService.cs b/ConduitLLM.Core/Services/MonitoringAudioService.cs deleted file mode 100644 index 9175297c7..000000000 --- a/ConduitLLM.Core/Services/MonitoringAudioService.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace ConduitLLM.Core.Services -{ - /// - /// Audio service wrapper that adds comprehensive monitoring and observability. - /// - /// - /// This class wraps audio service clients with monitoring, metrics collection, and tracing capabilities. - /// - /// This class is split into multiple partial files: - /// - MonitoringAudioService.cs: Main class declaration - /// - MonitoringAudioService.Core.cs: Core functionality, dependencies, and initialization - /// - MonitoringAudioService.Transcription.cs: Audio transcription monitoring (IAudioTranscriptionClient) - /// - MonitoringAudioService.TextToSpeech.cs: Text-to-speech monitoring (ITextToSpeechClient) - /// - MonitoringAudioService.Realtime.cs: Realtime audio monitoring (IRealtimeAudioClient) - /// - MonitoringAudioService.Utilities.cs: Utility classes and monitored stream wrapper - /// - /// - public partial class MonitoringAudioService - { - // All implementation is in partial class files - } -} \ No newline at end of file diff --git a/ConduitLLM.Core/Services/ProviderMetadataRegistry.cs b/ConduitLLM.Core/Services/ProviderMetadataRegistry.cs index 20799d596..9153de825 100644 --- a/ConduitLLM.Core/Services/ProviderMetadataRegistry.cs +++ b/ConduitLLM.Core/Services/ProviderMetadataRegistry.cs @@ -95,10 +95,6 @@ public ProviderRegistryDiagnostics GetDiagnostics() p => p.Capabilities.Features.VisionInput); AddCapabilityGroup(capabilityGroups, "FunctionCalling", p => p.Capabilities.Features.FunctionCalling); - AddCapabilityGroup(capabilityGroups, "AudioTranscription", - p => p.Capabilities.Features.AudioTranscription); - AddCapabilityGroup(capabilityGroups, "TextToSpeech", - p => p.Capabilities.Features.TextToSpeech); // Group by authentication AddCapabilityGroup(capabilityGroups, "RequiresApiKey", diff --git a/ConduitLLM.Core/Services/RealtimeSessionManager.cs b/ConduitLLM.Core/Services/RealtimeSessionManager.cs deleted file mode 100644 index d7606d1dc..000000000 --- a/ConduitLLM.Core/Services/RealtimeSessionManager.cs +++ /dev/null @@ -1,250 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Core.Services -{ - /// - /// Background service that manages real-time session lifecycle. - /// - public class RealtimeSessionManager : BackgroundService - { - private readonly ILogger _logger; - private readonly IServiceScopeFactory _serviceScopeFactory; - private readonly RealtimeSessionOptions _options; - private readonly Timer _cleanupTimer; - private readonly Timer _metricsTimer; - - /// - /// Initializes a new instance of the class. - /// - public RealtimeSessionManager( - ILogger logger, - IServiceScopeFactory serviceScopeFactory, - IOptions options) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _serviceScopeFactory = serviceScopeFactory ?? throw new ArgumentNullException(nameof(serviceScopeFactory)); - _options = options.Value ?? throw new ArgumentNullException(nameof(options)); - - _cleanupTimer = new Timer( - CleanupCallback, - null, - Timeout.Infinite, - Timeout.Infinite); - - _metricsTimer = new Timer( - MetricsCallback, - null, - Timeout.Infinite, - Timeout.Infinite); - } - - /// - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - _logger.LogInformation("Real-time session manager started"); - - // Start timers - _cleanupTimer.Change( - _options.CleanupInterval, - _options.CleanupInterval); - - _metricsTimer.Change( - _options.MetricsInterval, - _options.MetricsInterval); - - // Keep service running - await Task.Delay(Timeout.Infinite, stoppingToken); - } - - /// - public override async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Real-time session manager stopping"); - - _cleanupTimer?.Change(Timeout.Infinite, Timeout.Infinite); - _metricsTimer?.Change(Timeout.Infinite, Timeout.Infinite); - - await base.StopAsync(cancellationToken); - } - - /// - public override void Dispose() - { - _cleanupTimer?.Dispose(); - _metricsTimer?.Dispose(); - base.Dispose(); - } - - private void CleanupCallback(object? state) - { - _ = ExecuteCleanupAsync(); - } - - private async Task ExecuteCleanupAsync() - { - try - { - using var scope = _serviceScopeFactory.CreateScope(); - var sessionStore = scope.ServiceProvider.GetRequiredService(); - - var cleaned = await sessionStore.CleanupExpiredSessionsAsync( - _options.MaxSessionAge, - CancellationToken.None); - - if (cleaned > 0) - { - _logger.LogInformation("Cleaned up {Count} expired sessions", cleaned); - } - - // Also check for zombie sessions (active but not updated recently) - await CleanupZombieSessionsAsync(sessionStore); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during session cleanup"); - } - } - - private void MetricsCallback(object? state) - { - _ = ExecuteMetricsCollectionAsync(); - } - - private async Task ExecuteMetricsCollectionAsync() - { - try - { - using var scope = _serviceScopeFactory.CreateScope(); - var sessionStore = scope.ServiceProvider.GetRequiredService(); - var metricsCollector = scope.ServiceProvider.GetRequiredService(); - - var sessions = await sessionStore.GetActiveSessionsAsync(CancellationToken.None); - - // Collect aggregate metrics - var totalSessions = sessions.Count; - var sessionsByProvider = sessions.GroupBy(s => s.Provider) - .ToDictionary(g => g.Key, g => g.Count()); - - var totalInputDuration = sessions.Sum(s => s.Statistics.InputAudioDuration.TotalSeconds); - var totalOutputDuration = sessions.Sum(s => s.Statistics.OutputAudioDuration.TotalSeconds); - - _logger.LogInformation( - "Real-time sessions: {Total} active, Input: {InputDuration:F1}s, Output: {OutputDuration:F1}s", - totalSessions, totalInputDuration, totalOutputDuration); - - // Report to metrics collector - foreach (var (provider, count) in sessionsByProvider) - { - var providerSessions = sessions.Where(s => s.Provider == provider).ToList(); - if (providerSessions.Count() > 0) - { - // Report each session individually - foreach (var session in providerSessions) - { - await metricsCollector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = provider, - SessionId = session.Id, - SessionDurationSeconds = (DateTime.UtcNow - session.CreatedAt).TotalSeconds, - TotalAudioSentSeconds = session.Statistics.InputAudioDuration.TotalSeconds, - TotalAudioReceivedSeconds = session.Statistics.OutputAudioDuration.TotalSeconds, - TurnCount = session.Statistics.TurnCount - }); - } - } - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during metrics collection"); - } - } - - private async Task CleanupZombieSessionsAsync(IRealtimeSessionStore sessionStore) - { - var sessions = await sessionStore.GetActiveSessionsAsync(CancellationToken.None); - var zombieThreshold = DateTime.UtcNow - _options.ZombieSessionThreshold; - var zombies = new List(); - - foreach (var session in sessions) - { - // Check if session hasn't been updated recently - var lastActivity = session.Statistics.Duration > TimeSpan.Zero - ? session.CreatedAt + session.Statistics.Duration - : session.CreatedAt; - - if (lastActivity < zombieThreshold && session.State != SessionState.Closed) - { - zombies.Add(session); - } - } - - if (zombies.Count() > 0) - { - _logger.LogWarning("Found {Count} zombie sessions", zombies.Count); - - foreach (var zombie in zombies) - { - zombie.State = SessionState.Error; - zombie.Statistics.ErrorCount++; - - await sessionStore.UpdateSessionAsync(zombie, CancellationToken.None); - - // Optionally terminate the zombie session - if (_options.AutoTerminateZombies) - { - await sessionStore.RemoveSessionAsync(zombie.Id, CancellationToken.None); - _logger.LogInformation("Terminated zombie session {SessionId}", zombie.Id); - } - } - } - } - } - - /// - /// Options for real-time session management. - /// - public class RealtimeSessionOptions - { - /// - /// Gets or sets the interval for cleanup operations. - /// - public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromMinutes(5); - - /// - /// Gets or sets the interval for metrics collection. - /// - public TimeSpan MetricsInterval { get; set; } = TimeSpan.FromMinutes(1); - - /// - /// Gets or sets the maximum age for sessions before cleanup. - /// - public TimeSpan MaxSessionAge { get; set; } = TimeSpan.FromHours(2); - - /// - /// Gets or sets the threshold for identifying zombie sessions. - /// - public TimeSpan ZombieSessionThreshold { get; set; } = TimeSpan.FromMinutes(15); - - /// - /// Gets or sets whether to automatically terminate zombie sessions. - /// - public bool AutoTerminateZombies { get; set; } = true; - - /// - /// Gets or sets the maximum number of concurrent sessions per virtual key. - /// - public int MaxSessionsPerVirtualKey { get; set; } = 10; - - /// - /// Gets or sets whether to enable session persistence across restarts. - /// - public bool EnablePersistence { get; set; } = true; - } -} diff --git a/ConduitLLM.Core/Services/RealtimeSessionStore.cs b/ConduitLLM.Core/Services/RealtimeSessionStore.cs deleted file mode 100644 index 866ced272..000000000 --- a/ConduitLLM.Core/Services/RealtimeSessionStore.cs +++ /dev/null @@ -1,289 +0,0 @@ -using System.Collections.Concurrent; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -using ConduitLLM.Configuration.Interfaces; -namespace ConduitLLM.Core.Services -{ - /// - /// Hybrid implementation of real-time session storage using in-memory cache and Redis. - /// - public class RealtimeSessionStore : IRealtimeSessionStore - { - private readonly ILogger _logger; - private readonly ICacheService _cacheService; - private readonly ConcurrentDictionary _localCache = new(); - private readonly TimeSpan _defaultTtl = TimeSpan.FromHours(2); - private readonly string _keyPrefix = "realtime:session:"; - private readonly string _indexPrefix = "realtime:index:"; - - /// - /// Initializes a new instance of the class. - /// - public RealtimeSessionStore( - ILogger logger, - ICacheService cacheService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _cacheService = cacheService ?? throw new ArgumentNullException(nameof(cacheService)); - } - - /// - public async Task StoreSessionAsync( - RealtimeSession session, - TimeSpan? ttl = null, - CancellationToken cancellationToken = default) - { - var effectiveTtl = ttl ?? _defaultTtl; - var key = GetSessionKey(session.Id); - - // Store in local cache - _localCache[session.Id] = session; - - // Store in distributed cache - _cacheService.Set(key, session, effectiveTtl); - - // Update indices - await UpdateIndicesAsync(session, effectiveTtl, cancellationToken); - _logger.LogDebug("Stored session {SessionId} with TTL {TTL}", session.Id, effectiveTtl); - } - - /// - public async Task GetSessionAsync( - string sessionId, - CancellationToken cancellationToken = default) - { - // Check local cache first - if (_localCache.TryGetValue(sessionId, out var session)) - { - return session; - } - - // Check distributed cache - var key = GetSessionKey(sessionId); - session = _cacheService.Get(key); - - if (session != null) - { - // Populate local cache - _localCache[sessionId] = session; - } - - return await Task.FromResult(session); - } - - /// - public async Task UpdateSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - var existing = await GetSessionAsync(session.Id, cancellationToken); - if (existing == null) - { - _logger.LogWarning("Attempted to update non-existent session {SessionId}", session.Id); - return; - } - - // Calculate remaining TTL - var age = DateTime.UtcNow - existing.CreatedAt; - var remainingTtl = _defaultTtl - age; - - if (remainingTtl > TimeSpan.Zero) - { - await StoreSessionAsync(session, remainingTtl, cancellationToken); - } - } - - /// - public async Task RemoveSessionAsync( - string sessionId, - CancellationToken cancellationToken = default) - { - // Remove from local cache - _localCache.TryRemove(sessionId, out _); - - // Get session for cleanup - var key = GetSessionKey(sessionId); - var session = _cacheService.Get(key); - - if (session != null) - { - // Remove from indices - await RemoveFromIndicesAsync(session, cancellationToken); - } - - // Remove from distributed cache - _cacheService.Remove(key); -_logger.LogDebug("Removed session {SessionId}", sessionId.Replace(Environment.NewLine, "")); - - return true; - } - - /// - public async Task> GetActiveSessionsAsync( - CancellationToken cancellationToken = default) - { - var sessions = new List(); - var indexKey = $"{_indexPrefix}active"; - - // Get session IDs from index - var sessionIds = _cacheService.Get>(indexKey) ?? new List(); - - foreach (var sessionId in sessionIds) - { - var session = await GetSessionAsync(sessionId, cancellationToken); - if (session != null && session.State != SessionState.Closed) - { - sessions.Add(session); - } - } - - return sessions.OrderByDescending(s => s.CreatedAt).ToList(); - } - - /// - public async Task> GetSessionsByVirtualKeyAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - var sessions = new List(); - var indexKey = $"{_indexPrefix}vkey:{virtualKey}"; - - // Get session IDs from index - var sessionIds = _cacheService.Get>(indexKey) ?? new List(); - - foreach (var sessionId in sessionIds) - { - var session = await GetSessionAsync(sessionId, cancellationToken); - if (session != null) - { - sessions.Add(session); - } - } - - return sessions.OrderByDescending(s => s.CreatedAt).ToList(); - } - - /// - public async Task UpdateSessionMetricsAsync( - string sessionId, - SessionStatistics metrics, - CancellationToken cancellationToken = default) - { - var session = await GetSessionAsync(sessionId, cancellationToken); - if (session == null) - { - _logger.LogWarning("Cannot update metrics for non-existent session {SessionId}", sessionId); - return; - } - - session.Statistics = metrics; - await UpdateSessionAsync(session, cancellationToken); - } - - /// - public async Task CleanupExpiredSessionsAsync( - TimeSpan maxAge, - CancellationToken cancellationToken = default) - { - var cutoff = DateTime.UtcNow - maxAge; - var cleaned = 0; - - // Get all active sessions - var sessions = await GetActiveSessionsAsync(cancellationToken); - - foreach (var session in sessions) - { - if (session.CreatedAt < cutoff || session.State == SessionState.Closed) - { - if (await RemoveSessionAsync(session.Id, cancellationToken)) - { - cleaned++; - } - } - } - - if (cleaned > 0) - { - _logger.LogInformation("Cleaned up {Count} expired sessions", cleaned); - } - - return cleaned; - } - - private string GetSessionKey(string sessionId) => $"{_keyPrefix}{sessionId}"; - - private async Task UpdateIndicesAsync( - RealtimeSession session, - TimeSpan ttl, - CancellationToken cancellationToken) - { - // Update active sessions index - var activeKey = $"{_indexPrefix}active"; - var activeList = _cacheService.Get>(activeKey) ?? new List(); - - if (!activeList.Contains(session.Id)) - { - activeList.Add(session.Id); - _cacheService.Set(activeKey, activeList, ttl); - } - - // Update virtual key index if available - var virtualKey = session.Metadata?.GetValueOrDefault("VirtualKey")?.ToString(); - if (!string.IsNullOrEmpty(virtualKey)) - { - var vkeyKey = $"{_indexPrefix}vkey:{virtualKey}"; - var vkeyList = _cacheService.Get>(vkeyKey) ?? new List(); - - if (!vkeyList.Contains(session.Id)) - { - vkeyList.Add(session.Id); - _cacheService.Set(vkeyKey, vkeyList, ttl); - } - } - - await Task.CompletedTask; - } - - private async Task RemoveFromIndicesAsync( - RealtimeSession session, - CancellationToken cancellationToken) - { - // Remove from active sessions index - var activeKey = $"{_indexPrefix}active"; - var activeList = _cacheService.Get>(activeKey) ?? new List(); - activeList.Remove(session.Id); - - if (activeList.Count() > 0) - { - _cacheService.Set(activeKey, activeList, _defaultTtl); - } - else - { - _cacheService.Remove(activeKey); - } - - // Remove from virtual key index - var virtualKey = session.Metadata?.GetValueOrDefault("VirtualKey")?.ToString(); - if (!string.IsNullOrEmpty(virtualKey)) - { - var vkeyKey = $"{_indexPrefix}vkey:{virtualKey}"; - var vkeyList = _cacheService.Get>(vkeyKey) ?? new List(); - vkeyList.Remove(session.Id); - - if (vkeyList.Count() > 0) - { - _cacheService.Set(vkeyKey, vkeyList, _defaultTtl); - } - else - { - _cacheService.Remove(vkeyKey); - } - } - - await Task.CompletedTask; - } - } -} diff --git a/ConduitLLM.Core/Services/RouterService.cs b/ConduitLLM.Core/Services/RouterService.cs deleted file mode 100644 index 95750151c..000000000 --- a/ConduitLLM.Core/Services/RouterService.cs +++ /dev/null @@ -1,461 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Routing; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Service for managing the LLM router configuration and model deployments. - /// - /// - /// - /// The RouterService provides a unified interface for managing the routing configuration - /// that determines how requests are directed to different LLM providers and models. It - /// handles operations such as: - /// - /// - /// Initializing the router with the latest configuration - /// Managing model deployments (adding, updating, removing) - /// Configuring fallback models for high availability - /// Updating model health status - /// - /// - /// This service acts as a bridge between the persistence layer (router configuration repository) - /// and the runtime routing logic implemented by . - /// - /// - public class RouterService : ILLMRouterService - { - private readonly ILLMRouter _router; - private readonly IRouterConfigRepository _repository; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The LLM router implementation that handles runtime routing decisions. - /// Repository for persisting and retrieving router configurations. - /// Logger for recording diagnostic information. - /// Thrown when any parameter is null. - /// - /// The service requires three components to function properly: - /// - /// - /// - /// An implementation that performs the actual routing logic - /// - /// - /// - /// - /// An for configuration persistence - /// - /// - /// - /// - /// A logger component for diagnostic information - /// - /// - /// - /// - public RouterService( - ILLMRouter router, - IRouterConfigRepository repository, - ILogger logger) - { - _router = router ?? throw new ArgumentNullException(nameof(router)); - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Initializes the router with the latest configuration from the repository. - /// - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// - /// - /// This method performs the following steps: - /// - /// - /// - /// Retrieves the current router configuration from the repository - /// - /// - /// If no configuration exists, creates a default configuration and saves it - /// - /// - /// Initializes the router with the configuration - /// - /// - /// - /// This method should be called during application startup to ensure the router - /// has the correct configuration loaded. - /// - /// - /// Note: This method requires the router to be of type - /// to initialize it with the configuration. If the router is of a different type, - /// a warning is logged and no initialization is performed. - /// - /// - public async Task InitializeRouterAsync(CancellationToken cancellationToken = default) - { - var config = await _repository.GetRouterConfigAsync(cancellationToken); - if (config == null) - { - _logger.LogInformation("No router configuration found, creating default"); - config = CreateDefaultConfig(); - await _repository.SaveRouterConfigAsync(config, cancellationToken); - } - - if (_router is DefaultLLMRouter defaultRouter) - { - _logger.LogInformation("Initializing router with {ModelCount} model deployments", - config.ModelDeployments?.Count ?? 0); - defaultRouter.Initialize(config); - } - else - { - _logger.LogWarning("Router is not a DefaultLLMRouter, cannot initialize with config"); - } - } - - /// - /// Gets the current router configuration from the repository. - /// - /// A token to cancel the operation if needed. - /// - /// The current router configuration, or a default configuration if none exists. - /// - /// - /// This method never returns null. If no configuration exists in the repository, - /// a default configuration is created and returned, but not saved to the repository. - /// To save the default configuration, use . - /// - public async Task GetRouterConfigAsync(CancellationToken cancellationToken = default) - { - var config = await _repository.GetRouterConfigAsync(cancellationToken); - return config ?? CreateDefaultConfig(); - } - - /// - /// Updates the router configuration in the repository and reinitializes the router. - /// - /// The new router configuration to save and apply. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// Thrown when the config parameter is null. - /// - /// - /// This method performs the following steps: - /// - /// - /// Validates the configuration - /// Saves the configuration to the repository - /// Reinitializes the router with the new configuration if it's a DefaultLLMRouter - /// - /// - /// Note: This method requires the router to be of type - /// to apply the configuration at runtime. If the router is of a different type, - /// the configuration will be saved but not applied until the application restarts. - /// - /// - public async Task UpdateRouterConfigAsync(RouterConfig config, CancellationToken cancellationToken = default) - { - if (config == null) - { - throw new ArgumentNullException(nameof(config)); - } - - await _repository.SaveRouterConfigAsync(config, cancellationToken); - - if (_router is DefaultLLMRouter defaultRouter) - { - defaultRouter.Initialize(config); - } - } - - /// - /// Adds or updates a model deployment in the router configuration. - /// - /// The model deployment to add or update. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// Thrown when the deployment parameter is null. - /// - /// - /// This method performs the following steps: - /// - /// - /// Validates the deployment information - /// Retrieves the current router configuration - /// Removes any existing deployment with the same name - /// Adds the new deployment to the configuration - /// Saves the updated configuration - /// Reinitializes the router with the new configuration - /// - /// - /// If a deployment with the same name already exists, it will be replaced with the new deployment. - /// The deployment name comparison is case-insensitive. - /// - /// - public async Task AddModelDeploymentAsync(ModelDeployment deployment, CancellationToken cancellationToken = default) - { - if (deployment == null) - { - throw new ArgumentNullException(nameof(deployment)); - } - - var config = await _repository.GetRouterConfigAsync(cancellationToken); - if (config == null) - { - config = CreateDefaultConfig(); - } - - // Remove existing deployment with the same name if present - config.ModelDeployments.RemoveAll(m => - m.DeploymentName.Equals(deployment.DeploymentName, StringComparison.OrdinalIgnoreCase)); - - // Add the new deployment - config.ModelDeployments.Add(deployment); - - // Save the updated config - await _repository.SaveRouterConfigAsync(config, cancellationToken); - - // Update the router - if (_router is DefaultLLMRouter defaultRouter) - { - defaultRouter.Initialize(config); - } - } - - /// - /// Updates an existing model deployment in the router configuration. - /// - /// The updated model deployment information. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// Thrown when the deployment parameter is null. - /// - /// This method is a convenience alias for . - /// It calls AddModelDeploymentAsync, which will replace any existing deployment with - /// the same name. See the documentation for AddModelDeploymentAsync for more details. - /// - public async Task UpdateModelDeploymentAsync(ModelDeployment deployment, CancellationToken cancellationToken = default) - { - await AddModelDeploymentAsync(deployment, cancellationToken); - } - - /// - /// Removes a model deployment from the router configuration. - /// - /// The name of the deployment to remove. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// Thrown when the deploymentName parameter is null or empty. - /// - /// - /// This method performs the following steps: - /// - /// - /// Validates the deployment name - /// Retrieves the current router configuration - /// Removes the deployment with the matching name (case-insensitive) - /// If a deployment was removed, saves the updated configuration - /// Reinitializes the router with the new configuration - /// - /// - /// If no deployment with the specified name exists, no changes will be made. - /// - /// - public async Task RemoveModelDeploymentAsync(string deploymentName, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(deploymentName)) - { - throw new ArgumentException("Deployment name cannot be null or empty", nameof(deploymentName)); - } - - var config = await _repository.GetRouterConfigAsync(cancellationToken); - if (config == null) - { - return; - } - - // Remove the deployment - int removed = config.ModelDeployments.RemoveAll(m => - m.DeploymentName.Equals(deploymentName, StringComparison.OrdinalIgnoreCase)); - - if (removed > 0) - { - // Save the updated config - await _repository.SaveRouterConfigAsync(config, cancellationToken); - - // Update the router - if (_router is DefaultLLMRouter defaultRouter) - { - defaultRouter.Initialize(config); - } - } - } - - /// - /// Gets all available model deployments from the router configuration. - /// - /// A token to cancel the operation if needed. - /// - /// A list of all model deployments defined in the configuration. - /// Returns an empty list if no configuration exists or no deployments are defined. - /// - /// - /// This method retrieves the model deployments from the persisted configuration - /// in the repository, not from the runtime router state. This means that if the - /// router has been modified in memory without saving the configuration, this - /// method will not reflect those changes. - /// - public async Task> GetModelDeploymentsAsync(CancellationToken cancellationToken = default) - { - var config = await _repository.GetRouterConfigAsync(cancellationToken); - return config?.ModelDeployments ?? new List(); - } - - /// - /// Sets or removes fallback models for a primary model in the router configuration. - /// - /// The name of the primary model to configure fallbacks for. - /// A list of fallback model names, or null/empty to remove fallbacks. - /// A token to cancel the operation if needed. - /// A task representing the asynchronous operation. - /// Thrown when the primaryModel parameter is null or empty. - /// - /// - /// Fallback models provide high availability by offering alternative models to try - /// if the primary model is unavailable or fails. This method allows configuring which - /// models should be used as fallbacks for a specific primary model. - /// - /// - /// When fallbacks is null or empty, any existing fallback configuration for the - /// primary model will be removed. - /// - /// - /// This method updates both the persisted configuration in the repository and - /// the runtime router state if using a DefaultLLMRouter. - /// - /// - public async Task SetFallbackModelsAsync(string primaryModel, List fallbacks, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(primaryModel)) - { - throw new ArgumentException("Primary model name cannot be null or empty", nameof(primaryModel)); - } - - var config = await _repository.GetRouterConfigAsync(cancellationToken); - if (config == null) - { - config = CreateDefaultConfig(); - } - - // Update fallbacks mapping - if (fallbacks == null || fallbacks.Count() == 0) - { - // Remove the fallback configuration if empty - if (config.Fallbacks.ContainsKey(primaryModel)) - { - config.Fallbacks.Remove(primaryModel); - } - } - else - { - // Set the fallbacks - config.Fallbacks[primaryModel] = fallbacks; - } - - // Save the updated config - await _repository.SaveRouterConfigAsync(config, cancellationToken); - - // Update the router - if (_router is DefaultLLMRouter defaultRouter) - { - if (fallbacks == null || fallbacks.Count() == 0) - { - defaultRouter.RemoveFallbacks(primaryModel); - } - else - { - defaultRouter.AddFallbackModels(primaryModel, fallbacks); - } - } - } - - /// - /// Gets the configured fallback models for a primary model. - /// - /// The name of the primary model to get fallbacks for. - /// A token to cancel the operation if needed. - /// - /// A list of fallback model names for the specified primary model. - /// Returns an empty list if no fallbacks are configured for the model. - /// - /// Thrown when the primaryModel parameter is null or empty. - /// - /// - /// Fallback models provide high availability by offering alternative models to try - /// if the primary model is unavailable or fails. This method retrieves the currently - /// configured fallback models for a specific primary model. - /// - /// - /// This method retrieves the fallback configuration from the persisted configuration - /// in the repository, not from the runtime router state. - /// - /// - public async Task> GetFallbackModelsAsync(string primaryModel, CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(primaryModel)) - { - throw new ArgumentException("Primary model name cannot be null or empty", nameof(primaryModel)); - } - - var config = await _repository.GetRouterConfigAsync(cancellationToken); - if (config == null || !config.Fallbacks.TryGetValue(primaryModel, out var fallbacks)) - { - return new List(); - } - - return fallbacks; - } - - - /// - /// Creates a default router configuration with sensible defaults. - /// - /// A new RouterConfig object with default settings. - /// - /// - /// This method creates a new RouterConfig with the following default settings: - /// - /// - /// DefaultRoutingStrategy: "simple" - /// MaxRetries: 3 - /// RetryBaseDelayMs: 500 - /// RetryMaxDelayMs: 10000 - /// Empty model deployments list - /// Empty fallbacks dictionary - /// - /// - /// This configuration is used when no existing configuration is found in the repository. - /// - /// - private RouterConfig CreateDefaultConfig() - { - return new RouterConfig - { - DefaultRoutingStrategy = "simple", - MaxRetries = 3, - RetryBaseDelayMs = 500, - RetryMaxDelayMs = 10000, - ModelDeployments = new List(), - Fallbacks = new Dictionary>() - }; - } - } -} diff --git a/ConduitLLM.Core/Services/VirtualKeyTrackingAudioRouter.cs b/ConduitLLM.Core/Services/VirtualKeyTrackingAudioRouter.cs deleted file mode 100644 index 2c9b22647..000000000 --- a/ConduitLLM.Core/Services/VirtualKeyTrackingAudioRouter.cs +++ /dev/null @@ -1,218 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Core.Services -{ - /// - /// Audio router wrapper that ensures virtual key tracking throughout the audio pipeline. - /// - public class VirtualKeyTrackingAudioRouter : IAudioRouter - { - private readonly IAudioRouter _innerRouter; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - public VirtualKeyTrackingAudioRouter( - IAudioRouter innerRouter, - IServiceProvider serviceProvider, - ILogger logger) - { - _innerRouter = innerRouter ?? throw new ArgumentNullException(nameof(innerRouter)); - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - public async Task GetTranscriptionClientAsync( - AudioTranscriptionRequest request, - string virtualKey, - CancellationToken cancellationToken = default) - { - var client = await _innerRouter.GetTranscriptionClientAsync(request, virtualKey, cancellationToken); - - // Store virtual key in request metadata if available - if (client != null && request.ProviderOptions != null) - { - request.ProviderOptions["_virtualKey"] = virtualKey; - } - - return client; - } - - /// - public async Task GetTextToSpeechClientAsync( - TextToSpeechRequest request, - string virtualKey, - CancellationToken cancellationToken = default) - { - var client = await _innerRouter.GetTextToSpeechClientAsync(request, virtualKey, cancellationToken); - - // Store virtual key in request metadata if available - if (client != null && request.ProviderOptions != null) - { - request.ProviderOptions["_virtualKey"] = virtualKey; - } - - return client; - } - - /// - public async Task GetRealtimeClientAsync( - RealtimeSessionConfig config, - string virtualKey, - CancellationToken cancellationToken = default) - { - var client = await _innerRouter.GetRealtimeClientAsync(config, virtualKey, cancellationToken); - - if (client != null) - { - // Wrap the client to ensure virtual key tracking - return new VirtualKeyTrackingRealtimeClient(client, virtualKey, _serviceProvider, _logger); - } - - return client; - } - - /// - public Task> GetAvailableTranscriptionProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - return _innerRouter.GetAvailableTranscriptionProvidersAsync(virtualKey, cancellationToken); - } - - /// - public Task> GetAvailableTextToSpeechProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - return _innerRouter.GetAvailableTextToSpeechProvidersAsync(virtualKey, cancellationToken); - } - - /// - public Task> GetAvailableRealtimeProvidersAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - return _innerRouter.GetAvailableRealtimeProvidersAsync(virtualKey, cancellationToken); - } - - /// - public bool ValidateAudioOperation( - AudioOperation operation, - string provider, - AudioRequestBase request, - out string errorMessage) - { - return _innerRouter.ValidateAudioOperation(operation, provider, request, out errorMessage); - } - - /// - public Task GetRoutingStatisticsAsync( - string virtualKey, - CancellationToken cancellationToken = default) - { - return _innerRouter.GetRoutingStatisticsAsync(virtualKey, cancellationToken); - } - } - - /// - /// Wrapper for real-time audio client that ensures virtual key is tracked in sessions. - /// - internal class VirtualKeyTrackingRealtimeClient : IRealtimeAudioClient - { - private readonly IRealtimeAudioClient _innerClient; - private readonly string _virtualKey; - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - - public VirtualKeyTrackingRealtimeClient( - IRealtimeAudioClient innerClient, - string virtualKey, - IServiceProvider serviceProvider, - ILogger logger) - { - _innerClient = innerClient; - _virtualKey = virtualKey; - _serviceProvider = serviceProvider; - _logger = logger; - } - - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - var session = await _innerClient.CreateSessionAsync(config, apiKey ?? _virtualKey, cancellationToken); - - // Ensure virtual key is stored in metadata - if (session.Metadata == null) - { - session.Metadata = new Dictionary(); - } - session.Metadata["VirtualKey"] = apiKey ?? _virtualKey; - - // Store session in session store if available - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - if (sessionStore != null) - { - await sessionStore.StoreSessionAsync(session, cancellationToken: cancellationToken); - _logger.LogDebug("Stored realtime session {SessionId} for virtual key {VirtualKey}", - session.Id, apiKey ?? _virtualKey); - } - - return session; - } - - public IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - return _innerClient.StreamAudioAsync(session, cancellationToken); - } - - public Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - return _innerClient.UpdateSessionAsync(session, updates, cancellationToken); - } - - public async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - await _innerClient.CloseSessionAsync(session, cancellationToken); - - // Update session in store - using var scope = _serviceProvider.CreateScope(); - var sessionStore = scope.ServiceProvider.GetService(); - if (sessionStore != null) - { - session.State = SessionState.Closed; - await sessionStore.UpdateSessionAsync(session, cancellationToken); - } - } - - public Task SupportsRealtimeAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return _innerClient.SupportsRealtimeAsync(apiKey ?? _virtualKey, cancellationToken); - } - - public Task GetCapabilitiesAsync( - CancellationToken cancellationToken = default) - { - return _innerClient.GetCapabilitiesAsync(cancellationToken); - } - } -} diff --git a/ConduitLLM.Core/Validation/UsageValidator.cs b/ConduitLLM.Core/Validation/UsageValidator.cs index 74f35fb73..08914f4e1 100644 --- a/ConduitLLM.Core/Validation/UsageValidator.cs +++ b/ConduitLLM.Core/Validation/UsageValidator.cs @@ -83,11 +83,6 @@ public ValidationResult Validate(Usage usage) errors.Add("Video duration must be positive"); } - // Validate audio duration - if (usage.AudioDurationSeconds.HasValue && usage.AudioDurationSeconds.Value <= 0) - { - errors.Add("Audio duration must be positive"); - } // Validate search metadata consistency if (usage.SearchMetadata != null) diff --git a/ConduitLLM.Http/Controllers/AudioController.cs b/ConduitLLM.Http/Controllers/AudioController.cs deleted file mode 100644 index e525fd9a4..000000000 --- a/ConduitLLM.Http/Controllers/AudioController.cs +++ /dev/null @@ -1,409 +0,0 @@ -using System.ComponentModel.DataAnnotations; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.Mvc; -using ConduitLLM.Http.Authorization; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Handles audio-related operations including transcription and text-to-speech. - /// - [ApiController] - [Route("v1/audio")] - [Authorize(AuthenticationSchemes = "VirtualKey")] - [RequireBalance] - [Tags("Audio")] - public class AudioController : ControllerBase - { - private readonly IAudioRouter _audioRouter; - private readonly ConduitLLM.Configuration.Interfaces.IVirtualKeyService _virtualKeyService; - private readonly ILogger _logger; - private readonly ConduitLLM.Configuration.Interfaces.IModelProviderMappingService _modelMappingService; - - public AudioController( - IAudioRouter audioRouter, - ConduitLLM.Configuration.Interfaces.IVirtualKeyService virtualKeyService, - ILogger logger, - ConduitLLM.Configuration.Interfaces.IModelProviderMappingService modelMappingService) - { - _audioRouter = audioRouter ?? throw new ArgumentNullException(nameof(audioRouter)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _modelMappingService = modelMappingService ?? throw new ArgumentNullException(nameof(modelMappingService)); - } - - /// - /// Transcribes audio into text. - /// - /// The audio file to transcribe. - /// The model to use for transcription (e.g., "whisper-1"). - /// The language of the input audio (ISO-639-1). - /// Optional text to guide the model's style. - /// The format of the transcript output. - /// Sampling temperature between 0 and 1. - /// The timestamp granularities to populate. - /// The transcription result. - [HttpPost("transcriptions")] - [Consumes("multipart/form-data")] - [ProducesResponseType(typeof(AudioTranscriptionResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] - public async Task TranscribeAudio( - [Required] IFormFile file, - [FromForm] string model = "whisper-1", - [FromForm] string? language = null, - [FromForm] string? prompt = null, - [FromForm] string? response_format = null, - [FromForm, Range(0, 1)] double? temperature = null, - [FromForm] string[]? timestamp_granularities = null) - { - // Get virtual key from context - var virtualKey = HttpContext.User.FindFirst("VirtualKey")?.Value; - if (string.IsNullOrEmpty(virtualKey)) - { - return Unauthorized(new ProblemDetails - { - Title = "Unauthorized", - Detail = "Invalid or missing API key" - }); - } - - // Validate file - if (file.Length == 0) - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = "Audio file is empty" - }); - } - - // Check file size (25MB limit for OpenAI) - const long maxFileSize = 25 * 1024 * 1024; - if (file.Length > maxFileSize) - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = $"Audio file exceeds maximum size of {maxFileSize / (1024 * 1024)}MB" - }); - } - - try - { - // Get provider info for usage tracking - try - { - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(model); - if (modelMapping != null) - { - HttpContext.Items["ProviderId"] = modelMapping.ProviderId; - HttpContext.Items["ProviderType"] = modelMapping.Provider?.ProviderType; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to get provider info for model {Model}", model); - } - - // Read file into memory - byte[] audioData; - using (var memoryStream = new MemoryStream()) - { - await file.CopyToAsync(memoryStream); - audioData = memoryStream.ToArray(); - } - - // Parse response format - TranscriptionFormat? format = null; - if (!string.IsNullOrEmpty(response_format)) - { - if (Enum.TryParse(response_format, true, out var parsedFormat)) - { - format = parsedFormat; - } - else - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = $"Invalid response_format: {response_format}" - }); - } - } - - // Create transcription request - var request = new AudioTranscriptionRequest - { - AudioData = audioData, - FileName = file.FileName, - Model = model, - Language = language, - Prompt = prompt, - ResponseFormat = format, - Temperature = temperature, - TimestampGranularity = timestamp_granularities?.Contains("word") == true ? TimestampGranularity.Word : - timestamp_granularities?.Contains("segment") == true ? TimestampGranularity.Segment : - TimestampGranularity.None - }; - - // Route to appropriate provider - var client = await _audioRouter.GetTranscriptionClientAsync(request, virtualKey); - if (client == null) - { - return BadRequest(new ProblemDetails - { - Title = "No Provider Available", - Detail = "No audio transcription provider is available for this request" - }); - } - - // Perform transcription - var response = await client.TranscribeAudioAsync(request); - - // Update spend based on estimated cost - // Estimate cost based on audio duration (rough estimate: 1MB = 1 minute = $0.006) - var estimatedMinutes = audioData.Length / (1024.0 * 1024.0); - var estimatedCost = (decimal)(estimatedMinutes * 0.006); - - // Get virtual key entity to update spend - var virtualKeyEntity = await _virtualKeyService.GetVirtualKeyByKeyValueAsync(virtualKey); - if (virtualKeyEntity != null) - { - await _virtualKeyService.UpdateSpendAsync(virtualKeyEntity.Id, estimatedCost); - } - - // Return response based on format - if (format == TranscriptionFormat.Vtt || - format == TranscriptionFormat.Srt) - { - return Content(response.Text, "text/plain"); - } - - return Ok(response); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error transcribing audio"); - return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails - { - Title = "Internal Server Error", - Detail = "An error occurred while transcribing the audio" - }); - } - } - - /// - /// Generates audio from input text. - /// - /// The text-to-speech request. - /// The generated audio file. - [HttpPost("speech")] - [Consumes("application/json")] - [Produces("audio/mpeg", "audio/opus", "audio/aac", "audio/flac", "audio/wav", "audio/pcm")] - [ProducesResponseType(StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status429TooManyRequests)] - public async Task GenerateSpeech([FromBody, Required] TextToSpeechRequestDto request) - { - // Get virtual key from context - var virtualKey = HttpContext.User.FindFirst("VirtualKey")?.Value; - if (string.IsNullOrEmpty(virtualKey)) - { - return Unauthorized(new ProblemDetails - { - Title = "Unauthorized", - Detail = "Invalid or missing API key" - }); - } - - // Validate request - if (string.IsNullOrWhiteSpace(request.Input)) - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = "Input text is required" - }); - } - - // Validate input length (4096 chars limit for OpenAI) - const int maxInputLength = 4096; - if (request.Input.Length > maxInputLength) - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = $"Input text exceeds maximum length of {maxInputLength} characters" - }); - } - - try - { - // Get provider info for usage tracking - try - { - var modelMapping = await _modelMappingService.GetMappingByModelAliasAsync(request.Model); - if (modelMapping != null) - { - HttpContext.Items["ProviderId"] = modelMapping.ProviderId; - HttpContext.Items["ProviderType"] = modelMapping.Provider?.ProviderType; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, "Failed to get provider info for model {Model}", request.Model); - } - - // Parse response format - AudioFormat format = AudioFormat.Mp3; - if (!string.IsNullOrEmpty(request.ResponseFormat)) - { - if (!Enum.TryParse(request.ResponseFormat, true, out format)) - { - return BadRequest(new ProblemDetails - { - Title = "Invalid Request", - Detail = $"Invalid response_format: {request.ResponseFormat}" - }); - } - } - - // Create TTS request - var ttsRequest = new TextToSpeechRequest - { - Input = request.Input, - Model = request.Model, - Voice = request.Voice, - ResponseFormat = format, - Speed = request.Speed - }; - - // Route to appropriate provider - var client = await _audioRouter.GetTextToSpeechClientAsync(ttsRequest, virtualKey); - if (client == null) - { - return BadRequest(new ProblemDetails - { - Title = "No Provider Available", - Detail = "No text-to-speech provider is available for this request" - }); - } - - // Check if streaming is requested - if (HttpContext.Request.Headers.Accept.Contains("text/event-stream")) - { - // Stream the audio - Response.ContentType = GetContentType(format); - Response.Headers["Cache-Control"] = "no-cache"; - Response.Headers["X-Accel-Buffering"] = "no"; - - await foreach (var chunk in client.StreamSpeechAsync(ttsRequest, virtualKey)) - { - if (chunk.Data != null && chunk.Data.Length > 0) - { - await Response.Body.WriteAsync(chunk.Data); - await Response.Body.FlushAsync(); - } - } - - return new EmptyResult(); - } - else - { - // Generate complete audio - var response = await client.CreateSpeechAsync(ttsRequest, virtualKey); - - // Update spend based on estimated cost - // Estimate cost based on character count (rough estimate: $0.015 per 1K chars for tts-1) - var characterCount = request.Input.Length; - var estimatedCost = (decimal)(characterCount / 1000.0 * 0.015); - - // Get virtual key entity to update spend - var virtualKeyEntity = await _virtualKeyService.GetVirtualKeyByKeyValueAsync(virtualKey); - if (virtualKeyEntity != null) - { - await _virtualKeyService.UpdateSpendAsync(virtualKeyEntity.Id, estimatedCost); - } - - // Return audio file - return File(response.AudioData, GetContentType(format)); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error generating speech"); - return StatusCode(StatusCodes.Status500InternalServerError, new ProblemDetails - { - Title = "Internal Server Error", - Detail = "An error occurred while generating speech" - }); - } - } - - /// - /// Translates audio into English text. - /// - /// The audio file to translate. - /// The model to use for translation. - /// Optional text to guide the model's style. - /// The format of the translation output. - /// Sampling temperature between 0 and 1. - /// The translation result. - [HttpPost("translations")] - [Consumes("multipart/form-data")] - [ProducesResponseType(typeof(AudioTranscriptionResponse), StatusCodes.Status200OK)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)] - [ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status401Unauthorized)] - public async Task TranslateAudio( - [Required] IFormFile file, - [FromForm] string model = "whisper-1", - [FromForm] string? prompt = null, - [FromForm] string? response_format = null, - [FromForm, Range(0, 1)] double? temperature = null) - { - // Translation is just transcription with target language set to English - return await TranscribeAudio( - file, - model, - "en", // Force English output - prompt, - response_format, - temperature, - null); - } - - private string GetContentType(AudioFormat format) - { - return format switch - { - AudioFormat.Mp3 => "audio/mpeg", - AudioFormat.Opus => "audio/opus", - AudioFormat.Aac => "audio/aac", - AudioFormat.Flac => "audio/flac", - AudioFormat.Wav => "audio/wav", - AudioFormat.Pcm => "audio/pcm", - _ => "audio/mpeg" - }; - } - - private long EstimateTranscriptionTokens(string text) - { - // Rough estimate: 1 token per 4 characters - return text.Length / 4; - } - - private long EstimateTTSTokens(string text) - { - // Rough estimate: 1 token per 4 characters - return text.Length / 4; - } - - } -} diff --git a/ConduitLLM.Http/Controllers/DiscoveryController.cs b/ConduitLLM.Http/Controllers/DiscoveryController.cs index b17c6b7a0..cae7bbd2c 100644 --- a/ConduitLLM.Http/Controllers/DiscoveryController.cs +++ b/ConduitLLM.Http/Controllers/DiscoveryController.cs @@ -113,9 +113,6 @@ public async Task GetModels([FromQuery] string? capability = null "chat" => caps.SupportsChat, "streaming" or "chat_stream" => caps.SupportsStreaming, "vision" => caps.SupportsVision, - "audio_transcription" => caps.SupportsAudioTranscription, - "text_to_speech" => caps.SupportsTextToSpeech, - "realtime_audio" => caps.SupportsRealtimeAudio, "video_generation" => caps.SupportsVideoGeneration, "image_generation" => caps.SupportsImageGeneration, "embeddings" => caps.SupportsEmbeddings, @@ -174,9 +171,6 @@ public async Task GetModels([FromQuery] string? capability = null supports_streaming = caps.SupportsStreaming, supports_vision = caps.SupportsVision, supports_function_calling = caps.SupportsFunctionCalling, - supports_audio_transcription = caps.SupportsAudioTranscription, - supports_text_to_speech = caps.SupportsTextToSpeech, - supports_realtime_audio = caps.SupportsRealtimeAudio, supports_video_generation = caps.SupportsVideoGeneration, supports_image_generation = caps.SupportsImageGeneration, supports_embeddings = caps.SupportsEmbeddings @@ -185,9 +179,6 @@ public async Task GetModels([FromQuery] string? capability = null // - context_window (from capabilities or series metadata) // - training_cutoff date // - pricing_tier or cost information - // - supported_languages (parsed from JSON) - // - supported_voices (for TTS models) - // - supported_formats (for audio models) // - rate_limits // - model_version }); @@ -234,9 +225,6 @@ public Task GetCapabilities() "chat", "chat_stream", "vision", - "audio_transcription", - "text_to_speech", - "realtime_audio", "video_generation", "image_generation", "embeddings", diff --git a/ConduitLLM.Http/Controllers/EmbeddingsController.cs b/ConduitLLM.Http/Controllers/EmbeddingsController.cs index eda463e55..9effef700 100644 --- a/ConduitLLM.Http/Controllers/EmbeddingsController.cs +++ b/ConduitLLM.Http/Controllers/EmbeddingsController.cs @@ -1,3 +1,4 @@ +using ConduitLLM.Core; using ConduitLLM.Core.Controllers; using ConduitLLM.Core.Interfaces; using ConduitLLM.Core.Models; @@ -20,17 +21,17 @@ namespace ConduitLLM.Http.Controllers [Tags("Embeddings")] public class EmbeddingsController : EventPublishingControllerBase { - private readonly ILLMRouter _router; + private readonly Conduit _conduit; private readonly ILogger _logger; private readonly ConduitLLM.Configuration.Interfaces.IModelProviderMappingService _modelMappingService; public EmbeddingsController( - ILLMRouter router, + Conduit conduit, ILogger logger, ConduitLLM.Configuration.Interfaces.IModelProviderMappingService modelMappingService, IPublishEndpoint publishEndpoint) : base(publishEndpoint, logger) { - _router = router ?? throw new ArgumentNullException(nameof(router)); + _conduit = conduit ?? throw new ArgumentNullException(nameof(conduit)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _modelMappingService = modelMappingService ?? throw new ArgumentNullException(nameof(modelMappingService)); } @@ -81,7 +82,9 @@ public async Task CreateEmbedding( _logger.LogWarning(ex, "Failed to get provider info for model {Model}", request.Model); } - var response = await _router.CreateEmbeddingAsync(request, cancellationToken: cancellationToken); + // Get the client for the specified model and create embeddings + var client = _conduit.GetClient(request.Model); + var response = await client.CreateEmbeddingAsync(request, cancellationToken: cancellationToken); return Ok(response); } catch (Exception ex) diff --git a/ConduitLLM.Http/Controllers/HybridAudioController.cs b/ConduitLLM.Http/Controllers/HybridAudioController.cs deleted file mode 100644 index 522edbdc7..000000000 --- a/ConduitLLM.Http/Controllers/HybridAudioController.cs +++ /dev/null @@ -1,317 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.AspNetCore.Authorization; -using ConduitLLM.Configuration.DTOs; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Controller for hybrid audio processing that chains STT, LLM, and TTS services. - /// - /// - /// This controller provides conversational AI capabilities for providers that don't have - /// native real-time audio support, by orchestrating a pipeline of separate services. - /// - [ApiController] - [Route("v1/audio/hybrid")] - [Authorize(AuthenticationSchemes = "VirtualKey")] - public class HybridAudioController : ControllerBase - { - private readonly IHybridAudioService _hybridAudioService; - private readonly ConduitLLM.Configuration.Interfaces.IVirtualKeyService _virtualKeyService; - private readonly ILogger _logger; - - /// - /// Initializes a new instance of the class. - /// - /// The hybrid audio service. - /// The virtual key service. - /// The logger instance. - public HybridAudioController( - IHybridAudioService hybridAudioService, - ConduitLLM.Configuration.Interfaces.IVirtualKeyService virtualKeyService, - ILogger logger) - { - _hybridAudioService = hybridAudioService ?? throw new ArgumentNullException(nameof(hybridAudioService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Processes audio input through the hybrid STT-LLM-TTS pipeline. - /// - /// The audio file to process. - /// Optional session ID for maintaining conversation context. - /// Optional language code for transcription. - /// Optional system prompt for the LLM. - /// Optional voice ID for TTS synthesis. - /// Desired output audio format (default: mp3). - /// Temperature for LLM response generation (0.0-2.0). - /// Maximum tokens for the LLM response. - /// The synthesized audio response. - /// Returns the synthesized audio data. - /// If the request is invalid. - /// If authentication fails. - /// If the user lacks audio permissions. - /// If an internal error occurs. - [HttpPost("process")] - [Consumes("multipart/form-data")] - [Produces("audio/mpeg", "audio/wav", "audio/flac")] - [ProducesResponseType(typeof(FileContentResult), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status500InternalServerError)] - public async Task ProcessAudio( - IFormFile file, - [FromForm] string? sessionId = null, - [FromForm] string? language = null, - [FromForm] string? systemPrompt = null, - [FromForm] string? voiceId = null, - [FromForm] string outputFormat = "mp3", - [FromForm] double temperature = 0.7, - [FromForm] int maxTokens = 150) - { - try - { - // Validate file - if (file == null || file.Length == 0) - { - return BadRequest(new ErrorResponseDto("No audio file provided")); - } - - // Check permissions - var apiKey = HttpContext.Items["ApiKey"]?.ToString(); - if (!string.IsNullOrEmpty(apiKey) && apiKey.StartsWith("vk-")) - { - var virtualKey = await _virtualKeyService.GetVirtualKeyByKeyValueAsync(apiKey); - if (virtualKey == null || !virtualKey.IsEnabled) - { - return Forbid("Virtual key is not valid or enabled"); - } - } - - // Read audio data - byte[] audioData; - using (var memoryStream = new MemoryStream()) - { - await file.CopyToAsync(memoryStream); - audioData = memoryStream.ToArray(); - } - - // Determine audio format from content type or filename - var audioFormat = GetAudioFormat(file.ContentType, file.FileName); - - // Create request - var request = new HybridAudioRequest - { - SessionId = sessionId, - AudioData = audioData, - AudioFormat = audioFormat, - Language = language, - SystemPrompt = systemPrompt, - VoiceId = voiceId, - OutputFormat = outputFormat, - Temperature = temperature, - MaxTokens = maxTokens, - EnableStreaming = false, - VirtualKey = apiKey - }; - - // Process audio - var response = await _hybridAudioService.ProcessAudioAsync(request, HttpContext.RequestAborted); - - // Log usage - _logger.LogInformation("Hybrid audio processed - Input: {InputDuration}s, Output: {OutputDuration}s, Session: {SessionId}", - response.Metrics?.InputDurationSeconds, - response.Metrics?.OutputDurationSeconds, - (sessionId ?? "none").Replace(Environment.NewLine, "")); - - // Return audio file - var contentType = GetContentType(response.AudioFormat); - return File(response.AudioData, contentType, $"response.{response.AudioFormat}"); - } - catch (ArgumentException ex) - { - _logger.LogWarning(ex, - "Invalid hybrid audio request"); - return BadRequest(new ErrorResponseDto(ex.Message)); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error processing hybrid audio"); - return StatusCode(500, new ErrorResponseDto("An error occurred processing the audio")); - } - } - - /// - /// Creates a new conversation session for maintaining context. - /// - /// The session configuration. - /// The created session ID. - /// Returns the session ID. - /// If the configuration is invalid. - /// If authentication fails. - /// If the user lacks audio permissions. - [HttpPost("sessions")] - [Produces("application/json")] - [ProducesResponseType(typeof(CreateSessionResponse), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - public async Task CreateSession([FromBody] HybridSessionConfig config) - { - try - { - // Check permissions - var apiKey = HttpContext.Items["ApiKey"]?.ToString(); - if (!string.IsNullOrEmpty(apiKey) && apiKey.StartsWith("vk-")) - { - var virtualKey = await _virtualKeyService.GetVirtualKeyByKeyValueAsync(apiKey); - if (virtualKey == null || !virtualKey.IsEnabled) - { - return Forbid("Virtual key is not valid or enabled"); - } - } - - // Create session - var sessionId = await _hybridAudioService.CreateSessionAsync(config, HttpContext.RequestAborted); - - return Ok(new CreateSessionResponse { SessionId = sessionId }); - } - catch (ArgumentException ex) - { - return BadRequest(new ErrorResponseDto(ex.Message)); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error creating hybrid audio session"); - return StatusCode(500, new ErrorResponseDto("An error occurred creating the session")); - } - } - - /// - /// Closes an active conversation session. - /// - /// The session ID to close. - /// No content. - /// Session closed successfully. - /// If the session ID is invalid. - /// If authentication fails. - [HttpDelete("sessions/{sessionId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task CloseSession(string sessionId) - { - try - { - await _hybridAudioService.CloseSessionAsync(sessionId, HttpContext.RequestAborted); - return NoContent(); - } - catch (ArgumentException ex) - { - return BadRequest(new ErrorResponseDto(ex.Message)); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error closing hybrid audio session"); - return StatusCode(500, new ErrorResponseDto("An error occurred closing the session")); - } - } - - /// - /// Checks if the hybrid audio service is available. - /// - /// Service availability status. - /// Returns the availability status. - /// If authentication fails. - [HttpGet("status")] - [Produces("application/json")] - [ProducesResponseType(typeof(ServiceStatus), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task GetStatus() - { - try - { - var isAvailable = await _hybridAudioService.IsAvailableAsync(HttpContext.RequestAborted); - var metrics = await _hybridAudioService.GetLatencyMetricsAsync(HttpContext.RequestAborted); - - return Ok(new ServiceStatus - { - Available = isAvailable, - LatencyMetrics = metrics - }); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error checking hybrid audio status"); - return Ok(new ServiceStatus { Available = false }); - } - } - - private string GetAudioFormat(string contentType, string fileName) - { - // Try to determine from content type - return contentType?.ToLower() switch - { - "audio/mpeg" => "mp3", - "audio/mp3" => "mp3", - "audio/wav" => "wav", - "audio/wave" => "wav", - "audio/webm" => "webm", - "audio/flac" => "flac", - "audio/ogg" => "ogg", - _ => string.IsNullOrEmpty(Path.GetExtension(fileName)?.TrimStart('.').ToLower()) - ? "mp3" - : Path.GetExtension(fileName).TrimStart('.').ToLower() - }; - } - - private string GetContentType(string format) - { - return format?.ToLower() switch - { - "mp3" => "audio/mpeg", - "wav" => "audio/wav", - "webm" => "audio/webm", - "flac" => "audio/flac", - "ogg" => "audio/ogg", - _ => "audio/mpeg" - }; - } - - /// - /// Response for session creation. - /// - public class CreateSessionResponse - { - /// - /// Gets or sets the created session ID. - /// - public string SessionId { get; set; } = string.Empty; - } - - /// - /// Service status response. - /// - public class ServiceStatus - { - /// - /// Gets or sets whether the service is available. - /// - public bool Available { get; set; } - - /// - /// Gets or sets the latency metrics. - /// - public HybridLatencyMetrics? LatencyMetrics { get; set; } - } - } -} diff --git a/ConduitLLM.Http/Controllers/ModelsController.cs b/ConduitLLM.Http/Controllers/ModelsController.cs index 12620c01f..147015165 100644 --- a/ConduitLLM.Http/Controllers/ModelsController.cs +++ b/ConduitLLM.Http/Controllers/ModelsController.cs @@ -16,18 +16,18 @@ namespace ConduitLLM.Http.Controllers [Tags("Models")] public class ModelsController : ControllerBase { - private readonly ILLMRouter _router; private readonly ILogger _logger; private readonly IModelMetadataService _metadataService; + private readonly ConduitLLM.Configuration.Interfaces.IModelProviderMappingRepository _modelMappingRepository; public ModelsController( - ILLMRouter router, ILogger logger, - IModelMetadataService metadataService) + IModelMetadataService metadataService, + ConduitLLM.Configuration.Interfaces.IModelProviderMappingRepository modelMappingRepository) { - _router = router ?? throw new ArgumentNullException(nameof(router)); _logger = logger ?? throw new ArgumentNullException(nameof(logger)); _metadataService = metadataService ?? throw new ArgumentNullException(nameof(metadataService)); + _modelMappingRepository = modelMappingRepository ?? throw new ArgumentNullException(nameof(modelMappingRepository)); } /// @@ -37,21 +37,24 @@ public ModelsController( [HttpGet("models")] [ProducesResponseType(typeof(object), StatusCodes.Status200OK)] [ProducesResponseType(typeof(OpenAIErrorResponse), StatusCodes.Status500InternalServerError)] - public IActionResult ListModels() + public async Task ListModels() { try { _logger.LogInformation("Getting available models"); - // Get model names from the router - var modelNames = _router.GetAvailableModels(); - - // Convert to OpenAI format - var basicModelData = modelNames.Select(m => new - { - id = m, - @object = "model" - }).ToList(); + // Get model mappings from the repository + var mappings = await _modelMappingRepository.GetAllAsync(); + + // Convert to OpenAI format using model aliases + var basicModelData = mappings + .Select(m => m.ModelAlias) + .Distinct() + .Select(alias => new + { + id = alias, + @object = "model" + }).ToList(); // Create the response envelope var response = new diff --git a/ConduitLLM.Http/Controllers/ProviderModelsController.cs b/ConduitLLM.Http/Controllers/ProviderModelsController.cs index e6002b554..9283ce344 100644 --- a/ConduitLLM.Http/Controllers/ProviderModelsController.cs +++ b/ConduitLLM.Http/Controllers/ProviderModelsController.cs @@ -70,9 +70,7 @@ public async Task GetProviderModels(int providerId) case ProviderType.OpenAICompatible: query = query.Where(m => m.Capabilities.SupportsChat || m.Capabilities.SupportsImageGeneration || - m.Capabilities.SupportsEmbeddings || - m.Capabilities.SupportsAudioTranscription || - m.Capabilities.SupportsTextToSpeech); + m.Capabilities.SupportsEmbeddings); break; case ProviderType.Replicate: @@ -82,9 +80,6 @@ public async Task GetProviderModels(int providerId) m.Capabilities.SupportsChat); break; - case ProviderType.ElevenLabs: - query = query.Where(m => m.Capabilities.SupportsTextToSpeech); - break; case ProviderType.Groq: case ProviderType.Cerebras: diff --git a/ConduitLLM.Http/Controllers/RealtimeController.cs b/ConduitLLM.Http/Controllers/RealtimeController.cs deleted file mode 100644 index cf82c3497..000000000 --- a/ConduitLLM.Http/Controllers/RealtimeController.cs +++ /dev/null @@ -1,262 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.AspNetCore.Authorization; -using ConduitLLM.Configuration.DTOs; -using Microsoft.AspNetCore.Mvc; - -namespace ConduitLLM.Http.Controllers -{ - /// - /// Handles WebSocket connections for real-time audio streaming. - /// - /// - /// This controller manages WebSocket connections between clients and real-time audio providers, - /// acting as a proxy that handles authentication, routing, usage tracking, and message translation. - /// - [ApiController] - [Route("v1/realtime")] - [Authorize] - public class RealtimeController : ControllerBase - { - private readonly ILogger _logger; - private readonly IRealtimeProxyService _proxyService; - private readonly IVirtualKeyService _virtualKeyService; - private readonly IRealtimeConnectionManager _connectionManager; - - /// - /// Initializes a new instance of the RealtimeController class. - /// - public RealtimeController( - ILogger logger, - IRealtimeProxyService proxyService, - IVirtualKeyService virtualKeyService, - IRealtimeConnectionManager connectionManager) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _proxyService = proxyService ?? throw new ArgumentNullException(nameof(proxyService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager)); - } - - /// - /// Establishes a WebSocket connection for real-time audio streaming. - /// - /// The model to use for the real-time session (e.g., "gpt-4o-realtime-preview") - /// Optional provider override (defaults to routing based on model) - /// WebSocket connection or error response - /// WebSocket connection established - /// Invalid request or WebSocket not supported - /// Authentication failed - /// Virtual key does not have access to real-time features - /// No available providers for the requested model - [HttpGet("connect")] - [ProducesResponseType(StatusCodes.Status101SwitchingProtocols)] - [ProducesResponseType(StatusCodes.Status400BadRequest)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status403Forbidden)] - [ProducesResponseType(StatusCodes.Status503ServiceUnavailable)] - public async Task Connect( - [FromQuery] string model, - [FromQuery] string? provider = null) - { - if (!HttpContext.WebSockets.IsWebSocketRequest) - { - return BadRequest(new ErrorResponseDto("WebSocket connection required")); - } - - // Extract virtual key from authorization header - var virtualKey = ExtractVirtualKey(); - if (string.IsNullOrEmpty(virtualKey)) - { - return Unauthorized(new ErrorResponseDto("Virtual key required")); - } - - // Validate virtual key and check permissions - var keyEntity = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKey, model); - if (keyEntity == null) - { - return Unauthorized(new ErrorResponseDto("Invalid virtual key")); - } - - // Check if the key has real-time permissions - if (!HasRealtimePermissions(keyEntity)) - { - return StatusCode(403, new ErrorResponseDto("Virtual key does not have real-time audio permissions")); - } - - try - { - // Accept the WebSocket connection - using var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync(); - - // Generate a unique connection ID - var connectionId = Guid.NewGuid().ToString(); - - _logger.LogInformation("WebSocket connection established. ConnectionId: {ConnectionId}, Model: {Model}, VirtualKeyId: {KeyId}", - connectionId, - model.Replace(Environment.NewLine, ""), - keyEntity.Id); - - // Register the connection - await _connectionManager.RegisterConnectionAsync(connectionId, keyEntity.Id, model, webSocket); - - try - { - // Start the proxy session - await _proxyService.HandleConnectionAsync( - connectionId, - webSocket, - keyEntity, - model, - provider, - HttpContext.RequestAborted); - } - finally - { - // Ensure connection is unregistered - await _connectionManager.UnregisterConnectionAsync(connectionId); - } - - return new EmptyResult(); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error handling WebSocket connection"); - return StatusCode(503, new { error = "Failed to establish real-time connection", details = ex.Message }); - } - } - - /// - /// Gets the status of active real-time connections for the authenticated user. - /// - /// List of active connection statuses - [HttpGet("connections")] - [ProducesResponseType(typeof(ConnectionStatusResponse), StatusCodes.Status200OK)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - public async Task GetConnections() - { - var virtualKey = ExtractVirtualKey(); - if (string.IsNullOrEmpty(virtualKey)) - { - return Unauthorized(new ErrorResponseDto("Virtual key required")); - } - - var keyEntity = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKey); - if (keyEntity == null) - { - return Unauthorized(new ErrorResponseDto("Invalid virtual key")); - } - - var connections = await _connectionManager.GetActiveConnectionsAsync(keyEntity.Id); - - return Ok(new ConnectionStatusResponse - { - VirtualKeyId = keyEntity.Id, - ActiveConnections = connections - }); - } - - /// - /// Terminates a specific real-time connection. - /// - /// The ID of the connection to terminate - /// Success or error response - [HttpDelete("connections/{connectionId}")] - [ProducesResponseType(StatusCodes.Status204NoContent)] - [ProducesResponseType(StatusCodes.Status401Unauthorized)] - [ProducesResponseType(StatusCodes.Status404NotFound)] - public async Task TerminateConnection(string connectionId) - { - var virtualKey = ExtractVirtualKey(); - if (string.IsNullOrEmpty(virtualKey)) - { - return Unauthorized(new ErrorResponseDto("Virtual key required")); - } - - var keyEntity = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKey); - if (keyEntity == null) - { - return Unauthorized(new ErrorResponseDto("Invalid virtual key")); - } - - var terminated = await _connectionManager.TerminateConnectionAsync(connectionId, keyEntity.Id); - if (!terminated) - { - return NotFound(new ErrorResponseDto("Connection not found or not owned by this key")); - } - - return NoContent(); - } - - private string? ExtractVirtualKey() - { - // Try Authorization header first - var authHeader = Request.Headers["Authorization"].ToString(); - if (!string.IsNullOrEmpty(authHeader)) - { - if (authHeader.StartsWith("Bearer ", StringComparison.OrdinalIgnoreCase)) - { - return authHeader.Substring(7); - } - } - - // Try X-API-Key header - var apiKeyHeader = Request.Headers["X-API-Key"].ToString(); - if (!string.IsNullOrEmpty(apiKeyHeader)) - { - return apiKeyHeader; - } - - return null; - } - - private bool HasRealtimePermissions(ConduitLLM.Configuration.Entities.VirtualKey keyEntity) - { - // Check if the key has real-time permissions - // This could be based on: - // 1. A specific permission flag - // 2. The models allowed for the key - // 3. A feature flag in the key's metadata - - // For now, we'll allow all keys with audio model access - // In production, you'd want more granular control - - if (keyEntity.AllowedModels?.Contains("realtime", StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - - // Check if any allowed model contains "realtime" or specific realtime models - var realtimeModels = new[] { "gpt-4o-realtime-preview", "ultravox", "elevenlabs-conversational" }; - if (keyEntity.AllowedModels != null) - { - foreach (var model in keyEntity.AllowedModels.Split(',', StringSplitOptions.RemoveEmptyEntries)) - { - if (realtimeModels.Any(rm => model.Trim().Equals(rm, StringComparison.OrdinalIgnoreCase))) - { - return true; - } - } - } - - return false; // Default to false - require explicit permissions - } - } - - /// - /// Response model for connection status queries. - /// - public class ConnectionStatusResponse - { - /// - /// The virtual key ID. - /// - public int VirtualKeyId { get; set; } - - /// - /// List of active connections. - /// - public List ActiveConnections { get; set; } = new(); - } -} diff --git a/ConduitLLM.Http/Extensions/AudioServiceExtensions.cs b/ConduitLLM.Http/Extensions/AudioServiceExtensions.cs deleted file mode 100644 index 471694f4e..000000000 --- a/ConduitLLM.Http/Extensions/AudioServiceExtensions.cs +++ /dev/null @@ -1,107 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; -using ConduitLLM.Http.Middleware; -using ConduitLLM.Http.Services; - -namespace ConduitLLM.Http.Extensions -{ - /// - /// Extension methods for configuring audio services in production. - /// - public static class AudioServiceExtensions - { - /// - /// Adds production-ready audio services to the service collection. - /// - public static IServiceCollection AddProductionAudioServices( - this IServiceCollection services, - IConfiguration configuration) - { - // Configure HTTP clients for audio services - services.AddHttpClient(); - services.AddHttpClient(); - - // PrometheusAudioMetricsExporter removed - metrics handled differently now - - // Add graceful shutdown - services.AddSingleton(); - services.AddHostedService(); - - // Configure options - services.Configure( - configuration.GetSection("AudioService:ConnectionPool")); - services.Configure( - configuration.GetSection("AudioService:Cache")); - services.Configure( - configuration.GetSection("AudioService:Monitoring")); - services.Configure( - configuration.GetSection("AudioService:Cdn")); - services.Configure( - configuration.GetSection("AudioService:Monitoring")); - - return services; - } - - - /// - /// Configures the audio service middleware pipeline. - /// - public static IApplicationBuilder UseProductionAudioServices( - this IApplicationBuilder app) - { - // Add correlation ID middleware - app.UseMiddleware(); - - // Add Prometheus metrics endpoint - app.UseMiddleware(); - - // Health check endpoints are mapped in Program.cs via MapConduitHealthChecks() - // to avoid duplicate endpoint registrations - - return app; - } - } - - /// - /// Manages realtime sessions for graceful shutdown. - /// - internal class RealtimeSessionManager : IRealtimeSessionManager - { - private readonly IRealtimeAudioClient _realtimeClient; - private readonly ILogger _logger; - - public RealtimeSessionManager( - IRealtimeAudioClient realtimeClient, - ILogger logger) - { - _realtimeClient = realtimeClient; - _logger = logger; - } - - public async Task> GetActiveSessionsAsync(CancellationToken cancellationToken) - { - // This would typically query a session store or tracking service - _logger.LogInformation("Getting active realtime sessions"); - - // For now, return empty list as placeholder - await Task.CompletedTask; // Make it truly async - return new List(); - } - - public async Task SendCloseNotificationAsync(string sessionId, string reason, CancellationToken cancellationToken) - { - _logger.LogInformation("Sending close notification to session {SessionId}: {Reason}", sessionId, reason); - - // Implementation would send a WebSocket message to the client - await Task.CompletedTask; - } - - public async Task CloseSessionAsync(string sessionId, CancellationToken cancellationToken) - { - _logger.LogInformation("Forcefully closing session {SessionId}", sessionId); - - // Implementation would close the WebSocket connection and clean up resources - await Task.CompletedTask; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Program.CoreServices.cs b/ConduitLLM.Http/Program.CoreServices.cs index 3d248c28c..8917881b4 100644 --- a/ConduitLLM.Http/Program.CoreServices.cs +++ b/ConduitLLM.Http/Program.CoreServices.cs @@ -182,10 +182,6 @@ public static void ConfigureCoreServices(WebApplicationBuilder builder) // Image generation metrics service removed - not needed - // Add required services for the router components - builder.Services.AddScoped(); - builder.Services.AddScoped(); - // Register token counter service for context management builder.Services.AddScoped(); builder.Services.AddScoped(); @@ -398,8 +394,8 @@ public static void ConfigureCoreServices(WebApplicationBuilder builder) // Register File Retrieval Service builder.Services.AddScoped(); - // Register Audio services - builder.Services.AddConduitAudioServices(builder.Configuration); + // Register Model Capability services (capability detection and caching) + builder.Services.AddModelCapabilityServices(builder.Configuration); // Register Batch Cache Invalidation service builder.Services.AddBatchCacheInvalidation(builder.Configuration); @@ -413,23 +409,7 @@ public static void ConfigureCoreServices(WebApplicationBuilder builder) // Register Redis batch operations for optimized cache management builder.Services.AddSingleton(); - // Register Real-time Audio services - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddScoped(); - builder.Services.AddScoped(); - builder.Services.AddHostedService(provider => - provider.GetRequiredService() as RealtimeConnectionManager ?? - throw new InvalidOperationException("RealtimeConnectionManager not registered properly")); - - // Register Real-time Message Translators - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - builder.Services.AddSingleton(); - - // Register Audio routing - builder.Services.AddScoped(); - builder.Services.AddScoped(); + // Register Image Generation Retry Configuration builder.Services.Configure( diff --git a/ConduitLLM.Http/Services/DiscoveryCacheWarmingService.cs b/ConduitLLM.Http/Services/DiscoveryCacheWarmingService.cs index e00f82e4a..22824f164 100644 --- a/ConduitLLM.Http/Services/DiscoveryCacheWarmingService.cs +++ b/ConduitLLM.Http/Services/DiscoveryCacheWarmingService.cs @@ -186,9 +186,6 @@ private async Task WarmCacheForCapability( "chat" => caps.SupportsChat, "streaming" or "chat_stream" => caps.SupportsStreaming, "vision" => caps.SupportsVision, - "audio_transcription" => caps.SupportsAudioTranscription, - "text_to_speech" => caps.SupportsTextToSpeech, - "realtime_audio" => caps.SupportsRealtimeAudio, "video_generation" => caps.SupportsVideoGeneration, "image_generation" => caps.SupportsImageGeneration, "embeddings" => caps.SupportsEmbeddings, @@ -223,9 +220,6 @@ private async Task WarmCacheForCapability( supports_streaming = caps.SupportsStreaming, supports_vision = caps.SupportsVision, supports_function_calling = caps.SupportsFunctionCalling, - supports_audio_transcription = caps.SupportsAudioTranscription, - supports_text_to_speech = caps.SupportsTextToSpeech, - supports_realtime_audio = caps.SupportsRealtimeAudio, supports_video_generation = caps.SupportsVideoGeneration, supports_image_generation = caps.SupportsImageGeneration, supports_embeddings = caps.SupportsEmbeddings diff --git a/ConduitLLM.Http/Services/GracefulShutdownService.cs b/ConduitLLM.Http/Services/GracefulShutdownService.cs deleted file mode 100644 index 8915be11c..000000000 --- a/ConduitLLM.Http/Services/GracefulShutdownService.cs +++ /dev/null @@ -1,300 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Http.Services -{ - /// - /// Manages graceful shutdown of audio services to prevent data loss and ensure clean termination. - /// - public class GracefulShutdownService : IHostedService - { - private readonly IHostApplicationLifetime _applicationLifetime; - private readonly IAudioConnectionPool _connectionPool; - private readonly IAudioStreamCache _cache; - private readonly IAudioMetricsCollector _metricsCollector; - private readonly IRealtimeSessionManager _sessionManager; - private readonly ILogger _logger; - private readonly SemaphoreSlim _shutdownSemaphore = new(1, 1); - private CancellationTokenSource? _shutdownTokenSource; - private bool _isShuttingDown; - - public GracefulShutdownService( - IHostApplicationLifetime applicationLifetime, - IAudioConnectionPool connectionPool, - IAudioStreamCache cache, - IAudioMetricsCollector metricsCollector, - IRealtimeSessionManager sessionManager, - ILogger logger) - { - _applicationLifetime = applicationLifetime; - _connectionPool = connectionPool; - _cache = cache; - _metricsCollector = metricsCollector; - _sessionManager = sessionManager; - _logger = logger; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _applicationLifetime.ApplicationStopping.Register(OnApplicationStopping); - _logger.LogInformation("Graceful shutdown service started"); - return Task.CompletedTask; - } - - public Task StopAsync(CancellationToken cancellationToken) - { - return Task.CompletedTask; - } - - private void OnApplicationStopping() - { - _logger.LogInformation("Application shutdown initiated, beginning graceful shutdown sequence"); - - try - { - // Create a cancellation token for shutdown operations - _shutdownTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(30)); - - // Execute shutdown sequence - Task.Run(async () => await ExecuteShutdownSequenceAsync(_shutdownTokenSource.Token)) - .GetAwaiter() - .GetResult(); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during graceful shutdown"); - } - } - - private async Task ExecuteShutdownSequenceAsync(CancellationToken cancellationToken) - { - await _shutdownSemaphore.WaitAsync(cancellationToken); - try - { - if (_isShuttingDown) - { - _logger.LogWarning("Shutdown already in progress"); - return; - } - - _isShuttingDown = true; - var shutdownSteps = new (string stepName, Func stepAction)[] - { - ("Stop accepting new requests", StopAcceptingRequestsAsync), - ("Close realtime sessions", CloseRealtimeSessionsAsync), - ("Flush cache", FlushCacheAsync), - ("Export final metrics", ExportFinalMetricsAsync), - ("Close connection pool", CloseConnectionPoolAsync), - ("Final cleanup", FinalCleanupAsync) - }; - - foreach (var (stepName, stepAction) in shutdownSteps) - { - if (cancellationToken.IsCancellationRequested) - { - _logger.LogWarning("Shutdown sequence cancelled at step: {Step}", stepName); - break; - } - - try - { - _logger.LogInformation("Executing shutdown step: {Step}", stepName); - await stepAction(cancellationToken); - _logger.LogInformation("Completed shutdown step: {Step}", stepName); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error during shutdown step: {Step}", stepName); - } - } - - _logger.LogInformation("Graceful shutdown sequence completed"); - } - finally - { - _shutdownSemaphore.Release(); - } - } - - private async Task StopAcceptingRequestsAsync(CancellationToken cancellationToken) - { - // Signal that we're no longer accepting new requests - // This would typically be done through a health check endpoint - _logger.LogInformation("Marking service as not ready for new requests"); - - // Give load balancer time to stop routing traffic - await Task.Delay(TimeSpan.FromSeconds(15), cancellationToken); - } - - private async Task CloseRealtimeSessionsAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Closing active realtime sessions"); - - var activeSessions = await _sessionManager.GetActiveSessionsAsync(cancellationToken); - _logger.LogInformation("Found {Count} active sessions to close", activeSessions.Count); - - var closeTasks = activeSessions.Select(async session => - { - try - { - // Send graceful close message to client - await _sessionManager.SendCloseNotificationAsync( - session.Id, - "Server is shutting down for maintenance", - cancellationToken); - - // Wait briefly for client acknowledgment - await Task.Delay(TimeSpan.FromSeconds(2), cancellationToken); - - // Force close the session - await _sessionManager.CloseSessionAsync(session.Id, cancellationToken); - - _logger.LogDebug("Closed session: {SessionId}", session.Id); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing session: {SessionId}", session.Id); - } - }); - - await Task.WhenAll(closeTasks); - _logger.LogInformation("All realtime sessions closed"); - } - - private async Task FlushCacheAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Flushing cache to persistent storage"); - - try - { - // Get cache statistics before flush - var stats = await _cache.GetStatisticsAsync(); - _logger.LogInformation( - "Cache statistics before flush: Items={Items}, Size={Size}MB", - stats.TotalEntries, - stats.TotalSizeBytes / (1024 * 1024)); - - // Flush any pending writes - FlushAsync doesn't exist on IAudioStreamCache - // await _cache.FlushAsync(cancellationToken); - await _cache.ClearExpiredAsync(); - - _logger.LogInformation("Cache flush completed"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error flushing cache"); - } - } - - private async Task ExportFinalMetricsAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Exporting final metrics"); - - try - { - // Get final snapshot - var snapshot = await _metricsCollector.GetCurrentSnapshotAsync(); - - // Log summary metrics - _logger.LogInformation( - "Final metrics summary: TotalRequests={TotalRequests}, ErrorRate={ErrorRate:P2}, AvgLatency={AvgLatency}ms", - snapshot.RequestsPerSecond, - snapshot.CurrentErrorRate, - 0.0); // ProviderMetrics doesn't exist on AudioMetricsSnapshot - - // Force export to monitoring system - method doesn't exist - // await _metricsCollector.ForceExportAsync(cancellationToken); - _logger.LogInformation("Metrics collector doesn't have ForceExportAsync method"); - - _logger.LogInformation("Metrics export completed"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error exporting metrics"); - } - } - - private async Task CloseConnectionPoolAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Closing connection pool"); - - try - { - // Get pool statistics - var stats = await _connectionPool.GetStatisticsAsync(); - _logger.LogInformation( - "Connection pool status: Total={Total}, Active={Active}, Idle={Idle}", - stats.TotalCreated, - stats.ActiveConnections, - stats.IdleConnections); - - // Close all connections gracefully - method doesn't exist - // await _connectionPool.CloseAllAsync(cancellationToken); - // Clear idle connections instead - await _connectionPool.ClearIdleConnectionsAsync(TimeSpan.Zero); - - _logger.LogInformation("Connection pool closed"); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error closing connection pool"); - } - } - - private async Task FinalCleanupAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("Performing final cleanup"); - - // Any additional cleanup tasks - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken); - - _logger.LogInformation("Final cleanup completed"); - } - } - - /// - /// Manages active realtime sessions during shutdown. - /// - public interface IRealtimeSessionManager - { - /// - /// Gets all active realtime sessions. - /// - Task> GetActiveSessionsAsync(CancellationToken cancellationToken = default); - - /// - /// Sends a close notification to a session. - /// - Task SendCloseNotificationAsync(string sessionId, string reason, CancellationToken cancellationToken = default); - - /// - /// Closes a session forcefully. - /// - Task CloseSessionAsync(string sessionId, CancellationToken cancellationToken = default); - } - - /// - /// Information about an active realtime session. - /// - public class RealtimeSessionInfo - { - /// - /// Gets or sets the session ID. - /// - public string Id { get; set; } = string.Empty; - - /// - /// Gets or sets the user ID. - /// - public string UserId { get; set; } = string.Empty; - - /// - /// Gets or sets when the session was created. - /// - public DateTime CreatedAt { get; set; } - - /// - /// Gets or sets whether the session is active. - /// - public bool IsActive { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/RealtimeConnectionManager.cs b/ConduitLLM.Http/Services/RealtimeConnectionManager.cs deleted file mode 100644 index 7c4d1f007..000000000 --- a/ConduitLLM.Http/Services/RealtimeConnectionManager.cs +++ /dev/null @@ -1,389 +0,0 @@ -using System.Collections.Concurrent; -using System.Net.WebSockets; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Realtime; - -namespace ConduitLLM.Http.Services -{ - /// - /// Manages active WebSocket connections for real-time audio streaming. - /// - public class RealtimeConnectionManager : IRealtimeConnectionManager, IHostedService - { - private readonly ILogger _logger; - private readonly IConfiguration? _configuration; - private readonly ConcurrentDictionary _connections = new(); - private readonly ConcurrentDictionary> _connectionsByVirtualKey = new(); - private readonly ConcurrentDictionary _connectionToVirtualKey = new(); // For string-based virtual keys in tests - private readonly Timer? _cleanupTimer; - private readonly int _maxConnectionsPerKey; - private readonly int _maxTotalConnections; - private readonly TimeSpan _staleConnectionTimeout; - - public RealtimeConnectionManager(ILogger logger) - : this(logger, null) - { - } - - public RealtimeConnectionManager( - ILogger logger, - IConfiguration? configuration) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _configuration = configuration; - - // Load configuration with defaults - _maxConnectionsPerKey = _configuration?.GetValue("Realtime:MaxConnectionsPerKey", 5) ?? 5; - _maxTotalConnections = _configuration?.GetValue("Realtime:MaxTotalConnections", 1000) ?? 1000; - _staleConnectionTimeout = TimeSpan.FromMinutes(_configuration?.GetValue("Realtime:StaleConnectionTimeoutMinutes", 30) ?? 30); - - // Only setup cleanup timer if we have configuration - if (_configuration != null) - { - _cleanupTimer = new Timer( - async _ => await CleanupStaleConnectionsAsync(), - null, - TimeSpan.FromMinutes(5), - TimeSpan.FromMinutes(5)); - } - } - - // Synchronous methods for testing - public void RegisterConnection(string connectionId, string virtualKey, string model, string provider) - { - var connection = new ManagedConnection - { - Info = new ConduitLLM.Core.Models.Realtime.ConnectionInfo - { - ConnectionId = connectionId, - Model = model, - ConnectedAt = DateTime.UtcNow, - State = "active", - StartTime = DateTime.UtcNow, - LastActivity = DateTime.UtcNow, - VirtualKey = virtualKey, - Provider = provider - }, - VirtualKeyId = 0, // For testing - LastHeartbeat = DateTime.UtcNow, - IsHealthy = true - }; - - _connections.TryAdd(connectionId, connection); - _connectionToVirtualKey.TryAdd(connectionId, virtualKey); - } - - public void UnregisterConnection(string connectionId) - { - if (_connections.TryRemove(connectionId, out var connection)) - { - _connectionToVirtualKey.TryRemove(connectionId, out _); - - // Remove from per-key collection - if (_connectionsByVirtualKey.TryGetValue(connection.VirtualKeyId, out var keyConnections)) - { - keyConnections.Remove(connectionId); - - if (keyConnections.Count() == 0) - { - _connectionsByVirtualKey.TryRemove(connection.VirtualKeyId, out _); - } - } - } - } - - public void UpdateConnectionProvider(string connectionId, string providerConnectionId) - { - if (_connections.TryGetValue(connectionId, out var connection)) - { - connection.Info.ProviderConnectionId = providerConnectionId; - } - } - - public ConduitLLM.Core.Models.Realtime.ConnectionInfo? GetConnectionInfo(string connectionId) - { - return _connections.TryGetValue(connectionId, out var connection) ? connection.Info : null; - } - - public List GetConnectionsByVirtualKey(string virtualKey) - { - return _connections.Values - .Where(c => _connectionToVirtualKey.TryGetValue(c.Info.ConnectionId, out var key) && key == virtualKey) - .Select(c => c.Info) - .ToList(); - } - - public void IncrementUsage(string connectionId, long audioBytes, long tokens, decimal cost) - { - if (_connections.TryGetValue(connectionId, out var connection)) - { - connection.Info.AudioBytesProcessed += audioBytes; - connection.Info.TokensUsed += tokens; - connection.Info.EstimatedCost += cost; - connection.Info.LastActivity = DateTime.UtcNow; - } - } - - public List GetActiveConnections() - { - return _connections.Values.Select(c => c.Info).ToList(); - } - - // Async interface methods - public async Task RegisterConnectionAsync( - string connectionId, - int virtualKeyId, - string model, - WebSocket webSocket) - { - // Check total connection limit - if (_connections.Count() >= _maxTotalConnections) - { - throw new InvalidOperationException($"Maximum total connections ({_maxTotalConnections}) reached"); - } - - // Check per-key limit - if (await IsAtConnectionLimitAsync(virtualKeyId)) - { - throw new InvalidOperationException($"Virtual key {virtualKeyId} has reached maximum connections ({_maxConnectionsPerKey})"); - } - - var connection = new ManagedConnection - { - Info = new ConduitLLM.Core.Models.Realtime.ConnectionInfo - { - ConnectionId = connectionId, - Model = model, - ConnectedAt = DateTime.UtcNow, - State = "active", - StartTime = DateTime.UtcNow, - LastActivity = DateTime.UtcNow - }, - WebSocket = webSocket, - VirtualKeyId = virtualKeyId, - LastHeartbeat = DateTime.UtcNow, - IsHealthy = true - }; - - // Add to main collection - if (!_connections.TryAdd(connectionId, connection)) - { - throw new InvalidOperationException($"Connection {connectionId} already registered"); - } - - // Add to per-key collection - _connectionsByVirtualKey.AddOrUpdate( - virtualKeyId, - new HashSet { connectionId }, - (_, set) => - { - set.Add(connectionId); - return set; - }); - - _logger.LogInformation( - "Registered connection {ConnectionId} for virtual key {VirtualKeyId}", - connectionId, virtualKeyId); - } - - public async Task UnregisterConnectionAsync(string connectionId) - { - if (_connections.TryRemove(connectionId, out var connection)) - { - // Remove from per-key collection - if (_connectionsByVirtualKey.TryGetValue(connection.VirtualKeyId, out var keyConnections)) - { - keyConnections.Remove(connectionId); - - if (keyConnections.Count() == 0) - { - _connectionsByVirtualKey.TryRemove(connection.VirtualKeyId, out _); - } - } - - _logger.LogInformation( - "Unregistered connection {ConnectionId} for virtual key {VirtualKeyId}", - connectionId.Replace(Environment.NewLine, ""), connection.VirtualKeyId); - } - - await Task.CompletedTask; - } - - public async Task> GetActiveConnectionsAsync(int virtualKeyId) - { - var connections = new List(); - - if (_connectionsByVirtualKey.TryGetValue(virtualKeyId, out var connectionIds)) - { - foreach (var id in connectionIds) - { - if (_connections.TryGetValue(id, out var connection)) - { - connections.Add(connection.Info); - } - } - } - - return await Task.FromResult(connections); - } - - public async Task GetTotalConnectionCountAsync() - { - return await Task.FromResult(_connections.Count()); - } - - public async Task GetConnectionAsync(string connectionId) - { - if (_connections.TryGetValue(connectionId, out var connection)) - { - return await Task.FromResult(connection.Info); - } - - return await Task.FromResult(null); - } - - public async Task TerminateConnectionAsync(string connectionId, int virtualKeyId) - { - if (_connections.TryGetValue(connectionId, out var connection)) - { - // Verify ownership - if (connection.VirtualKeyId != virtualKeyId) - { - return false; - } - - // Close WebSocket if still open - if (connection.WebSocket != null && connection.WebSocket.State == WebSocketState.Open) - { - try - { - await connection.WebSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Connection terminated by user", - CancellationToken.None); - } - catch (Exception ex) - { -_logger.LogError(ex, "Error closing WebSocket for connection {ConnectionId}", connectionId.Replace(Environment.NewLine, "")); - } - } - - // Remove from collections - await UnregisterConnectionAsync(connectionId); - - return true; - } - - return false; - } - - public async Task IsAtConnectionLimitAsync(int virtualKeyId) - { - if (_connectionsByVirtualKey.TryGetValue(virtualKeyId, out var connections)) - { - return await Task.FromResult(connections.Count() >= _maxConnectionsPerKey); - } - - return false; - } - - public async Task UpdateUsageStatsAsync(string connectionId, ConnectionUsageStats stats) - { - if (_connections.TryGetValue(connectionId, out var connection)) - { - connection.Info.Usage = stats; - connection.LastHeartbeat = DateTime.UtcNow; - } - - await Task.CompletedTask; - } - - public async Task CleanupStaleConnectionsAsync() - { - var cleanedCount = 0; - var now = DateTime.UtcNow; - var staleConnections = new List(); - - foreach (var kvp in _connections) - { - var connection = kvp.Value; - var timeSinceHeartbeat = now - connection.LastHeartbeat; - - // Check if connection is stale - if (timeSinceHeartbeat > _staleConnectionTimeout) - { - staleConnections.Add(kvp.Key); - } - // Check WebSocket state - else if (connection.WebSocket != null && - connection.WebSocket.State != WebSocketState.Open && - connection.WebSocket.State != WebSocketState.Connecting) - { - staleConnections.Add(kvp.Key); - } - } - - // Clean up stale connections - foreach (var connectionId in staleConnections) - { - _logger.LogWarning( - "Cleaning up stale connection {ConnectionId}", - connectionId); - - if (_connections.TryRemove(connectionId, out var connection)) - { - await TerminateConnectionAsync(connectionId, connection.VirtualKeyId); - cleanedCount++; - } - } - - if (cleanedCount > 0) - { - _logger.LogInformation( - "Cleaned up {Count} stale connections", - cleanedCount); - } - - return cleanedCount; - } - - public Task StartAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("RealtimeConnectionManager started"); - return Task.CompletedTask; - } - - public async Task StopAsync(CancellationToken cancellationToken) - { - _logger.LogInformation("RealtimeConnectionManager stopping..."); - - // Stop cleanup timer - if (_cleanupTimer != null) - { - await _cleanupTimer.DisposeAsync(); - } - - // Close all active connections - var tasks = new List(); - - foreach (var connection in _connections.Values) - { - if (connection.WebSocket != null && connection.WebSocket.State == WebSocketState.Open) - { - tasks.Add(connection.WebSocket.CloseAsync( - WebSocketCloseStatus.EndpointUnavailable, - "Server shutting down", - cancellationToken)); - } - } - - await Task.WhenAll(tasks); - - _connections.Clear(); - _connectionsByVirtualKey.Clear(); - - _logger.LogInformation("RealtimeConnectionManager stopped"); - } - } -} diff --git a/ConduitLLM.Http/Services/RealtimeMessageTranslatorFactory.cs b/ConduitLLM.Http/Services/RealtimeMessageTranslatorFactory.cs deleted file mode 100644 index 0ddcdb720..000000000 --- a/ConduitLLM.Http/Services/RealtimeMessageTranslatorFactory.cs +++ /dev/null @@ -1,87 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; - -namespace ConduitLLM.Http.Services -{ - /// - /// Factory for creating real-time message translators. - /// - public class RealtimeMessageTranslatorFactory : IRealtimeMessageTranslatorFactory - { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _translators = new(); - - public RealtimeMessageTranslatorFactory( - IServiceProvider serviceProvider, - ILogger logger) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - // Register default translators - RegisterDefaultTranslators(); - } - - public IRealtimeMessageTranslator? GetTranslator(string provider) - { - if (_translators.TryGetValue(provider.ToLowerInvariant(), out var translator)) - { - return translator; - } - - // Try to resolve from DI container - var translatorType = provider.ToLowerInvariant() switch - { - "openai" => typeof(Providers.Translators.OpenAIRealtimeTranslatorV2), - // Add other providers as they're implemented - // "ultravox" => typeof(Providers.Translators.UltravoxRealtimeTranslator), - // "elevenlabs" => typeof(Providers.Translators.ElevenLabsRealtimeTranslator), - _ => null - }; - - if (translatorType != null) - { - try - { - var instance = ActivatorUtilities.CreateInstance(_serviceProvider, translatorType) as IRealtimeMessageTranslator; - if (instance != null) - { - RegisterTranslator(provider, instance); - return instance; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create translator for provider {Provider}", provider); - } - } - - _logger.LogWarning("No translator found for provider: {Provider}", provider); - return null; - } - - public void RegisterTranslator(string provider, IRealtimeMessageTranslator translator) - { - _translators[provider.ToLowerInvariant()] = translator; - _logger.LogInformation("Registered translator for provider: {Provider}", provider); - } - - public bool HasTranslator(string provider) - { - return _translators.ContainsKey(provider.ToLowerInvariant()); - } - - public string[] GetRegisteredProviders() - { - return _translators.Keys.ToArray(); - } - - private void RegisterDefaultTranslators() - { - // OpenAI translator will be created on demand - // Add any translators that should be pre-registered here - } - } -} diff --git a/ConduitLLM.Http/Services/RealtimeProxyService.ProxyOperations.cs b/ConduitLLM.Http/Services/RealtimeProxyService.ProxyOperations.cs deleted file mode 100644 index 7ae1918fe..000000000 --- a/ConduitLLM.Http/Services/RealtimeProxyService.ProxyOperations.cs +++ /dev/null @@ -1,200 +0,0 @@ -using System.Net.WebSockets; -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that proxies WebSocket connections - Proxy operations - /// - public partial class RealtimeProxyService - { - private async Task ProxyClientToProviderAsync( - WebSocket clientWs, - IAsyncDuplexStream providerStream, - string connectionId, - string virtualKey, - CancellationToken cancellationToken) - { - var bufferArray = new byte[4096]; - var buffer = new ArraySegment(bufferArray); - - while (!cancellationToken.IsCancellationRequested && - clientWs.State == WebSocketState.Open && - providerStream.IsConnected) - { - try - { - var result = await clientWs.ReceiveAsync(buffer, cancellationToken); - - if (result.MessageType == WebSocketMessageType.Close) - { - _logger.LogInformation("Client closed connection {ConnectionId}", - connectionId); - break; - } - - if (result.MessageType == WebSocketMessageType.Binary) - { - // Assume binary data is audio - var audioFrame = new RealtimeAudioFrame - { - AudioData = bufferArray.Take(result.Count).ToArray(), - IsOutput = false, - SampleRate = 24000, // Default, should be configurable - Channels = 1 - }; - - await providerStream.SendAsync(audioFrame, cancellationToken); - - // Track input audio usage - var audioDuration = audioFrame.AudioData.Length / (double)(audioFrame.SampleRate * audioFrame.Channels * 2); // 16-bit audio - await _usageTracker.RecordAudioUsageAsync(connectionId, audioDuration, isInput: true); - - // Track bytes sent to provider - UpdateConnectionMetrics(connectionId, bytesSent: audioFrame.AudioData.Length); - } - else if (result.MessageType == WebSocketMessageType.Text) - { - var message = Encoding.UTF8.GetString(bufferArray, 0, result.Count); - - // Track bytes sent to provider - UpdateConnectionMetrics(connectionId, bytesSent: result.Count); - - // Try to parse as JSON control message - try - { - using var doc = JsonDocument.Parse(message); - var root = doc.RootElement; - - if (root.TryGetProperty("type", out var typeElement)) - { - var type = typeElement.GetString(); - - if (type == "audio" && root.TryGetProperty("data", out var dataElement)) - { - // Audio data sent as base64 in JSON - var audioData = Convert.FromBase64String(dataElement.GetString() ?? ""); - var audioFrame = new RealtimeAudioFrame - { - AudioData = audioData, - IsOutput = false, - SampleRate = 24000, - Channels = 1 - }; - - await providerStream.SendAsync(audioFrame, cancellationToken); - - // Track input audio usage - var audioDuration = audioFrame.AudioData.Length / (double)(audioFrame.SampleRate * audioFrame.Channels * 2); // 16-bit audio - await _usageTracker.RecordAudioUsageAsync(connectionId, audioDuration, isInput: true); - } - // Handle other control messages as needed - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, - "Error parsing client message"); - } - } - } - catch (WebSocketException ex) - { - _logger.LogError(ex, - "WebSocket error in client->provider proxy for {ConnectionId}", - connectionId); - - // Track error - UpdateConnectionMetrics(connectionId, lastError: ex.Message); - break; - } - } - } - - private async Task ProxyProviderToClientAsync( - IAsyncDuplexStream providerStream, - WebSocket clientWs, - string connectionId, - string virtualKey, - CancellationToken cancellationToken) - { - try - { - await foreach (var response in providerStream.ReceiveAsync(cancellationToken)) - { - if (!clientWs.State.Equals(WebSocketState.Open)) - { - _logger.LogInformation("Client WebSocket closed for {ConnectionId}", - connectionId); - break; - } - - try - { - // Convert response to client format - var clientMessage = ConvertResponseToClientMessage(response); - var messageBytes = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(clientMessage)); - - await clientWs.SendAsync( - new ArraySegment(messageBytes), - WebSocketMessageType.Text, - true, - cancellationToken); - - // Track bytes received from provider - UpdateConnectionMetrics(connectionId, bytesReceived: messageBytes.Length); - - // Track usage based on response type - await TrackResponseUsageAsync(connectionId, response, virtualKey); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error processing provider response for {ConnectionId}", - connectionId); - - // Track error - UpdateConnectionMetrics(connectionId, lastError: ex.Message); - } - } - } - catch (OperationCanceledException) - { - _logger.LogInformation("Provider stream cancelled for {ConnectionId}", - connectionId); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error in provider->client proxy for {ConnectionId}", - connectionId); - - // Track error - UpdateConnectionMetrics(connectionId, lastError: ex.Message); - } - } - - private async Task CloseWebSocketAsync(WebSocket webSocket, string reason) - { - if (webSocket.State == WebSocketState.Open || webSocket.State == WebSocketState.CloseReceived) - { - try - { - await webSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - reason, - CancellationToken.None); - } - catch (Exception ex) - { - _logger.LogWarning(ex, - "Error closing WebSocket"); - } - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/RealtimeProxyService.Tracking.cs b/ConduitLLM.Http/Services/RealtimeProxyService.Tracking.cs deleted file mode 100644 index cd1836c4e..000000000 --- a/ConduitLLM.Http/Services/RealtimeProxyService.Tracking.cs +++ /dev/null @@ -1,358 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Core.Models.Realtime; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that proxies WebSocket connections - Tracking and helper methods - /// - public partial class RealtimeProxyService - { - private RealtimeMessage? ParseClientMessage(string message) - { - try - { - using var doc = JsonDocument.Parse(message); - var root = doc.RootElement; - - if (root.TryGetProperty("type", out var typeElement)) - { - var type = typeElement.GetString(); - - // Simple parsing based on type - return type switch - { - "audio_frame" => new RealtimeAudioFrame - { - AudioData = Convert.FromBase64String( - root.GetProperty("audio").GetString() ?? ""), - IsOutput = false - }, - _ => null - }; - } - } - catch - { - // Ignore parsing errors - } - - return null; - } - - private RealtimeUsageUpdate? ParseUsageFromProviderMessage(string message) - { - try - { - using var doc = JsonDocument.Parse(message); - var root = doc.RootElement; - - if (root.TryGetProperty("type", out var typeElement) && - typeElement.GetString() == "response.done" && - root.TryGetProperty("response", out var response) && - response.TryGetProperty("usage", out var usage)) - { - var update = new RealtimeUsageUpdate(); - - if (usage.TryGetProperty("total_tokens", out var totalTokens)) - update.TotalTokens = totalTokens.GetInt64(); - - if (usage.TryGetProperty("input_tokens", out var inputTokens)) - update.InputTokens = inputTokens.GetInt64(); - - if (usage.TryGetProperty("output_tokens", out var outputTokens)) - update.OutputTokens = outputTokens.GetInt64(); - - if (usage.TryGetProperty("input_token_details", out var inputDetails)) - { - update.InputTokenDetails = new Dictionary(); - foreach (var prop in inputDetails.EnumerateObject()) - { - if (prop.Value.ValueKind == JsonValueKind.Number) - update.InputTokenDetails[prop.Name] = prop.Value.GetInt64(); - } - } - - if (usage.TryGetProperty("output_token_details", out var outputDetails)) - { - update.OutputTokenDetails = new Dictionary(); - foreach (var prop in outputDetails.EnumerateObject()) - { - if (prop.Value.ValueKind == JsonValueKind.Number) - update.OutputTokenDetails[prop.Name] = prop.Value.GetInt64(); - } - } - - return update; - } - } - catch (Exception ex) - { - _logger.LogWarning(ex, - "Error parsing usage from provider message"); - } - - return null; - } - - private async Task HandleUsageUpdate(string connectionId, string virtualKey, RealtimeUsageUpdate usage) - { - try - { - // Update usage statistics - var stats = new ConnectionUsageStats - { - AudioDurationSeconds = 0, // Will be tracked separately - MessagesSent = 0, - MessagesReceived = 0, - EstimatedCost = 0.01m * usage.TotalTokens // Simple cost calculation - }; - await _connectionManager.UpdateUsageStatsAsync(connectionId, stats); - - // Update spend for virtual key tracking - var estimatedCost = stats.EstimatedCost; - if (estimatedCost > 0) - { - try - { - var virtualKeyEntity = await _virtualKeyService.ValidateVirtualKeyAsync(virtualKey); - if (virtualKeyEntity != null) - { - await _virtualKeyService.UpdateSpendAsync(virtualKeyEntity.Id, estimatedCost); - _logger.LogDebug("Updated virtual key {VirtualKeyId} spend by ${Cost:F4} for connection {ConnectionId}", - virtualKeyEntity.Id, estimatedCost, connectionId); - } - else - { - _logger.LogWarning("Virtual key not found for key value during spend update for connection {ConnectionId}", - connectionId); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to update virtual key spend for connection {ConnectionId}, cost: ${Cost:F4}", - connectionId, estimatedCost); - // Don't throw - continue processing even if spend tracking fails - } - } - - _logger.LogDebug("Updated usage for connection {ConnectionId}: {TotalTokens} tokens", - connectionId, - usage.TotalTokens); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error handling usage update for connection {ConnectionId}", - connectionId); - } - } - - private async Task TrackResponseUsageAsync(string connectionId, RealtimeResponse response, string virtualKey) - { - try - { - switch (response.EventType) - { - case RealtimeEventType.AudioDelta: - if (response.Audio != null && response.Audio.Data.Length > 0) - { - // Assume 24kHz, 1 channel, 16-bit audio for output - var audioDuration = response.Audio.Data.Length / (24000.0 * 1 * 2); - await _usageTracker.RecordAudioUsageAsync(connectionId, audioDuration, isInput: false); - } - break; - - case RealtimeEventType.ResponseComplete: - // Check if response contains usage information - if (response.Usage != null) - { - var usage = new Usage - { - PromptTokens = (int)(response.Usage.InputTokens ?? 0), - CompletionTokens = (int)(response.Usage.OutputTokens ?? 0), - TotalTokens = (int)(response.Usage.TotalTokens ?? 0) - }; - await _usageTracker.RecordTokenUsageAsync(connectionId, usage); - - // Track function calls if any - if (response.Usage.FunctionCalls > 0) - { - // Record each function call - for (int i = 0; i < response.Usage.FunctionCalls; i++) - { - await _usageTracker.RecordFunctionCallAsync(connectionId); - } - } - } - break; - - case RealtimeEventType.ToolCallRequest: - // Track function call when it's requested - if (response.ToolCall != null) - { - await _usageTracker.RecordFunctionCallAsync(connectionId, response.ToolCall.FunctionName); - } - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error tracking response usage for connection {ConnectionId}", - connectionId); - } - } - - private object ConvertResponseToClientMessage(RealtimeResponse response) - { - // Convert to a simple client-friendly format - return response.EventType switch - { - RealtimeEventType.AudioDelta => new - { - type = "audio", - data = response.Audio != null ? Convert.ToBase64String(response.Audio.Data) : null, - isComplete = response.Audio?.IsComplete ?? false - }, - RealtimeEventType.TranscriptionDelta => new - { - type = "transcription", - text = response.Transcription?.Text, - role = response.Transcription?.Role, - isFinal = response.Transcription?.IsFinal ?? false - }, - RealtimeEventType.TextResponse => new - { - type = "text", - text = response.TextResponse - }, - RealtimeEventType.ToolCallRequest => new - { - type = "function_call", - callId = response.ToolCall?.CallId, - name = response.ToolCall?.FunctionName, - arguments = response.ToolCall?.Arguments - }, - RealtimeEventType.Error => new - { - type = "error", - message = response.Error?.Message, - code = response.Error?.Code - }, - _ => new - { - type = response.EventType.ToString().ToLowerInvariant(), - data = response - } - }; - } - - /// - /// Updates connection metrics for enhanced tracking. - /// - private void UpdateConnectionMetrics(string connectionId, long bytesSent = 0, long bytesReceived = 0, string? lastError = null) - { - lock (_metricsLock) - { - if (!_connectionMetrics.ContainsKey(connectionId)) - { - _connectionMetrics[connectionId] = new ConnectionMetrics(); - } - - var metrics = _connectionMetrics[connectionId]; - metrics.BytesSent += bytesSent; - metrics.BytesReceived += bytesReceived; - metrics.LastActivityAt = DateTime.UtcNow; - - if (!string.IsNullOrEmpty(lastError)) - { - metrics.LastError = lastError; - metrics.ErrorCount++; - } - } - } - - public async Task GetConnectionStatusAsync(string connectionId) - { - var info = await _connectionManager.GetConnectionAsync(connectionId); - if (info == null) - return null; - - // Get enhanced metrics - ConnectionMetrics? metrics = null; - lock (_metricsLock) - { - _connectionMetrics.TryGetValue(connectionId, out metrics); - } - - return new ProxyConnectionStatus - { - ConnectionId = connectionId, - State = info.State switch - { - "active" => ProxyConnectionState.Active, - "closed" => ProxyConnectionState.Closed, - "error" => ProxyConnectionState.Failed, - _ => ProxyConnectionState.Connecting - }, - Provider = info.Provider ?? string.Empty, - Model = info.Model, - ConnectedAt = info.ConnectedAt, - LastActivityAt = info.LastActivity, - MessagesToProvider = info.Usage?.MessagesSent ?? 0, - MessagesFromProvider = info.Usage?.MessagesReceived ?? 0, - BytesSent = metrics?.BytesSent ?? 0, - BytesReceived = metrics?.BytesReceived ?? 0, - EstimatedCost = info.EstimatedCost, - LastError = metrics?.LastError - }; - } - - public async Task CloseConnectionAsync(string connectionId, string? reason = null) - { - var info = await _connectionManager.GetConnectionAsync(connectionId); - if (info == null) - return false; - - await _connectionManager.UnregisterConnectionAsync(connectionId); - - // Clean up metrics - lock (_metricsLock) - { - _connectionMetrics.Remove(connectionId); - } - - return await Task.FromResult(true); - } - } - - /// - /// Enhanced metrics for realtime connections. - /// - internal class ConnectionMetrics - { - public long BytesSent { get; set; } - public long BytesReceived { get; set; } - public string? LastError { get; set; } - public int ErrorCount { get; set; } - public DateTime LastActivityAt { get; set; } = DateTime.UtcNow; - public DateTime CreatedAt { get; set; } = DateTime.UtcNow; - } - - /// - /// Usage update from real-time providers. - /// - public class RealtimeUsageUpdate - { - public long TotalTokens { get; set; } - public long InputTokens { get; set; } - public long OutputTokens { get; set; } - public Dictionary? InputTokenDetails { get; set; } - public Dictionary? OutputTokenDetails { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Http/Services/RealtimeProxyService.cs b/ConduitLLM.Http/Services/RealtimeProxyService.cs deleted file mode 100644 index f3ee54f2c..000000000 --- a/ConduitLLM.Http/Services/RealtimeProxyService.cs +++ /dev/null @@ -1,165 +0,0 @@ -using System.Net.WebSockets; - -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Core.Models.Realtime; - -namespace ConduitLLM.Http.Services -{ - /// - /// Service that proxies WebSocket connections between clients and real-time audio providers. - /// - public partial class RealtimeProxyService : IRealtimeProxyService - { - private readonly IRealtimeMessageTranslatorFactory _translatorFactory; - private readonly IVirtualKeyService _virtualKeyService; - private readonly IRealtimeConnectionManager _connectionManager; - private readonly IRealtimeUsageTracker _usageTracker; - private readonly ILogger _logger; - - // Enhanced metrics tracking - private readonly Dictionary _connectionMetrics = new(); - private readonly object _metricsLock = new(); - - public RealtimeProxyService( - IRealtimeMessageTranslatorFactory translatorFactory, - IVirtualKeyService virtualKeyService, - IRealtimeConnectionManager connectionManager, - IRealtimeUsageTracker usageTracker, - ILogger logger) - { - _translatorFactory = translatorFactory ?? throw new ArgumentNullException(nameof(translatorFactory)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - _connectionManager = connectionManager ?? throw new ArgumentNullException(nameof(connectionManager)); - _usageTracker = usageTracker ?? throw new ArgumentNullException(nameof(usageTracker)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public async Task HandleConnectionAsync( - string connectionId, - WebSocket clientWebSocket, - VirtualKey virtualKey, - string model, - string? provider, - CancellationToken cancellationToken = default) - { - _logger.LogInformation("Starting proxy for connection {ConnectionId}, model {Model}, provider {Provider}", - connectionId, - model.Replace(Environment.NewLine, ""), - provider?.Replace(Environment.NewLine, "") ?? "default"); - - // Initialize connection metrics - lock (_metricsLock) - { - _connectionMetrics[connectionId] = new ConnectionMetrics(); - } - - // Validate virtual key is enabled - if (!virtualKey.IsEnabled) - { - throw new UnauthorizedAccessException("Virtual key is not active"); - } - - // Note: Budget validation happens at the service layer during key validation - - // Get the provider client and establish connection - var audioRouter = _connectionManager as IAudioRouter ?? - throw new InvalidOperationException("Connection manager must implement IAudioRouter"); - - // Create session configuration - var sessionConfig = new RealtimeSessionConfig - { - Model = model, - Voice = "alloy", // Default voice, could be made configurable - SystemPrompt = "You are a helpful assistant." - }; - - var realtimeClient = await audioRouter.GetRealtimeClientAsync(sessionConfig, virtualKey.KeyHash); - if (realtimeClient == null) - { - throw new InvalidOperationException($"No real-time audio provider available for model {model}"); - } - - // Connect to provider - RealtimeSession? providerSession = null; - - using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken); - - try - { - // Start usage tracking - await _usageTracker.StartTrackingAsync(connectionId, virtualKey.Id, model, provider ?? "default"); - - // Create the session - providerSession = await realtimeClient.CreateSessionAsync(sessionConfig, virtualKey.KeyHash, cancellationToken); - - // Get the duplex stream from the provider - var providerStream = realtimeClient.StreamAudioAsync(providerSession, cts.Token); - - // Start proxying in both directions - var clientToProvider = ProxyClientToProviderAsync( - clientWebSocket, providerStream, connectionId, virtualKey.KeyHash, cts.Token); - var providerToClient = ProxyProviderToClientAsync( - providerStream, clientWebSocket, connectionId, virtualKey.KeyHash, cts.Token); - - await Task.WhenAny(clientToProvider, providerToClient); - - // If one direction fails, cancel the other - cts.Cancel(); - - await Task.WhenAll(clientToProvider, providerToClient); - } - catch (OperationCanceledException) - { - _logger.LogInformation("Proxy connection {ConnectionId} cancelled", - connectionId); - throw; - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error in proxy for connection {ConnectionId}", - connectionId); - throw; - } - finally - { - try - { - // Finalize usage tracking and update virtual key spend - var connectionInfo = await _connectionManager.GetConnectionAsync(connectionId); - var finalStats = connectionInfo?.Usage ?? new ConnectionUsageStats(); - var totalCost = await _usageTracker.FinalizeUsageAsync(connectionId, finalStats); - - _logger.LogInformation("Session {ConnectionId} completed with total cost: ${Cost:F4}", - connectionId, - totalCost); - } - catch (Exception ex) - { - _logger.LogError(ex, - "Error finalizing usage for connection {ConnectionId}", - connectionId); - } - - // Ensure client WebSocket is closed - await CloseWebSocketAsync(clientWebSocket, "Proxy connection ended"); - - // Close provider session - if (providerSession != null) - { - await realtimeClient.CloseSessionAsync(providerSession, cancellationToken); - } - } - } - - - - - - - - - } -} diff --git a/ConduitLLM.Http/Services/RealtimeUsageTracker.cs b/ConduitLLM.Http/Services/RealtimeUsageTracker.cs deleted file mode 100644 index b3520f3ba..000000000 --- a/ConduitLLM.Http/Services/RealtimeUsageTracker.cs +++ /dev/null @@ -1,307 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Extensions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Realtime; -namespace ConduitLLM.Http.Services -{ - /// - /// Tracks usage and costs for real-time audio sessions. - /// - public class RealtimeUsageTracker : IRealtimeUsageTracker - { - private readonly ILogger _logger; - private readonly Configuration.Interfaces.IModelCostService _costService; - private readonly Configuration.Interfaces.IVirtualKeyService _virtualKeyService; - private readonly ConcurrentDictionary _sessions = new(); - - public RealtimeUsageTracker( - ILogger logger, - Configuration.Interfaces.IModelCostService costService, - Configuration.Interfaces.IVirtualKeyService virtualKeyService) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _costService = costService ?? throw new ArgumentNullException(nameof(costService)); - _virtualKeyService = virtualKeyService ?? throw new ArgumentNullException(nameof(virtualKeyService)); - } - - public async Task StartTrackingAsync(string connectionId, int virtualKeyId, string model, string provider) - { - // Get virtual key details - var virtualKey = await _virtualKeyService.GetVirtualKeyByIdAsync(virtualKeyId); - if (virtualKey == null) - { - throw new ArgumentException($"Virtual key with ID {virtualKeyId} not found"); - } - - var session = new SessionUsage - { - ConnectionId = connectionId, - VirtualKeyId = virtualKeyId, - VirtualKey = virtualKey.KeyHash, - Model = model, - Provider = provider, - StartTime = DateTime.UtcNow, - LastActivity = DateTime.UtcNow, - InputAudioSeconds = 0, - OutputAudioSeconds = 0, - InputTokens = 0, - OutputTokens = 0, - FunctionCalls = 0, - EstimatedCost = 0 - }; - - _sessions[connectionId] = session; - - _logger.LogInformation("Started usage tracking for connection {ConnectionId}, model {Model}, virtualKeyId {VirtualKeyId}", - connectionId, - model.Replace(Environment.NewLine, ""), - virtualKeyId); - - await Task.CompletedTask; - } - - public async Task UpdateUsageAsync(string connectionId, ConnectionUsageStats stats) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - _logger.LogWarning("Attempted to update usage for unknown connection {ConnectionId}", - connectionId); - return; - } - - // Update from stats - session.LastActivity = DateTime.UtcNow; - session.EstimatedCost = stats.EstimatedCost; - - _logger.LogDebug("Updated usage stats for connection {ConnectionId}", - connectionId); - - await Task.CompletedTask; - } - - public async Task RecordAudioUsageAsync(string connectionId, double audioSeconds, bool isInput) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - _logger.LogWarning("Attempted to track audio for unknown connection {ConnectionId}", - connectionId); - return; - } - - if (isInput) - { - session.InputAudioSeconds += audioSeconds; - } - else - { - session.OutputAudioSeconds += audioSeconds; - } - - session.LastActivity = DateTime.UtcNow; - - _logger.LogDebug("Tracked {Seconds}s of {Type} audio for connection {ConnectionId}", - audioSeconds, - isInput ? "input" : "output".Replace(Environment.NewLine, ""), - connectionId); - - await Task.CompletedTask; - } - - public async Task RecordTokenUsageAsync(string connectionId, Usage usage) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - _logger.LogWarning("Attempted to track tokens for unknown connection {ConnectionId}", - connectionId); - return; - } - - session.InputTokens += usage.PromptTokens.GetValueOrDefault(); - session.OutputTokens += usage.CompletionTokens.GetValueOrDefault(); - session.LastActivity = DateTime.UtcNow; - - _logger.LogDebug("Tracked {InputTokens} input tokens and {OutputTokens} output tokens for connection {ConnectionId}", - usage.PromptTokens, - usage.CompletionTokens, - connectionId); - - await Task.CompletedTask; - } - - /// - /// Records a function call for billing purposes. - /// - /// The connection identifier. - /// Optional function name for logging. - /// A task that completes when the function call is recorded. - public async Task RecordFunctionCallAsync(string connectionId, string? functionName = null) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - _logger.LogWarning("Attempted to track function call for unknown connection {ConnectionId}", - connectionId); - return; - } - - session.FunctionCalls++; - session.LastActivity = DateTime.UtcNow; - - _logger.LogDebugSecure( - "Tracked function call {FunctionName} for connection {ConnectionId}. Total calls: {TotalCalls}", - functionName ?? "(unnamed)", connectionId, session.FunctionCalls); - - await Task.CompletedTask; - } - - public async Task GetEstimatedCostAsync(string connectionId) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - return 0; - } - - // Get cost configuration for the model - var modelCost = await _costService.GetCostForModelAsync(session.Model); - if (modelCost == null) - { - _logger.LogWarning("No cost configuration found for model {Model}", - session.Model.Replace(Environment.NewLine, "")); - return 0; - } - - decimal totalCost = 0; - - // Calculate token costs (costs are now per million tokens) - if (session.InputTokens > 0) - { - totalCost += (session.InputTokens * modelCost.InputCostPerMillionTokens) / 1_000_000m; - } - - if (session.OutputTokens > 0) - { - totalCost += (session.OutputTokens * modelCost.OutputCostPerMillionTokens) / 1_000_000m; - } - - // For audio, we'll use a simple approximation - // Assuming 1 minute of audio ≈ 1000 tokens for cost estimation - if (session.InputAudioSeconds > 0) - { - var inputMinutes = (decimal)(session.InputAudioSeconds / 60.0); - var estimatedTokens = inputMinutes * 1000; // 1 minute ≈ 1K tokens - totalCost += (estimatedTokens * modelCost.InputCostPerMillionTokens) / 1_000_000m; - } - - if (session.OutputAudioSeconds > 0) - { - var outputMinutes = (decimal)(session.OutputAudioSeconds / 60.0); - var estimatedTokens = outputMinutes * 1000; // 1 minute ≈ 1K tokens - totalCost += (estimatedTokens * modelCost.OutputCostPerMillionTokens) / 1_000_000m; - } - - // Add function call costs - // Assuming each function call costs approximately 100 tokens worth - if (session.FunctionCalls > 0) - { - var estimatedTokens = session.FunctionCalls * 100m; // Each call ≈ 100 tokens - totalCost += (estimatedTokens * modelCost.OutputCostPerMillionTokens) / 1_000_000m; - } - - return totalCost; - } - - public async Task FinalizeUsageAsync(string connectionId, ConnectionUsageStats finalStats) - { - if (!_sessions.TryRemove(connectionId, out var session)) - { - throw new InvalidOperationException($"Connection {connectionId} not found in usage tracking"); - } - - // Update with final stats if provided - if (finalStats != null) - { - session.EstimatedCost = finalStats.EstimatedCost; - session.LastActivity = DateTime.UtcNow; - } - - var totalCost = await GetEstimatedCostAsync(connectionId); - - // Record usage with virtual key service by updating spend - if (totalCost > 0) - { - await _virtualKeyService.UpdateSpendAsync(session.VirtualKeyId, totalCost); - } - - var duration = DateTime.UtcNow - session.StartTime; - _logger.LogInformation("Finalized session {ConnectionId}: Duration={Duration}s, Cost=${Cost:F4}", - connectionId, - duration.TotalSeconds, - totalCost); - - return totalCost; - } - - public async Task GetUsageDetailsAsync(string connectionId) - { - if (!_sessions.TryGetValue(connectionId, out var session)) - { - return null; - } - - var duration = DateTime.UtcNow - session.StartTime; - var totalCost = await GetEstimatedCostAsync(connectionId); - - // Get cost configuration for breakdown - var modelCost = await _costService.GetCostForModelAsync(session.Model); - - var details = new RealtimeUsageDetails - { - ConnectionId = connectionId, - InputAudioSeconds = session.InputAudioSeconds, - OutputAudioSeconds = session.OutputAudioSeconds, - InputTokens = (int)session.InputTokens, - OutputTokens = (int)session.OutputTokens, - FunctionCalls = session.FunctionCalls, - SessionDurationSeconds = duration.TotalSeconds, - StartedAt = session.StartTime, - EndedAt = null // Still active - }; - - if (modelCost != null) - { - // Calculate cost breakdown - details.Costs = new CostBreakdown - { - InputAudioCost = (decimal)(session.InputAudioSeconds / 60.0 * 1000) * modelCost.InputCostPerMillionTokens / 1_000_000m, - OutputAudioCost = (decimal)(session.OutputAudioSeconds / 60.0 * 1000) * modelCost.OutputCostPerMillionTokens / 1_000_000m, - TokenCost = (session.InputTokens * modelCost.InputCostPerMillionTokens / 1_000_000m) + - (session.OutputTokens * modelCost.OutputCostPerMillionTokens / 1_000_000m), - FunctionCallCost = session.FunctionCalls > 0 - ? (session.FunctionCalls * 100m * modelCost.OutputCostPerMillionTokens) / 1_000_000m - : 0, - AdditionalFees = 0 - }; - } - - return details; - } - - private class SessionUsage - { - public string ConnectionId { get; set; } = string.Empty; - public int VirtualKeyId { get; set; } - public string VirtualKey { get; set; } = string.Empty; - public string Model { get; set; } = string.Empty; - public string Provider { get; set; } = string.Empty; - public DateTime StartTime { get; set; } - public DateTime LastActivity { get; set; } - public double InputAudioSeconds { get; set; } - public double OutputAudioSeconds { get; set; } - public long InputTokens { get; set; } - public long OutputTokens { get; set; } - public int FunctionCalls { get; set; } - public decimal EstimatedCost { get; set; } - } - } -} diff --git a/ConduitLLM.Http/Services/RedisModelCostCache.Helpers.cs b/ConduitLLM.Http/Services/RedisModelCostCache.Helpers.cs index b171e1d92..61bf613a1 100644 --- a/ConduitLLM.Http/Services/RedisModelCostCache.Helpers.cs +++ b/ConduitLLM.Http/Services/RedisModelCostCache.Helpers.cs @@ -148,10 +148,6 @@ private CachedModelCost ConvertToCachedModelCost(ModelCost cost) CostPerSearchUnit = cost.CostPerSearchUnit, CostPerInferenceStep = cost.CostPerInferenceStep, DefaultInferenceSteps = cost.DefaultInferenceSteps, - AudioCostPerMinute = cost.AudioCostPerMinute, - AudioCostPerKCharacters = cost.AudioCostPerKCharacters, - AudioInputCostPerMinute = cost.AudioInputCostPerMinute, - AudioOutputCostPerMinute = cost.AudioOutputCostPerMinute, ModelType = cost.ModelType, IsActive = cost.IsActive, Priority = cost.Priority, diff --git a/ConduitLLM.Http/Startup.Production.cs b/ConduitLLM.Http/Startup.Production.cs index d06192c45..07c9200ec 100644 --- a/ConduitLLM.Http/Startup.Production.cs +++ b/ConduitLLM.Http/Startup.Production.cs @@ -75,8 +75,7 @@ public void ConfigureServices(IServiceCollection services) options.EnableForHttps = true; }); - // Add production audio services - services.AddProductionAudioServices(Configuration); + // Audio services removed - system no longer supports audio functionality // Health checks are registered in Program.cs via AddConduitHealthChecks // Audio-specific health checks should be added there as well to avoid duplicate registrations @@ -141,9 +140,6 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IHostApp app.UseHttpsRedirection(); app.UseResponseCompression(); - // Use production audio services middleware - app.UseProductionAudioServices(); - app.UseRouting(); app.UseCors("ProductionCors"); app.UseAuthentication(); diff --git a/ConduitLLM.Providers/AWSTranscribeClient.cs b/ConduitLLM.Providers/AWSTranscribeClient.cs deleted file mode 100644 index 998691a97..000000000 --- a/ConduitLLM.Providers/AWSTranscribeClient.cs +++ /dev/null @@ -1,380 +0,0 @@ -using System.Net.Http.Headers; - -using Amazon; -using Amazon.Polly; -using Amazon.Polly.Model; -using Amazon.TranscribeService; - -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Providers.Common.Models; -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers -{ - /// - /// Client for interacting with AWS Transcribe and Polly services. - /// - public class AWSTranscribeClient : BaseLLMClient, IAudioTranscriptionClient, ITextToSpeechClient - { - private readonly string _region; - private readonly AmazonTranscribeServiceClient _transcribeClient; - private readonly AmazonPollyClient _pollyClient; - - /// - /// Initializes a new instance of the class. - /// - /// The provider credentials. - /// The provider's model identifier. - /// The logger to use. - /// Optional HTTP client factory. - /// Optional default model configuration for the provider. - public AWSTranscribeClient( - Provider provider, - ProviderKeyCredential keyCredential, - string providerModelId, - ILogger logger, - IHttpClientFactory? httpClientFactory = null, - ProviderDefaultModels? defaultModels = null) - : base( - provider, - keyCredential, - providerModelId, - logger, - httpClientFactory, - "aws", - defaultModels) - { - // Extract region from provider.BaseUrl or use default - _region = string.IsNullOrWhiteSpace(provider.BaseUrl) ? "us-east-1" : provider.BaseUrl; - - // Initialize AWS clients - // For now, we'll use environment variables for AWS credentials - // In production, use IAM roles or proper credential management - var secretKey = Environment.GetEnvironmentVariable("AWS_SECRET_ACCESS_KEY") ?? "dummy-secret-key"; - var awsCredentials = new Amazon.Runtime.BasicAWSCredentials( - keyCredential.ApiKey!, - secretKey); - - var regionEndpoint = RegionEndpoint.GetBySystemName(_region); - - _transcribeClient = new AmazonTranscribeServiceClient(awsCredentials, regionEndpoint); - _pollyClient = new AmazonPollyClient(awsCredentials, regionEndpoint); - } - - /// - protected override void ConfigureHttpClient(HttpClient client, string apiKey) - { - // Not used for AWS SDK clients - client.DefaultRequestHeaders.Accept.Clear(); - client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - client.DefaultRequestHeaders.Add("User-Agent", "ConduitLLM"); - } - - /// - public async Task TranscribeAudioAsync( - AudioTranscriptionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "AudioTranscription"); - - return await ExecuteApiRequestAsync(async () => - { - // AWS Transcribe requires audio to be in S3, so we'll use synchronous transcription - // For production, you'd upload to S3 and use async transcription - - // For now, we'll simulate with a simple approach - // Note: Real implementation would require S3 upload or streaming transcription - - if (request.AudioData == null) - { - throw new ValidationException("Audio data is required for transcription"); - } - - // Create a unique job name - var jobName = $"conduit-transcribe-{Guid.NewGuid():N}"; - - // In a real implementation, we would: - // 1. Upload audio to S3 - // 2. Start transcription job - // 3. Wait for completion - // 4. Retrieve results - - // For this example, we'll return a simulated response - Logger.LogWarning("AWS Transcribe implementation is simplified. Production use requires S3 integration."); - - // Simulate transcription - await Task.Delay(100, cancellationToken); - - return new AudioTranscriptionResponse - { - Text = "This is a simulated transcription. Real AWS Transcribe integration requires S3 bucket setup.", - Language = request.Language ?? "en-US", - Duration = CalculateDuration(request.AudioData), - Segments = new List - { - new TranscriptionSegment - { - Text = "This is a simulated transcription.", - Start = 0.0, - End = 2.0, - Confidence = 0.95f - } - } - }; - }, "AudioTranscription", cancellationToken); - } - - /// - public async Task SupportsTranscriptionAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - return true; - } - - /// - public async Task> GetSupportedFormatsAsync( - CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - return new List - { - "wav", - "mp3", - "mp4", - "flac", - "ogg", - "amr", - "webm" - }; - } - - /// - public async Task> GetSupportedLanguagesAsync( - CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - return new List - { - "en-US", "en-GB", "en-AU", "en-IN", "es-US", "es-ES", - "fr-FR", "fr-CA", "de-DE", "it-IT", "pt-BR", "pt-PT", - "ja-JP", "ko-KR", "zh-CN", "ar-SA", "hi-IN", "ru-RU" - }; - } - - /// - public async Task CreateSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "TextToSpeech"); - - return await ExecuteApiRequestAsync(async () => - { - var synthesizeRequest = new SynthesizeSpeechRequest - { - Text = request.Input, - OutputFormat = MapOutputFormat(request.ResponseFormat), - VoiceId = MapVoiceId(request.Voice), - LanguageCode = request.Language ?? "en-US", - Engine = request.Model?.Contains("neural") == true ? Engine.Neural : Engine.Standard, - SampleRate = "22050" - }; - - var response = await _pollyClient.SynthesizeSpeechAsync(synthesizeRequest, cancellationToken); - - if (response.AudioStream == null) - { - throw new LLMCommunicationException("Failed to synthesize speech from AWS Polly"); - } - - // Read audio stream - using var memoryStream = new MemoryStream(); - await response.AudioStream.CopyToAsync(memoryStream, cancellationToken); - var audioData = memoryStream.ToArray(); - - return new TextToSpeechResponse - { - AudioData = audioData, - Format = (request.ResponseFormat ?? AudioFormat.Mp3).ToString().ToLower(), - Duration = EstimateDuration(audioData, request.ResponseFormat ?? AudioFormat.Mp3), - ModelUsed = request.Model ?? "standard", - VoiceUsed = request.Voice ?? "Joanna" - }; - }, "TextToSpeech", cancellationToken); - } - - /// - public async Task SupportsTextToSpeechAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - return true; - } - - /// - public IAsyncEnumerable StreamSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("AWS Polly streaming is not implemented in this client"); - } - - // GetSupportedFormatsAsync is implemented in IAudioTranscriptionClient section - - /// - public async Task> ListVoicesAsync( - string? language = null, - CancellationToken cancellationToken = default) - { - return await ExecuteApiRequestAsync(async () => - { - var describeVoicesRequest = new DescribeVoicesRequest(); - - if (!string.IsNullOrWhiteSpace(language)) - { - describeVoicesRequest.LanguageCode = language; - } - - var response = await _pollyClient.DescribeVoicesAsync(describeVoicesRequest, cancellationToken); - - return response.Voices.Select(v => new VoiceInfo - { - VoiceId = v.Id.Value, - Name = v.Name, - SupportedLanguages = new List { v.LanguageCode.Value }, - Gender = MapGender(v.Gender.Value), - SupportedStyles = v.SupportedEngines?.Select(e => e).ToList() ?? new List() - }).ToList(); - }, "GetAvailableVoices", cancellationToken); - } - - /// - public override Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("AWS Transcribe client does not support chat completions"); - } - - /// - public override IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("AWS Transcribe client does not support streaming chat completions"); - } - - /// - public override Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("AWS Transcribe client does not support embeddings"); - } - - /// - public override Task CreateImageAsync( - ImageGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("AWS Transcribe client does not support image generation"); - } - - /// - public override async Task> GetModelsAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - await Task.CompletedTask; - return new List - { - ExtendedModelInfo.Create("standard", ProviderName, "standard"), - ExtendedModelInfo.Create("neural", ProviderName, "neural") - }; - } - - #region Helper Methods - - private VoiceGender? MapGender(string gender) - { - return gender?.ToLowerInvariant() switch - { - "male" => VoiceGender.Male, - "female" => VoiceGender.Female, - "neutral" => VoiceGender.Neutral, - _ => null - }; - } - - private OutputFormat MapOutputFormat(AudioFormat? format) - { - return format switch - { - AudioFormat.Mp3 => OutputFormat.Mp3, - AudioFormat.Ogg => OutputFormat.Mp3, // AWS Polly doesn't have Ogg, use Mp3 - AudioFormat.Pcm => OutputFormat.Pcm, - _ => OutputFormat.Mp3 - }; - } - - private VoiceId MapVoiceId(string? voice) - { - if (string.IsNullOrWhiteSpace(voice)) - return VoiceId.Joanna; - - // Try to find matching voice - return VoiceId.FindValue(voice) ?? VoiceId.Joanna; - } - - private double CalculateDuration(byte[]? audioData) - { - // This is a rough estimate - actual duration would depend on format and bitrate - if (audioData == null || audioData.Length == 0) - return 0.0; - - // Assume ~16kbps for speech audio - return audioData.Length / 2000.0; // Very rough estimate - } - - private double EstimateDuration(byte[] audioData, AudioFormat format) - { - // Rough estimation based on typical bitrates - var bitrate = format switch - { - AudioFormat.Mp3 => 128000, // 128 kbps - AudioFormat.Pcm => 256000, // 256 kbps - AudioFormat.Ogg => 96000, // 96 kbps - _ => 128000 - }; - - return (audioData.Length * 8.0) / bitrate; - } - - #endregion - - /// - /// Disposes the AWS clients. - /// - public void DisposeClients() - { - _transcribeClient?.Dispose(); - _pollyClient?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Common/Models/ModelCapabilities.cs b/ConduitLLM.Providers/Common/Models/ModelCapabilities.cs index 63ec9c211..c47a87f8f 100644 --- a/ConduitLLM.Providers/Common/Models/ModelCapabilities.cs +++ b/ConduitLLM.Providers/Common/Models/ModelCapabilities.cs @@ -47,25 +47,7 @@ public class ModelCapabilities /// public bool JsonMode { get; set; } - /// - /// Gets or sets a value indicating whether the model supports audio transcription. - /// - public bool AudioTranscription { get; set; } - - /// - /// Gets or sets a value indicating whether the model supports text-to-speech. - /// - public bool TextToSpeech { get; set; } - /// - /// Gets or sets a value indicating whether the model supports real-time audio. - /// - public bool RealtimeAudio { get; set; } - - /// - /// Gets or sets the list of supported audio operations. - /// - public List? SupportedAudioOperations { get; set; } /// /// Gets or sets a value indicating whether the model supports video generation. @@ -88,10 +70,6 @@ public class ModelCapabilities [nameof(FunctionCalling)] = FunctionCalling, [nameof(ToolUsage)] = ToolUsage, [nameof(JsonMode)] = JsonMode, - [nameof(AudioTranscription)] = AudioTranscription, - [nameof(TextToSpeech)] = TextToSpeech, - [nameof(RealtimeAudio)] = RealtimeAudio, - [nameof(SupportedAudioOperations)] = SupportedAudioOperations, [nameof(VideoGeneration)] = VideoGeneration }; } diff --git a/ConduitLLM.Providers/Common/Models/ProviderRealtimeMessage.cs b/ConduitLLM.Providers/Common/Models/ProviderRealtimeMessage.cs deleted file mode 100644 index 0b1d30f32..000000000 --- a/ConduitLLM.Providers/Common/Models/ProviderRealtimeMessage.cs +++ /dev/null @@ -1,33 +0,0 @@ -namespace ConduitLLM.Providers.Common.Models -{ - /// - /// Internal message class used by providers for real-time communication. - /// - public class ProviderRealtimeMessage - { - /// - /// The type of message. - /// - public string? Type { get; set; } - - /// - /// Message data payload. - /// - public Dictionary? Data { get; set; } - - /// - /// Session identifier. - /// - public string? SessionId { get; set; } - - /// - /// Timestamp when the message was created. - /// - public DateTime Timestamp { get; set; } = DateTime.UtcNow; - - /// - /// Sequence number for ordering. - /// - public long? SequenceNumber { get; set; } - } -} diff --git a/ConduitLLM.Providers/DatabaseAwareLLMClientFactory.cs b/ConduitLLM.Providers/DatabaseAwareLLMClientFactory.cs index e64650860..0690f7f65 100644 --- a/ConduitLLM.Providers/DatabaseAwareLLMClientFactory.cs +++ b/ConduitLLM.Providers/DatabaseAwareLLMClientFactory.cs @@ -10,8 +10,6 @@ using ConduitLLM.Providers.Replicate; using ConduitLLM.Providers.Fireworks; using ConduitLLM.Providers.MiniMax; -using ConduitLLM.Providers.Ultravox; -using ConduitLLM.Providers.ElevenLabs; using ConduitLLM.Providers.Cerebras; using ConduitLLM.Providers.SambaNova; using ConduitLLM.Providers.DeepInfra; @@ -285,17 +283,6 @@ private ILLMClient CreateClientForProvider(Provider provider, ProviderKeyCredent _httpClientFactory, defaultModels); break; - case ProviderType.Ultravox: - var ultravoxLogger = _loggerFactory.CreateLogger(); - client = new UltravoxClient(provider, keyCredential, modelId, ultravoxLogger, - _httpClientFactory, defaultModels); - break; - - case ProviderType.ElevenLabs: - var elevenLabsLogger = _loggerFactory.CreateLogger(); - client = new ElevenLabsClient(provider, keyCredential, modelId, elevenLabsLogger, - _httpClientFactory, defaultModels); - break; case ProviderType.Cerebras: var cerebrasLogger = _loggerFactory.CreateLogger(); diff --git a/ConduitLLM.Providers/OpenAIRealtimeSession.cs b/ConduitLLM.Providers/OpenAIRealtimeSession.cs deleted file mode 100644 index d6e085844..000000000 --- a/ConduitLLM.Providers/OpenAIRealtimeSession.cs +++ /dev/null @@ -1,249 +0,0 @@ -using System.Net.WebSockets; -using System.Runtime.CompilerServices; -using System.Text; - -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Translators; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers -{ - /// - /// OpenAI-specific implementation of a real-time audio session. - /// - public class OpenAIRealtimeSession : RealtimeSession - { - private readonly string _url; - private readonly string _apiKey; - private readonly RealtimeSessionConfig _config; - private readonly ILogger _logger; - private readonly ClientWebSocket _webSocket; - private readonly OpenAIRealtimeTranslatorV2 _translator; - private readonly CancellationTokenSource _cancellationTokenSource; - private Task? _receiveTask; - - public OpenAIRealtimeSession( - string url, - string apiKey, - RealtimeSessionConfig config, - ILogger logger) - { - _url = url ?? throw new ArgumentNullException(nameof(url)); - _apiKey = apiKey ?? throw new ArgumentNullException(nameof(apiKey)); - _config = config ?? throw new ArgumentNullException(nameof(config)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _webSocket = new ClientWebSocket(); - _translator = new OpenAIRealtimeTranslatorV2(logger as ILogger ?? - throw new ArgumentException("Logger must be ILogger")); - _cancellationTokenSource = new CancellationTokenSource(); - - // Set base class properties - Provider = "OpenAI"; - Config = config; - } - - public async Task ConnectAsync(CancellationToken cancellationToken = default) - { - // Add required headers - _webSocket.Options.SetRequestHeader("Authorization", $"Bearer {_apiKey}"); - _webSocket.Options.SetRequestHeader("OpenAI-Beta", "realtime=v1"); - - // Set subprotocol if required - var subprotocol = _translator.GetRequiredSubprotocol(); - if (!string.IsNullOrEmpty(subprotocol)) - { - _webSocket.Options.AddSubProtocol(subprotocol); - } - - // Add any custom headers - var headers = await _translator.GetConnectionHeadersAsync(_config); - foreach (var header in headers) - { - _webSocket.Options.SetRequestHeader(header.Key, header.Value); - } - - try - { - State = SessionState.Connecting; - await _webSocket.ConnectAsync(new Uri(_url), cancellationToken); - _logger.LogInformation("Connected to OpenAI Realtime API at {Url}", _url); - - State = SessionState.Connected; - - // Send initialization messages - var initMessages = await _translator.GetInitializationMessagesAsync(_config); - foreach (var message in initMessages) - { - await SendRawMessageAsync(message, cancellationToken); - } - - // Start receive loop - _receiveTask = ReceiveLoopAsync(_cancellationTokenSource.Token); - - State = SessionState.Active; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to connect to OpenAI Realtime API"); - State = SessionState.Error; - throw; - } - } - - public async Task SendMessageAsync(RealtimeMessage message, CancellationToken cancellationToken = default) - { - if (_webSocket.State != WebSocketState.Open) - { - throw new InvalidOperationException("WebSocket is not open"); - } - - var jsonMessage = await _translator.TranslateToProviderAsync(message); - await SendRawMessageAsync(jsonMessage, cancellationToken); - } - - - public async IAsyncEnumerable ReceiveMessagesAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var bufferArray = new byte[4096]; - var buffer = new ArraySegment(bufferArray); - - while (!cancellationToken.IsCancellationRequested && _webSocket.State == WebSocketState.Open) - { - var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); - - if (result.MessageType == WebSocketMessageType.Close) - { - await _webSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Server closed connection", - cancellationToken); - break; - } - - if (result.MessageType == WebSocketMessageType.Text) - { - var message = Encoding.UTF8.GetString(bufferArray, 0, result.Count); - _logger.LogDebug("Received message from OpenAI: {Message}", message); - - var conduitMessages = await _translator.TranslateFromProviderAsync(message); - foreach (var conduitMessage in conduitMessages) - { - yield return conduitMessage; - } - } - } - } - - protected override void Dispose(bool disposing) - { - if (disposing) - { - _cancellationTokenSource.Cancel(); - - if (_receiveTask != null) - { - try - { - _receiveTask.Wait(TimeSpan.FromSeconds(5)); - } - catch (Exception ex) - { - // Log but don't throw - we're in Dispose - _logger?.LogDebug(ex, "Exception while waiting for receive task to complete during disposal"); - } - } - - if (_webSocket.State == WebSocketState.Open) - { - try - { - _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Disposing", CancellationToken.None) - .Wait(TimeSpan.FromSeconds(5)); - } - catch (Exception ex) - { - // Log but don't throw - we're in Dispose - _logger?.LogDebug(ex, "Exception while closing WebSocket during disposal"); - } - } - - _webSocket.Dispose(); - _cancellationTokenSource.Dispose(); - } - - base.Dispose(disposing); - } - - public async Task SendRawMessageAsync(string message, CancellationToken cancellationToken) - { - var bytes = Encoding.UTF8.GetBytes(message); - await _webSocket.SendAsync( - new ArraySegment(bytes), - WebSocketMessageType.Text, - true, - cancellationToken); - - _logger.LogDebug("Sent message to OpenAI: {Message}", message); - } - - private async Task ReceiveLoopAsync(CancellationToken cancellationToken) - { - var bufferArray = new byte[4096]; - var buffer = new ArraySegment(bufferArray); - - try - { - while (!cancellationToken.IsCancellationRequested && _webSocket.State == WebSocketState.Open) - { - var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); - - if (result.MessageType == WebSocketMessageType.Close) - { - await _webSocket.CloseAsync( - WebSocketCloseStatus.NormalClosure, - "Server closed connection", - cancellationToken); - break; - } - - if (result.MessageType == WebSocketMessageType.Text) - { - var message = Encoding.UTF8.GetString(bufferArray, 0, result.Count); - _logger.LogDebug("Received message from OpenAI: {Message}", message); - - try - { - var conduitMessages = await _translator.TranslateFromProviderAsync(message); - foreach (var conduitMessage in conduitMessages) - { - // Messages will be processed by the caller - _logger.LogDebug("Translated message type: {Type}", conduitMessage.Type); - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error translating provider message"); - // Error handling will be done by the caller - } - } - } - } - catch (WebSocketException ex) - { - _logger.LogError(ex, "WebSocket error in receive loop"); - State = SessionState.Error; - } - catch (Exception ex) - { - _logger.LogError(ex, "Unexpected error in receive loop"); - State = SessionState.Error; - } - finally - { - State = SessionState.Closed; - _logger.LogInformation("Connection closed"); - } - } - } -} diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsClient.cs b/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsClient.cs deleted file mode 100644 index 9c98f2f33..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsClient.cs +++ /dev/null @@ -1,437 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Common.Models; -using ConduitLLM.Providers.Translators; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// Client implementation for ElevenLabs voice AI services. - /// - /// - /// ElevenLabs provides high-quality text-to-speech and conversational AI - /// with support for voice cloning and real-time voice synthesis. - /// - public class ElevenLabsClient : BaseLLMClient, ILLMClient, ITextToSpeechClient, IRealtimeAudioClient - { - private const string DEFAULT_BASE_URL = "https://api.elevenlabs.io/v1"; - - private readonly ElevenLabsTextToSpeechService _textToSpeechService; - private readonly ElevenLabsRealtimeService _realtimeService; - private readonly ElevenLabsVoiceService _voiceService; - - /// - /// Initializes a new instance of the class. - /// - public ElevenLabsClient( - Provider provider, - ProviderKeyCredential keyCredential, - string providerModelId, - ILogger logger, - IHttpClientFactory? httpClientFactory = null, - ProviderDefaultModels? defaultModels = null) - : base(provider, keyCredential, providerModelId, logger, httpClientFactory, "ElevenLabs", defaultModels) - { - var translatorLogger = logger as ILogger - ?? Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance.CreateLogger(); - var translator = new ElevenLabsRealtimeTranslator(translatorLogger); - - _textToSpeechService = new ElevenLabsTextToSpeechService(logger, DefaultJsonOptions); - _realtimeService = new ElevenLabsRealtimeService(translator, logger); - _voiceService = new ElevenLabsVoiceService(logger, DefaultJsonOptions); - } - - /// - /// Sends a chat completion request to ElevenLabs. - /// - public override Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // ElevenLabs is primarily a voice AI provider - return Task.FromException( - new NotSupportedException("ElevenLabs does not support text-based chat completion. Use text-to-speech or real-time audio instead.")); - } - - /// - /// Streams chat completion responses from ElevenLabs. - /// - public override IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("ElevenLabs does not support streaming text chat. Use text-to-speech or real-time audio instead."); - } - - /// - /// Creates speech audio from text using ElevenLabs. - /// - public async Task CreateSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "CreateSpeech"); - - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey; - if (string.IsNullOrEmpty(effectiveApiKey)) - { - throw new InvalidOperationException("API key is required for ElevenLabs"); - } - - using var httpClient = CreateHttpClient(effectiveApiKey); - var model = request.Model ?? GetDefaultTextToSpeechModel(); - - return await _textToSpeechService.CreateSpeechAsync( - httpClient, - Provider.BaseUrl, - request, - model, - cancellationToken); - } - - /// - /// Streams speech audio from text using ElevenLabs. - /// - public async IAsyncEnumerable StreamSpeechAsync( - TextToSpeechRequest request, - string? apiKey = null, - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - ValidateRequest(request, "StreamSpeech"); - - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey; - if (string.IsNullOrEmpty(effectiveApiKey)) - { - throw new InvalidOperationException("API key is required for ElevenLabs"); - } - - using var httpClient = CreateHttpClient(effectiveApiKey); - var model = request.Model ?? GetDefaultTextToSpeechModel(); - - await foreach (var chunk in _textToSpeechService.StreamSpeechAsync( - httpClient, - Provider.BaseUrl, - request, - model, - cancellationToken)) - { - yield return chunk; - } - } - - /// - /// Lists available voices from ElevenLabs. - /// - public async Task> ListVoicesAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey; - if (string.IsNullOrEmpty(effectiveApiKey)) - { - throw new InvalidOperationException("API key is required for ElevenLabs"); - } - - using var httpClient = CreateHttpClient(effectiveApiKey); - - return await _voiceService.ListVoicesAsync( - httpClient, - Provider.BaseUrl, - cancellationToken); - } - - /// - /// Gets the audio formats supported by ElevenLabs. - /// - public async Task> GetSupportedFormatsAsync( - CancellationToken cancellationToken = default) - { - return await _textToSpeechService.GetSupportedFormatsAsync(cancellationToken); - } - - /// - /// Checks if the client supports text-to-speech synthesis. - /// - public async Task SupportsTextToSpeechAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return await _textToSpeechService.SupportsTextToSpeechAsync(cancellationToken); - } - - /// - /// Updates the configuration of an active real-time session. - /// - public async Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - await _realtimeService.UpdateSessionAsync(session, updates, cancellationToken); - } - - /// - /// Closes an active real-time session. - /// - public async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - await _realtimeService.CloseSessionAsync(session, cancellationToken); - } - - /// - /// Checks if the client supports real-time audio conversations. - /// - public async Task SupportsRealtimeAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return await _realtimeService.SupportsRealtimeAsync(cancellationToken); - } - - /// - /// Gets the capabilities of the ElevenLabs real-time audio system. - /// - public Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) - { - return _realtimeService.GetCapabilitiesAsync(cancellationToken); - } - - /// - /// Creates a new real-time session with ElevenLabs Conversational AI. - /// - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey; - if (string.IsNullOrWhiteSpace(effectiveApiKey)) - { - throw new InvalidOperationException("API key is required for creating a realtime session. Either provide an API key or ensure the client has a valid primary key credential."); - } - - var defaultModel = GetDefaultRealtimeModel(); - - return await _realtimeService.CreateSessionAsync( - config, - effectiveApiKey, - defaultModel, - cancellationToken); - } - - /// - /// Streams audio bidirectionally with ElevenLabs. - /// - public IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - return _realtimeService.StreamAudioAsync(session, cancellationToken); - } - - /// - /// Verifies ElevenLabs authentication by calling the user endpoint. - /// This is a free API call that validates the API key. - /// - public override async Task VerifyAuthenticationAsync( - string? apiKey = null, - string? baseUrl = null, - CancellationToken cancellationToken = default) - { - try - { - var startTime = DateTime.UtcNow; - var effectiveApiKey = !string.IsNullOrWhiteSpace(apiKey) ? apiKey : PrimaryKeyCredential.ApiKey; - - if (string.IsNullOrWhiteSpace(effectiveApiKey)) - { - return Core.Interfaces.AuthenticationResult.Failure("API key is required"); - } - - using var client = CreateHttpClient(effectiveApiKey); - - // Use the user endpoint which is free and validates the API key - var request = new HttpRequestMessage(HttpMethod.Get, "user"); - - var response = await client.SendAsync(request, cancellationToken); - var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds; - - if (response.IsSuccessStatusCode) - { - return Core.Interfaces.AuthenticationResult.Success($"Response time: {responseTime:F0}ms"); - } - - // Check for specific error codes - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - return Core.Interfaces.AuthenticationResult.Failure("Invalid API key"); - } - - if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - return Core.Interfaces.AuthenticationResult.Failure("Access denied. Check your API key permissions"); - } - - var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); - return Core.Interfaces.AuthenticationResult.Failure( - $"ElevenLabs authentication failed: {response.StatusCode}", - errorContent); - } - catch (HttpRequestException ex) - { - return Core.Interfaces.AuthenticationResult.Failure( - $"Network error during authentication: {ex.Message}", - ex.ToString()); - } - catch (TaskCanceledException) - { - return Core.Interfaces.AuthenticationResult.Failure("Authentication request timed out"); - } - catch (Exception ex) - { - Logger.LogError(ex, "Unexpected error during ElevenLabs authentication verification"); - return Core.Interfaces.AuthenticationResult.Failure( - $"Authentication verification failed: {ex.Message}", - ex.ToString()); - } - } - - /// - /// Gets available models from ElevenLabs. - /// - public override async Task> GetModelsAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return await Task.FromResult(new List - { - new ExtendedModelInfo - { - Id = "eleven_monolingual_v1", - OwnedBy = "elevenlabs", - ProviderName = "ElevenLabs", - Capabilities = new ConduitLLM.Providers.Common.Models.ModelCapabilities - { - Chat = false, - TextToSpeech = true, - RealtimeAudio = false, - SupportedAudioOperations = new List { AudioOperation.TextToSpeech } - } - }, - new ExtendedModelInfo - { - Id = "eleven_multilingual_v2", - OwnedBy = "elevenlabs", - ProviderName = "ElevenLabs", - Capabilities = new ConduitLLM.Providers.Common.Models.ModelCapabilities - { - Chat = false, - TextToSpeech = true, - RealtimeAudio = false, - SupportedAudioOperations = new List { AudioOperation.TextToSpeech } - } - }, - new ExtendedModelInfo - { - Id = "eleven_conversational_v1", - OwnedBy = "elevenlabs", - ProviderName = "ElevenLabs", - Capabilities = new ConduitLLM.Providers.Common.Models.ModelCapabilities - { - Chat = false, - TextToSpeech = false, - RealtimeAudio = true, - SupportedAudioOperations = new List { AudioOperation.Realtime } - } - } - }); - } - - /// - /// Creates image generation from ElevenLabs. - /// - public override Task CreateImageAsync( - ImageGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return Task.FromException( - new NotSupportedException("ElevenLabs does not support image generation. Use text-to-speech or real-time audio instead.")); - } - - /// - /// Creates embeddings from ElevenLabs. - /// - public override Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return Task.FromException( - new NotSupportedException("ElevenLabs does not support text embeddings. Use text-to-speech or real-time audio instead.")); - } - - - #region Configuration Helpers - - /// - /// Gets the default text-to-speech model from configuration or falls back to eleven_monolingual_v1. - /// - private string GetDefaultTextToSpeechModel() - { - // Check provider-specific override first - var providerOverride = DefaultModels?.Audio?.ProviderOverrides - ?.GetValueOrDefault(ProviderName.ToLowerInvariant())?.TextToSpeechModel; - - if (!string.IsNullOrWhiteSpace(providerOverride)) - return providerOverride; - - // Check global default - var globalDefault = DefaultModels?.Audio?.DefaultTextToSpeechModel; - if (!string.IsNullOrWhiteSpace(globalDefault)) - return globalDefault; - - // Fallback to hardcoded default for backward compatibility - return "eleven_monolingual_v1"; - } - - /// - /// Gets the default realtime model from configuration or falls back to eleven_conversational_v1. - /// - private string GetDefaultRealtimeModel() - { - // Check provider-specific override first - var providerOverride = DefaultModels?.Realtime?.ProviderOverrides - ?.GetValueOrDefault(ProviderName.ToLowerInvariant()); - - if (!string.IsNullOrWhiteSpace(providerOverride)) - return providerOverride; - - // Check global default - var globalDefault = DefaultModels?.Realtime?.DefaultRealtimeModel; - if (!string.IsNullOrWhiteSpace(globalDefault)) - return globalDefault; - - // Fallback to hardcoded default for backward compatibility - return "eleven_conversational_v1"; - } - - /// - protected override string GetDefaultBaseUrl() - { - return DEFAULT_BASE_URL; - } - - #endregion - } -} diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsModels.cs b/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsModels.cs deleted file mode 100644 index 4eb2a63a4..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsModels.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// Response model for ElevenLabs voices endpoint. - /// - internal class ElevenLabsVoicesResponse - { - public List? Voices { get; set; } - } - - /// - /// Represents a voice from ElevenLabs. - /// - internal class ElevenLabsVoice - { - public string VoiceId { get; set; } = string.Empty; - public string Name { get; set; } = string.Empty; - public string? PreviewUrl { get; set; } - public ElevenLabsVoiceLabels? Labels { get; set; } - } - - /// - /// Labels associated with an ElevenLabs voice. - /// - internal class ElevenLabsVoiceLabels - { - public string? Language { get; set; } - public string? Gender { get; set; } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsRealtimeSession.cs b/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsRealtimeSession.cs deleted file mode 100644 index 8ee629199..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/ElevenLabsRealtimeSession.cs +++ /dev/null @@ -1,258 +0,0 @@ -using System.Net.WebSockets; -using System.Runtime.CompilerServices; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Common.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// ElevenLabs-specific implementation of a real-time session. - /// - internal class ElevenLabsRealtimeSession : RealtimeSession - { - private readonly IRealtimeMessageTranslator _translator; - private readonly ILogger _logger; - private readonly RealtimeSessionConfig _config; - private readonly ClientWebSocket _webSocket; - - public ElevenLabsRealtimeSession( - ClientWebSocket webSocket, - IRealtimeMessageTranslator translator, - ILogger logger, - RealtimeSessionConfig config) - : base() - { - _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); - _translator = translator ?? throw new ArgumentNullException(nameof(translator)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = config ?? throw new ArgumentNullException(nameof(config)); - Config = config; - Provider = "ElevenLabs"; - } - - /// - /// Configures the ElevenLabs session with initial parameters. - /// - public async Task ConfigureAsync(RealtimeSessionConfig config, CancellationToken cancellationToken) - { - var configMessage = new ProviderRealtimeMessage - { - Type = "configure", - Data = new Dictionary - { - ["voice_id"] = config.Voice ?? "21m00Tcm4TlvDq8ikWAM", - ["language"] = config.Language ?? "en", - ["model_id"] = config.Model ?? "eleven_conversational_v1", // Model should be set in CreateSessionAsync - ["voice_settings"] = new Dictionary - { - ["stability"] = 0.5, - ["similarity_boost"] = 0.8 - }, - ["generation_config"] = new Dictionary - { - ["chunk_size"] = 200, // ms - ["streaming"] = true - } - } - }; - - await SendMessageAsync(configMessage, cancellationToken); - } - - /// - /// Sends a message through the ElevenLabs session. - /// - public async Task SendMessageAsync(ProviderRealtimeMessage message, CancellationToken cancellationToken = default) - { - if (_webSocket?.State != WebSocketState.Open) - throw new InvalidOperationException("WebSocket is not open"); - - // Convert ProviderRealtimeMessage to RealtimeMessage for translator - var realtimeMessage = new RealtimeAudioFrame - { - SessionId = message.SessionId, - Timestamp = message.Timestamp - }; - - var jsonMessage = await _translator.TranslateToProviderAsync(realtimeMessage); - var buffer = System.Text.Encoding.UTF8.GetBytes(jsonMessage); - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); - } - - /// - /// Receives messages from the ElevenLabs session. - /// - public async IAsyncEnumerable ReceiveMessagesAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var bufferArray = new byte[4096]; - var buffer = new ArraySegment(bufferArray); - - while (!cancellationToken.IsCancellationRequested && _webSocket?.State == WebSocketState.Open) - { - ProviderRealtimeMessage? messageToYield = null; - bool shouldBreak = false; - - try - { - var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); - if (result.MessageType == WebSocketMessageType.Text) - { - var json = System.Text.Encoding.UTF8.GetString(bufferArray, buffer.Offset, result.Count); - - // Parse the message - messageToYield = new ProviderRealtimeMessage - { - Type = "message", - Data = new Dictionary { ["raw"] = json }, - Timestamp = DateTime.UtcNow - }; - } - else if (result.MessageType == WebSocketMessageType.Close) - { - shouldBreak = true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error receiving message from ElevenLabs"); - messageToYield = new ProviderRealtimeMessage - { - Type = "error", - Data = new Dictionary - { - ["error"] = "Receive error", - ["details"] = ex.Message - }, - Timestamp = DateTime.UtcNow - }; - shouldBreak = true; - } - - if (messageToYield != null) - { - yield return messageToYield; - } - - if (shouldBreak) - { - break; - } - } - } - - /// - /// Creates a duplex stream for bidirectional communication. - /// - public IAsyncDuplexStream CreateDuplexStream() - { - return new RealtimeDuplexStream(this); - } - - /// - /// Closes the real-time session. - /// - public async Task CloseAsync(CancellationToken cancellationToken = default) - { - if (_webSocket?.State == WebSocketState.Open) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session closed", cancellationToken); - } - State = SessionState.Closed; - } - - /// - /// Duplex stream implementation for ElevenLabs. - /// - private class RealtimeDuplexStream : IAsyncDuplexStream - { - private readonly ElevenLabsRealtimeSession _session; - - public RealtimeDuplexStream(ElevenLabsRealtimeSession session) - { - _session = session ?? throw new ArgumentNullException(nameof(session)); - } - - public bool IsConnected => _session.State == SessionState.Connected; - - public async ValueTask SendAsync(RealtimeAudioFrame item, CancellationToken cancellationToken = default) - { - var message = new ProviderRealtimeMessage - { - Type = "audio", - Data = new Dictionary - { - ["audio"] = Convert.ToBase64String(item.AudioData), - ["timestamp"] = item.Timestamp - } - }; - await _session.SendMessageAsync(message, cancellationToken); - } - - public async IAsyncEnumerable ReceiveAsync( - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var message in _session.ReceiveMessagesAsync(cancellationToken)) - { - // Convert ProviderRealtimeMessage to RealtimeResponse - var response = new RealtimeResponse - { - SessionId = message.SessionId, - Timestamp = message.Timestamp, - EventType = RealtimeEventType.AudioDelta - }; - - if (message.Type == "audio" && message.Data != null) - { - if (message.Data.TryGetValue("audio", out var audioBase64) && audioBase64 is string base64String) - { - response.Audio = new AudioDelta - { - Data = Convert.FromBase64String(base64String), - IsComplete = false - }; - response.EventType = RealtimeEventType.AudioDelta; - } - } - else if (message.Type == "text" && message.Data != null) - { - if (message.Data.TryGetValue("text", out var text) && text is string textString) - { - response.TextResponse = textString; - response.EventType = RealtimeEventType.TextResponse; - } - } - else if (message.Type == "error" && message.Data != null) - { - if (message.Data.TryGetValue("error", out var error)) - { - response.Error = new ErrorInfo - { - Code = "ELEVENLABS_ERROR", - Message = error?.ToString() ?? "Unknown error" - }; - response.EventType = RealtimeEventType.Error; - } - } - - yield return response; - } - } - - public async ValueTask CompleteAsync() - { - // Send end-of-stream message - var message = new ProviderRealtimeMessage - { - Type = "end_stream", - Timestamp = DateTime.UtcNow - }; - await _session.SendMessageAsync(message, CancellationToken.None); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsRealtimeService.cs b/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsRealtimeService.cs deleted file mode 100644 index 68c2d09fb..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsRealtimeService.cs +++ /dev/null @@ -1,175 +0,0 @@ -using System.Net.WebSockets; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Common.Models; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// Service for handling ElevenLabs real-time audio operations. - /// - internal class ElevenLabsRealtimeService - { - private const string WS_BASE_URL = "wss://api.elevenlabs.io/v1"; - private readonly IRealtimeMessageTranslator _translator; - private readonly ILogger _logger; - - public ElevenLabsRealtimeService(IRealtimeMessageTranslator translator, ILogger logger) - { - _translator = translator ?? throw new ArgumentNullException(nameof(translator)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - /// - /// Creates a new real-time session with ElevenLabs Conversational AI. - /// - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string apiKey, - string defaultModel, - CancellationToken cancellationToken = default) - { - if (string.IsNullOrEmpty(apiKey)) - { - throw new InvalidOperationException("API key is required for ElevenLabs"); - } - - try - { - // Create WebSocket connection to ElevenLabs Conversational AI - var wsUri = new Uri($"{WS_BASE_URL}/conversational/websocket"); - var clientWebSocket = new ClientWebSocket(); - clientWebSocket.Options.SetRequestHeader("Authorization", $"Bearer {apiKey}"); - clientWebSocket.Options.SetRequestHeader("User-Agent", "ConduitLLM/1.0"); - - await clientWebSocket.ConnectAsync(wsUri, cancellationToken); - - // Ensure model is set to default if not provided - if (string.IsNullOrEmpty(config.Model)) - { - config.Model = defaultModel; - } - - var session = new ElevenLabsRealtimeSession( - clientWebSocket, - _translator, - _logger, - config); - - // Send initial configuration - await session.ConfigureAsync(config, cancellationToken); - - return session; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create ElevenLabs real-time session"); - throw new LLMCommunicationException("Failed to establish connection with ElevenLabs", ex); - } - } - - /// - /// Streams audio bidirectionally with ElevenLabs. - /// - public IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - if (session is not ElevenLabsRealtimeSession elevenLabsSession) - { - throw new ArgumentException("Session must be created by ElevenLabsClient", nameof(session)); - } - - return elevenLabsSession.CreateDuplexStream(); - } - - /// - /// Updates the configuration of an active real-time session. - /// - public async Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - if (session is not ElevenLabsRealtimeSession elevenLabsSession) - { - throw new ArgumentException("Session must be created by ElevenLabsClient", nameof(session)); - } - - // Send update message to ElevenLabs - var updateMessage = new ProviderRealtimeMessage - { - Type = "update_session", - Data = new Dictionary - { - ["voice_id"] = session.Config.Voice ?? "rachel", - ["language"] = "en", - ["system_prompt"] = updates.SystemPrompt ?? session.Config.SystemPrompt ?? string.Empty - } - }; - - await elevenLabsSession.SendMessageAsync(updateMessage, cancellationToken); - } - - /// - /// Closes an active real-time session. - /// - public async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - if (session is not ElevenLabsRealtimeSession elevenLabsSession) - { - throw new ArgumentException("Session must be created by ElevenLabsClient", nameof(session)); - } - - await elevenLabsSession.CloseAsync(cancellationToken); - } - - /// - /// Checks if the client supports real-time audio conversations. - /// - public Task SupportsRealtimeAsync(CancellationToken cancellationToken = default) - { - // ElevenLabs supports real-time audio with conversational AI models - return Task.FromResult(true); - } - - /// - /// Gets the capabilities of the ElevenLabs real-time audio system. - /// - public Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) - { - var capabilities = new RealtimeCapabilities - { - SupportedInputFormats = new List - { - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.PCM16_24kHz, - RealtimeAudioFormat.PCM16_48kHz - }, - SupportedOutputFormats = new List - { - RealtimeAudioFormat.PCM16_24kHz, - RealtimeAudioFormat.PCM16_48kHz - }, - MaxSessionDurationSeconds = 3600, // 1 hour - SupportsFunctionCalling = false, - SupportsInterruptions = true, - TurnDetection = new TurnDetectionCapabilities - { - SupportedTypes = new List { TurnDetectionType.ServerVAD }, - MinSilenceThresholdMs = 50, - MaxSilenceThresholdMs = 500, - SupportsCustomParameters = true - } - }; - - return Task.FromResult(capabilities); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsTextToSpeechService.cs b/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsTextToSpeechService.cs deleted file mode 100644 index 4c0d32068..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsTextToSpeechService.cs +++ /dev/null @@ -1,161 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// Service for handling ElevenLabs text-to-speech operations. - /// - internal class ElevenLabsTextToSpeechService - { - private const string DEFAULT_BASE_URL = "https://api.elevenlabs.io/v1"; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - public ElevenLabsTextToSpeechService(ILogger logger, JsonSerializerOptions jsonOptions) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _jsonOptions = jsonOptions ?? throw new ArgumentNullException(nameof(jsonOptions)); - } - - /// - /// Creates speech audio from text using ElevenLabs. - /// - public async Task CreateSpeechAsync( - HttpClient httpClient, - string? baseUrl, - TextToSpeechRequest request, - string model, - CancellationToken cancellationToken = default) - { - // ElevenLabs uses voice IDs instead of voice names - var voiceId = request.Voice ?? "21m00Tcm4TlvDq8ikWAM"; // Default voice ID - - var effectiveBaseUrl = baseUrl ?? DEFAULT_BASE_URL; - var requestUrl = $"{effectiveBaseUrl}/text-to-speech/{voiceId}"; - - var requestBody = new Dictionary - { - ["text"] = request.Input, - ["model_id"] = model, - ["voice_settings"] = new Dictionary - { - ["stability"] = request.VoiceSettings?.Stability ?? 0.5, - ["similarity_boost"] = request.VoiceSettings?.SimilarityBoost ?? 0.5, - ["style"] = request.VoiceSettings?.Style ?? "default" - } - }; - - var jsonContent = JsonSerializer.Serialize(requestBody, _jsonOptions); - using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - - var response = await httpClient.PostAsync(requestUrl, content, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); - throw new LLMCommunicationException($"ElevenLabs API error: {response.StatusCode} - {errorContent}"); - } - - var audioData = await response.Content.ReadAsByteArrayAsync(cancellationToken); - - return new TextToSpeechResponse - { - AudioData = audioData, - Format = request.ResponseFormat?.ToString().ToLower() ?? "mp3", - SampleRate = 22050, // ElevenLabs default - Duration = null // Would need to calculate from audio data - }; - } - - /// - /// Streams speech audio from text using ElevenLabs. - /// - public async IAsyncEnumerable StreamSpeechAsync( - HttpClient httpClient, - string? baseUrl, - TextToSpeechRequest request, - string model, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var voiceId = request.Voice ?? "21m00Tcm4TlvDq8ikWAM"; - - var effectiveBaseUrl = baseUrl ?? DEFAULT_BASE_URL; - var requestUrl = $"{effectiveBaseUrl}/text-to-speech/{voiceId}/stream"; - - var requestBody = new Dictionary - { - ["text"] = request.Input, - ["model_id"] = model, - ["voice_settings"] = new Dictionary - { - ["stability"] = request.VoiceSettings?.Stability ?? 0.5, - ["similarity_boost"] = request.VoiceSettings?.SimilarityBoost ?? 0.5 - } - }; - - var jsonContent = JsonSerializer.Serialize(requestBody, _jsonOptions); - using var content = new StringContent(jsonContent, Encoding.UTF8, "application/json"); - - var response = await httpClient.PostAsync(requestUrl, content, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); - throw new LLMCommunicationException($"ElevenLabs API error: {response.StatusCode} - {errorContent}"); - } - - using var stream = await response.Content.ReadAsStreamAsync(cancellationToken); - var buffer = new byte[4096]; - int bytesRead; - - while ((bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length, cancellationToken)) > 0) - { - var chunk = new byte[bytesRead]; - Array.Copy(buffer, 0, chunk, 0, bytesRead); - - yield return new AudioChunk - { - Data = chunk, - IsFinal = false - }; - } - - // Final chunk - yield return new AudioChunk - { - Data = Array.Empty(), - IsFinal = true - }; - } - - /// - /// Gets the audio formats supported by ElevenLabs. - /// - public Task> GetSupportedFormatsAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(new List - { - "mp3", - "wav", - "pcm", - "ogg", - "flac" - }); - } - - /// - /// Checks if the client supports text-to-speech synthesis. - /// - public Task SupportsTextToSpeechAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(true); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsVoiceService.cs b/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsVoiceService.cs deleted file mode 100644 index e52f8eb49..000000000 --- a/ConduitLLM.Providers/Providers/ElevenLabs/Services/ElevenLabsVoiceService.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text.Json; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.ElevenLabs -{ - /// - /// Service for handling ElevenLabs voice management operations. - /// - internal class ElevenLabsVoiceService - { - private const string DEFAULT_BASE_URL = "https://api.elevenlabs.io/v1"; - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - public ElevenLabsVoiceService(ILogger logger, JsonSerializerOptions jsonOptions) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _jsonOptions = jsonOptions ?? throw new ArgumentNullException(nameof(jsonOptions)); - } - - /// - /// Lists available voices from ElevenLabs. - /// - public async Task> ListVoicesAsync( - HttpClient httpClient, - string? baseUrl, - CancellationToken cancellationToken = default) - { - var effectiveBaseUrl = baseUrl ?? DEFAULT_BASE_URL; - var response = await httpClient.GetAsync($"{effectiveBaseUrl}/voices", cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); - throw new LLMCommunicationException($"ElevenLabs API error: {response.StatusCode} - {errorContent}"); - } - - var jsonContent = await response.Content.ReadAsStringAsync(cancellationToken); - var voicesResponse = JsonSerializer.Deserialize(jsonContent, _jsonOptions); - - return voicesResponse?.Voices?.Select(v => new VoiceInfo - { - VoiceId = v.VoiceId, - Name = v.Name, - SupportedLanguages = new List { v.Labels?.Language ?? "en" }, - Gender = v.Labels?.Gender?.ToLower() switch - { - "male" => VoiceGender.Male, - "female" => VoiceGender.Female, - _ => VoiceGender.Neutral - }, - SampleUrl = v.PreviewUrl, - Metadata = new Dictionary { { "provider", "ElevenLabs" } } - }).ToList() ?? new List(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Audio.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Audio.cs deleted file mode 100644 index 29717c1cd..000000000 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Audio.cs +++ /dev/null @@ -1,413 +0,0 @@ -using System.Net.Http.Headers; -using System.Runtime.CompilerServices; -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Helpers; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.OpenAI -{ - /// - /// OpenAIClient partial class containing audio transcription and text-to-speech functionality. - /// - public partial class OpenAIClient - { - /// - /// Transcribes audio content into text using OpenAI's Whisper model. - /// - public async Task TranscribeAudioAsync( - AudioTranscriptionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "TranscribeAudio"); - - using var client = CreateHttpClient(apiKey); - - var endpoint = _isAzure - ? GetAzureAudioEndpoint("transcriptions") - : UrlBuilder.Combine(BaseUrl, Constants.Endpoints.AudioTranscriptions); - - using var content = new MultipartFormDataContent(); - - // Add audio file - if (request.AudioData != null) - { - var audioContent = new ByteArrayContent(request.AudioData); - audioContent.Headers.ContentType = new MediaTypeHeaderValue("application/octet-stream"); - content.Add(audioContent, "file", request.FileName ?? "audio.mp3"); - } - else if (!string.IsNullOrWhiteSpace(request.AudioUrl)) - { - throw new NotSupportedException("URL-based audio transcription is not supported by OpenAI API. Please provide audio data directly."); - } - - // Add model - must be specified - if (string.IsNullOrWhiteSpace(request.Model)) - { - var defaultTranscriptionModel = GetDefaultTranscriptionModel(); - if (string.IsNullOrWhiteSpace(defaultTranscriptionModel)) - { - throw new ArgumentException("Model must be specified for transcription requests", nameof(request)); - } - request.Model = defaultTranscriptionModel; - } - content.Add(new StringContent(request.Model), "model"); - - // Add optional parameters - if (!string.IsNullOrWhiteSpace(request.Language)) - content.Add(new StringContent(request.Language), "language"); - - if (!string.IsNullOrWhiteSpace(request.Prompt)) - content.Add(new StringContent(request.Prompt), "prompt"); - - if (request.Temperature.HasValue) - content.Add(new StringContent(request.Temperature.Value.ToString()), "temperature"); - - if (request.ResponseFormat.HasValue) - content.Add(new StringContent(request.ResponseFormat.Value.ToString().ToLowerInvariant()), "response_format"); - - return await ExecuteApiRequestAsync(async () => - { - var response = await client.PostAsync(endpoint, content, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var error = await ReadErrorContentAsync(response, cancellationToken); - throw new LLMCommunicationException( - $"Audio transcription failed: {error}", - response.StatusCode, - ProviderName); - } - - var responseText = await response.Content.ReadAsStringAsync(cancellationToken); - - // Handle different response formats - if (request.ResponseFormat == TranscriptionFormat.Text || - request.ResponseFormat == TranscriptionFormat.Srt || - request.ResponseFormat == TranscriptionFormat.Vtt) - { - return new AudioTranscriptionResponse - { - Text = responseText, - Model = request.Model ?? GetDefaultTranscriptionModel() ?? "unknown" - }; - } - - // Default JSON response - var jsonResponse = JsonSerializer.Deserialize(responseText, DefaultJsonOptions); - - return new AudioTranscriptionResponse - { - Text = jsonResponse?.Text ?? string.Empty, - Language = jsonResponse?.Language, - Duration = jsonResponse?.Duration, - Model = request.Model ?? "whisper-1", - Segments = jsonResponse?.Segments?.Select(s => new ConduitLLM.Core.Models.Audio.TranscriptionSegment - { - Id = s.Id, - Start = s.Start, - End = s.End, - Text = s.Text - }).ToList(), - Words = jsonResponse?.Words?.Select(w => new ConduitLLM.Core.Models.Audio.TranscriptionWord - { - Word = w.Word, - Start = w.Start, - End = w.End - }).ToList() - }; - }, "TranscribeAudio", cancellationToken); - } - - /// - /// Converts text into speech using OpenAI's TTS models. - /// - public async Task CreateSpeechAsync( - ConduitLLM.Core.Models.Audio.TextToSpeechRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - ValidateRequest(request, "CreateSpeech"); - - using var client = CreateHttpClient(apiKey); - - var endpoint = _isAzure - ? GetAzureAudioEndpoint("speech") - : UrlBuilder.Combine(BaseUrl, Constants.Endpoints.AudioSpeech); - - var openAIRequest = new TextToSpeechRequest - { - Model = request.Model ?? GetDefaultTextToSpeechModel() ?? throw new ArgumentException("Model must be specified for text-to-speech requests", nameof(request)), - Input = request.Input, - Voice = request.Voice, - ResponseFormat = MapAudioFormat(request.ResponseFormat), - Speed = request.Speed - }; - - var json = JsonSerializer.Serialize(openAIRequest, DefaultJsonOptions); - using var content = new StringContent(json, Encoding.UTF8, "application/json"); - - return await ExecuteApiRequestAsync(async () => - { - var response = await client.PostAsync(endpoint, content, cancellationToken); - - if (!response.IsSuccessStatusCode) - { - var error = await ReadErrorContentAsync(response, cancellationToken); - throw new LLMCommunicationException( - $"Text-to-speech failed: {error}", - response.StatusCode, - ProviderName); - } - - var audioData = await response.Content.ReadAsByteArrayAsync(cancellationToken); - - return new TextToSpeechResponse - { - AudioData = audioData, - Format = request.ResponseFormat?.ToString().ToLowerInvariant() ?? "mp3", - VoiceUsed = request.Voice, - ModelUsed = request.Model ?? GetDefaultTextToSpeechModel() ?? "unknown", - CharacterCount = request.Input.Length - }; - }, "CreateSpeech", cancellationToken); - } - - /// - /// Streams text-to-speech audio as it's generated. - /// - public async IAsyncEnumerable StreamSpeechAsync( - ConduitLLM.Core.Models.Audio.TextToSpeechRequest request, - string? apiKey = null, - [EnumeratorCancellation] CancellationToken cancellationToken = default) - { - // OpenAI API doesn't support streaming TTS yet, so we'll get the full response and chunk it - var response = await CreateSpeechAsync(request, apiKey, cancellationToken); - - // Simulate streaming by chunking the response - const int chunkSize = 4096; // 4KB chunks - var totalChunks = (int)Math.Ceiling((double)response.AudioData.Length / chunkSize); - - for (int i = 0; i < totalChunks; i++) - { - var offset = i * chunkSize; - var length = Math.Min(chunkSize, response.AudioData.Length - offset); - var chunkData = new byte[length]; - Array.Copy(response.AudioData, offset, chunkData, 0, length); - - yield return new AudioChunk - { - Data = chunkData, - ChunkIndex = i, - IsFinal = i == totalChunks - 1 - }; - - // Small delay to simulate streaming - await Task.Delay(10, cancellationToken); - } - } - - /// - /// Lists available voices for text-to-speech. - /// - public async Task> ListVoicesAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // OpenAI has a fixed set of voices, return them directly - await Task.CompletedTask; // Async method signature requirement - - return new List - { - new VoiceInfo - { - VoiceId = "alloy", - Name = "Alloy", - Description = "Neutral and balanced voice", - Gender = VoiceGender.Neutral - }, - new VoiceInfo - { - VoiceId = "echo", - Name = "Echo", - Description = "Smooth male voice", - Gender = VoiceGender.Male - }, - new VoiceInfo - { - VoiceId = "fable", - Name = "Fable", - Description = "Expressive British voice", - Gender = VoiceGender.Male, - Accent = "British" - }, - new VoiceInfo - { - VoiceId = "onyx", - Name = "Onyx", - Description = "Deep male voice", - Gender = VoiceGender.Male - }, - new VoiceInfo - { - VoiceId = "nova", - Name = "Nova", - Description = "Friendly female voice", - Gender = VoiceGender.Female - }, - new VoiceInfo - { - VoiceId = "shimmer", - Name = "Shimmer", - Description = "Warm female voice", - Gender = VoiceGender.Female - } - }; - } - - /// - /// Checks if the client supports audio transcription. - /// - public async Task SupportsTranscriptionAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - if (_capabilityService != null) - { - try - { - return await _capabilityService.SupportsAudioTranscriptionAsync(ProviderModelId); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to check transcription capability via ModelCapabilityService, falling back to default"); - } - } - - // Fallback: OpenAI generally supports transcription with Whisper models - return true; - } - - /// - /// Gets supported audio formats for transcription. - /// - public async Task> GetSupportedFormatsAsync( - CancellationToken cancellationToken = default) - { - if (_capabilityService != null) - { - try - { - return await _capabilityService.GetSupportedFormatsAsync(ProviderModelId); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get supported formats via ModelCapabilityService, falling back to default"); - } - } - - // Fallback: OpenAI Whisper supported formats - return new List { "mp3", "mp4", "mpeg", "mpga", "m4a", "wav", "webm" }; - } - - /// - /// Gets supported languages for transcription. - /// - public async Task> GetSupportedLanguagesAsync( - CancellationToken cancellationToken = default) - { - if (_capabilityService != null) - { - try - { - return await _capabilityService.GetSupportedLanguagesAsync(ProviderModelId); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get supported languages via ModelCapabilityService, falling back to default"); - } - } - - // Fallback: Whisper supports many languages - return new List - { - "en", "zh", "de", "es", "ru", "ko", "fr", "ja", "pt", "tr", - "pl", "ca", "nl", "ar", "sv", "it", "id", "hi", "fi", "vi", - "he", "uk", "el", "ms", "cs", "ro", "da", "hu", "ta", "no", - "th", "ur", "hr", "bg", "lt", "la", "mi", "ml", "cy", "sk", - "te", "fa", "lv", "bn", "sr", "az", "sl", "kn", "et", "mk", - "br", "eu", "is", "hy", "ne", "mn", "bs", "kk", "sq", "sw", - "gl", "mr", "pa", "si", "km", "sn", "yo", "so", "af", "oc", - "ka", "be", "tg", "sd", "gu", "am", "yi", "lo", "uz", "fo", - "ht", "ps", "tk", "nn", "mt", "sa", "lb", "my", "bo", "tl", - "mg", "as", "tt", "haw", "ln", "ha", "ba", "jw", "su" - }; - } - - /// - /// Checks if the client supports text-to-speech. - /// - public async Task SupportsTextToSpeechAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - if (_capabilityService != null) - { - try - { - return await _capabilityService.SupportsTextToSpeechAsync(ProviderModelId); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to check TTS capability via ModelCapabilityService, falling back to default"); - } - } - - // Fallback: OpenAI generally supports TTS - return true; - } - - /// - /// Gets supported audio formats for text-to-speech. - /// - async Task> Core.Interfaces.ITextToSpeechClient.GetSupportedFormatsAsync( - CancellationToken cancellationToken) - { - await Task.CompletedTask; - return new List { "mp3", "opus", "aac", "flac", "wav", "pcm" }; - } - - /// - /// Gets the Azure-specific audio endpoint. - /// - private string GetAzureAudioEndpoint(string operation) - { - var url = UrlBuilder.Combine(BaseUrl, "openai", "deployments", ProviderModelId, "audio", operation); - return UrlBuilder.AppendQueryString(url, ("api-version", Constants.AzureApiVersion)); - } - - /// - /// Maps the audio format enum to OpenAI's expected string format. - /// - private static string? MapAudioFormat(AudioFormat? format) - { - if (!format.HasValue) return null; - - return format.Value switch - { - AudioFormat.Mp3 => "mp3", - AudioFormat.Opus => "opus", - AudioFormat.Aac => "aac", - AudioFormat.Flac => "flac", - AudioFormat.Wav => "wav", - AudioFormat.Pcm => "pcm", - _ => "mp3" - }; - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Capabilities.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Capabilities.cs index 743cc90c3..7cea8b1e9 100644 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Capabilities.cs +++ b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Capabilities.cs @@ -23,8 +23,6 @@ public override Task GetCapabilitiesAsync(string? modelId var isGpt35Turbo = modelLower.Contains("gpt-3.5-turbo", StringComparison.OrdinalIgnoreCase); var isChatModel = isGpt4 || isGpt35Turbo || modelLower.Contains("gpt", StringComparison.OrdinalIgnoreCase); var isEmbeddingModel = modelLower.Contains("embedding", StringComparison.OrdinalIgnoreCase); - var isWhisperModel = modelLower.Contains("whisper", StringComparison.OrdinalIgnoreCase); - var isTtsModel = modelLower.Contains("tts", StringComparison.OrdinalIgnoreCase); return Task.FromResult(new ProviderCapabilities { @@ -59,9 +57,7 @@ public override Task GetCapabilitiesAsync(string? modelId Embeddings = isEmbeddingModel, ImageGeneration = isDalleModel, VisionInput = isGpt4Vision, - FunctionCalling = isGpt4 || isGpt35Turbo, - AudioTranscription = isWhisperModel, - TextToSpeech = isTtsModel + FunctionCalling = isGpt4 || isGpt35Turbo } }); } diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.RealtimeAudio.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.RealtimeAudio.cs deleted file mode 100644 index 69e760cb0..000000000 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.RealtimeAudio.cs +++ /dev/null @@ -1,284 +0,0 @@ -using System.Runtime.CompilerServices; -using System.Text.Json; - -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Helpers; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.OpenAI -{ - /// - /// OpenAIClient partial class containing realtime audio functionality. - /// - public partial class OpenAIClient - { - /// - /// Creates a realtime audio session with OpenAI's API. - /// - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // OpenAI Realtime API uses WebSocket connection - var wsUrl = UrlBuilder.ToWebSocketUrl(BaseUrl); - - // Model must be specified - var model = config.Model; - if (string.IsNullOrWhiteSpace(model)) - { - model = GetDefaultRealtimeModel(); - if (string.IsNullOrWhiteSpace(model)) - { - throw new ArgumentException("Model must be specified for realtime audio sessions", nameof(config)); - } - } - - wsUrl = UrlBuilder.Combine(wsUrl, "realtime"); - wsUrl = UrlBuilder.AppendQueryString(wsUrl, ("model", model)); - - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey ?? throw new InvalidOperationException("API key is required"); - var session = new OpenAIRealtimeSession(wsUrl, effectiveApiKey, config, Logger); - await session.ConnectAsync(cancellationToken); - - return session; - } - - /// - /// Creates a realtime audio session with OpenAI's API. - /// - /// - /// This method is obsolete and will be removed in the next major version. - /// Use CreateSessionAsync instead, which has the correct parameter order. - /// - [Obsolete("Use CreateSessionAsync instead. This method will be removed in the next major version.")] - public async Task ConnectAsync( - string? apiKey, - RealtimeSessionConfig config, - CancellationToken cancellationToken = default) - { - // Forward to new method with corrected parameter order - return await CreateSessionAsync(config, apiKey, cancellationToken); - } - - /// - /// Checks if the specified model supports realtime audio. - /// - public async Task SupportsRealtimeAsync(string model, CancellationToken cancellationToken = default) - { - if (_capabilityService != null) - { - try - { - return await _capabilityService.SupportsRealtimeAudioAsync(model); - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to check realtime capability via ModelCapabilityService, falling back to default"); - } - } - - // Fallback: Check against known OpenAI realtime models - var supportedModels = new[] { "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01" }; - return supportedModels.Contains(model); - } - - /// - /// Gets the realtime capabilities for OpenAI. - /// - public Task GetRealtimeCapabilitiesAsync(CancellationToken cancellationToken = default) - { - return Task.FromResult(new RealtimeCapabilities - { - SupportedInputFormats = new List - { - RealtimeAudioFormat.PCM16_24kHz, - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.G711_ULAW, - RealtimeAudioFormat.G711_ALAW - }, - SupportedOutputFormats = new List - { - RealtimeAudioFormat.PCM16_24kHz, - RealtimeAudioFormat.PCM16_16kHz - }, - AvailableVoices = new List - { - new VoiceInfo { VoiceId = "alloy", Name = "Alloy", Gender = VoiceGender.Neutral }, - new VoiceInfo { VoiceId = "echo", Name = "Echo", Gender = VoiceGender.Male }, - new VoiceInfo { VoiceId = "shimmer", Name = "Shimmer", Gender = VoiceGender.Female } - }, - SupportedLanguages = new List { "en", "es", "fr", "de", "it", "pt", "ru", "zh", "ja", "ko" }, - SupportsFunctionCalling = true, - SupportsInterruptions = true - }); - } - - /// - /// Creates a stream for realtime audio communication. - /// - public Core.Interfaces.IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - if (session is not OpenAIRealtimeSession openAISession) - throw new InvalidOperationException("Session must be created by this client"); - - return new OpenAIRealtimeStream(openAISession, Logger as ILogger ?? - throw new InvalidOperationException("Logger must be ILogger")); - } - - /// - /// Updates an existing realtime session. - /// - public async Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - if (session is not OpenAIRealtimeSession openAISession) - throw new InvalidOperationException("Session must be created by this client"); - - // For OpenAI, we need to create a provider-specific message - var providerMessage = new Dictionary - { - ["type"] = "session.update", - ["session"] = new Dictionary() - }; - - var sessionData = (Dictionary)providerMessage["session"]; - - if (updates.SystemPrompt != null) - sessionData["instructions"] = updates.SystemPrompt; - - if (updates.Temperature.HasValue) - sessionData["temperature"] = updates.Temperature.Value; - - if (updates.VoiceSettings != null && updates.VoiceSettings.Speed.HasValue) - sessionData["speed"] = updates.VoiceSettings.Speed.Value; - - if (updates.TurnDetection != null) - { - sessionData["turn_detection"] = new Dictionary - { - ["type"] = updates.TurnDetection.Type.ToString().ToLowerInvariant(), - ["threshold"] = updates.TurnDetection.Threshold ?? 0.5, - ["prefix_padding_ms"] = updates.TurnDetection.PrefixPaddingMs ?? 300, - ["silence_duration_ms"] = updates.TurnDetection.SilenceThresholdMs ?? 500 - }; - } - - if (updates.Tools != null) - { - sessionData["tools"] = updates.Tools.Select(t => new - { - type = "function", - function = new - { - name = t.Function?.Name, - description = t.Function?.Description, - parameters = t.Function?.Parameters - } - }).ToList(); - } - - // Convert to JSON and send as a raw message - var json = JsonSerializer.Serialize(providerMessage, DefaultJsonOptions); - await openAISession.SendRawMessageAsync(json, cancellationToken); - } - - /// - /// Closes a realtime session. - /// - public async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - session?.Dispose(); - await Task.CompletedTask; - } - - /// - /// Checks if realtime audio is supported. - /// - Task Core.Interfaces.IRealtimeAudioClient.SupportsRealtimeAsync(string? apiKey, CancellationToken cancellationToken) - { - // OpenAI supports real-time with appropriate models - return Task.FromResult(true); - } - - /// - /// Gets realtime capabilities. - /// - Task Core.Interfaces.IRealtimeAudioClient.GetCapabilitiesAsync(CancellationToken cancellationToken) - { - return GetRealtimeCapabilitiesAsync(cancellationToken); - } - - /// - /// OpenAI-specific realtime stream implementation. - /// - private class OpenAIRealtimeStream : Core.Interfaces.IAsyncDuplexStream - { - private readonly OpenAIRealtimeSession _session; - private readonly ILogger _logger; - - public OpenAIRealtimeStream(OpenAIRealtimeSession session, ILogger logger) - { - _session = session; - _logger = logger; - } - - public bool IsConnected => _session.State == SessionState.Connected || _session.State == SessionState.Active; - - public async ValueTask SendAsync(RealtimeAudioFrame item, CancellationToken cancellationToken = default) - { - if (item.AudioData != null && item.AudioData.Length > 0) - { - // For OpenAI, we need to send the raw provider-specific message - var providerMessage = new Dictionary - { - ["type"] = "input_audio_buffer.append", - ["audio"] = Convert.ToBase64String(item.AudioData) - }; - - var json = JsonSerializer.Serialize(providerMessage, DefaultJsonOptions); - await _session.SendRawMessageAsync(json, cancellationToken); - } - } - - public async IAsyncEnumerable ReceiveAsync([EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var message in _session.ReceiveMessagesAsync(cancellationToken)) - { - var response = ConvertToRealtimeResponse(message); - if (response != null) - yield return response; - } - } - - public async ValueTask CompleteAsync() - { - var providerMessage = new Dictionary - { - ["type"] = "input_audio_buffer.commit" - }; - - var json = JsonSerializer.Serialize(providerMessage, DefaultJsonOptions); - await _session.SendRawMessageAsync(json, CancellationToken.None); - } - - private RealtimeResponse? ConvertToRealtimeResponse(RealtimeMessage message) - { - // The translator should have already converted to RealtimeResponse - if (message is RealtimeResponse response) - return response; - - // If not, we have an unexpected message type - _logger.LogWarning("Received unexpected message type: {Type}", message.GetType().Name); - return null; - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Utilities.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Utilities.cs index d50abc12d..2ed8034dd 100644 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Utilities.cs +++ b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.Utilities.cs @@ -7,112 +7,6 @@ namespace ConduitLLM.Providers.OpenAI /// public partial class OpenAIClient { - /// - /// Gets the default transcription model from configuration. - /// - private string? GetDefaultTranscriptionModel() - { - // Check provider-specific override first - var providerOverride = DefaultModels?.Audio?.ProviderOverrides - ?.GetValueOrDefault(ProviderName.ToLowerInvariant())?.TranscriptionModel; - - if (!string.IsNullOrWhiteSpace(providerOverride)) - return providerOverride; - - // Check global default - var globalDefault = DefaultModels?.Audio?.DefaultTranscriptionModel; - if (!string.IsNullOrWhiteSpace(globalDefault)) - return globalDefault; - - // Use ModelCapabilityService if available - if (_capabilityService != null) - { - try - { - var defaultModel = _capabilityService.GetDefaultModelAsync("openai", "transcription").GetAwaiter().GetResult(); - if (!string.IsNullOrWhiteSpace(defaultModel)) - return defaultModel; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get default transcription model via ModelCapabilityService"); - } - } - - // No default found - model must be specified - return null; - } - - /// - /// Gets the default text-to-speech model from configuration. - /// - private string? GetDefaultTextToSpeechModel() - { - // Check provider-specific override first - var providerOverride = DefaultModels?.Audio?.ProviderOverrides - ?.GetValueOrDefault(ProviderName.ToLowerInvariant())?.TextToSpeechModel; - - if (!string.IsNullOrWhiteSpace(providerOverride)) - return providerOverride; - - // Check global default - var globalDefault = DefaultModels?.Audio?.DefaultTextToSpeechModel; - if (!string.IsNullOrWhiteSpace(globalDefault)) - return globalDefault; - - // Use ModelCapabilityService if available - if (_capabilityService != null) - { - try - { - var defaultModel = _capabilityService.GetDefaultModelAsync("openai", "tts").GetAwaiter().GetResult(); - if (!string.IsNullOrWhiteSpace(defaultModel)) - return defaultModel; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get default TTS model via ModelCapabilityService"); - } - } - - // No default found - model must be specified - return null; - } - - /// - /// Gets the default realtime model from configuration. - /// - private string? GetDefaultRealtimeModel() - { - // Check provider-specific override first - var providerOverride = DefaultModels?.Realtime?.ProviderOverrides - ?.GetValueOrDefault(ProviderName.ToLowerInvariant()); - - if (!string.IsNullOrWhiteSpace(providerOverride)) - return providerOverride; - - // Check global default - var globalDefault = DefaultModels?.Realtime?.DefaultRealtimeModel; - if (!string.IsNullOrWhiteSpace(globalDefault)) - return globalDefault; - - // Use ModelCapabilityService if available - if (_capabilityService != null) - { - try - { - var defaultModel = _capabilityService.GetDefaultModelAsync("openai", "realtime").GetAwaiter().GetResult(); - if (!string.IsNullOrWhiteSpace(defaultModel)) - return defaultModel; - } - catch (Exception ex) - { - Logger.LogWarning(ex, "Failed to get default realtime model via ModelCapabilityService"); - } - } - - // No default found - model must be specified - return null; - } + // Audio and realtime functionality has been removed from the system } } \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.cs index bca8ba030..95ca60777 100644 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.cs +++ b/ConduitLLM.Providers/Providers/OpenAI/OpenAIClient.cs @@ -19,10 +19,7 @@ namespace ConduitLLM.Providers.OpenAI /// It supports both OpenAI's standard API endpoint structure and Azure OpenAI's deployment-based /// endpoints, with automatic URL and authentication format selection based on the provider name. /// - public partial class OpenAIClient : ConduitLLM.Providers.OpenAICompatible.OpenAICompatibleClient, - Core.Interfaces.IAudioTranscriptionClient, - Core.Interfaces.ITextToSpeechClient, - Core.Interfaces.IRealtimeAudioClient + public partial class OpenAIClient : ConduitLLM.Providers.OpenAICompatible.OpenAICompatibleClient { // Default API configuration constants private static class Constants diff --git a/ConduitLLM.Providers/Providers/OpenAI/OpenAIModels.Audio.cs b/ConduitLLM.Providers/Providers/OpenAI/OpenAIModels.Audio.cs deleted file mode 100644 index a462fbaff..000000000 --- a/ConduitLLM.Providers/Providers/OpenAI/OpenAIModels.Audio.cs +++ /dev/null @@ -1,94 +0,0 @@ -using System.Text.Json.Serialization; - -namespace ConduitLLM.Providers.OpenAI -{ - /// - /// OpenAI-specific audio transcription response model. - /// - public class TranscriptionResponse - { - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; - - [JsonPropertyName("language")] - public string? Language { get; set; } - - [JsonPropertyName("duration")] - public double? Duration { get; set; } - - [JsonPropertyName("segments")] - public List? Segments { get; set; } - - [JsonPropertyName("words")] - public List? Words { get; set; } - } - - /// - /// OpenAI-specific transcription segment. - /// - public class TranscriptionSegment - { - [JsonPropertyName("id")] - public int Id { get; set; } - - [JsonPropertyName("start")] - public double Start { get; set; } - - [JsonPropertyName("end")] - public double End { get; set; } - - [JsonPropertyName("text")] - public string Text { get; set; } = string.Empty; - - [JsonPropertyName("tokens")] - public List? Tokens { get; set; } - - [JsonPropertyName("temperature")] - public double? Temperature { get; set; } - - [JsonPropertyName("avg_logprob")] - public double? AvgLogprob { get; set; } - - [JsonPropertyName("compression_ratio")] - public double? CompressionRatio { get; set; } - - [JsonPropertyName("no_speech_prob")] - public double? NoSpeechProb { get; set; } - } - - /// - /// OpenAI-specific transcription word. - /// - public class TranscriptionWord - { - [JsonPropertyName("word")] - public string Word { get; set; } = string.Empty; - - [JsonPropertyName("start")] - public double Start { get; set; } - - [JsonPropertyName("end")] - public double End { get; set; } - } - - /// - /// OpenAI text-to-speech request model. - /// - public class TextToSpeechRequest - { - [JsonPropertyName("model")] - public required string Model { get; set; } - - [JsonPropertyName("input")] - public string Input { get; set; } = string.Empty; - - [JsonPropertyName("voice")] - public string Voice { get; set; } = string.Empty; - - [JsonPropertyName("response_format")] - public string? ResponseFormat { get; set; } - - [JsonPropertyName("speed")] - public double? Speed { get; set; } - } -} diff --git a/ConduitLLM.Providers/Providers/OpenAICompatible/OpenAICompatibleClient.Utilities.cs b/ConduitLLM.Providers/Providers/OpenAICompatible/OpenAICompatibleClient.Utilities.cs index 10803716a..a11be6017 100644 --- a/ConduitLLM.Providers/Providers/OpenAICompatible/OpenAICompatibleClient.Utilities.cs +++ b/ConduitLLM.Providers/Providers/OpenAICompatible/OpenAICompatibleClient.Utilities.cs @@ -463,9 +463,7 @@ protected override void ConfigureHttpClient(HttpClient client, string apiKey) Embeddings = false, // Usually separate models ImageGeneration = false, // Usually separate models VisionInput = false, // Provider-specific - FunctionCalling = true, - AudioTranscription = false, // Provider-specific - TextToSpeech = false // Provider-specific + FunctionCalling = true } }); } diff --git a/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.Realtime.cs b/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.Realtime.cs deleted file mode 100644 index 3ebe7b9cb..000000000 --- a/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.Realtime.cs +++ /dev/null @@ -1,182 +0,0 @@ -using System.Net.WebSockets; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Ultravox -{ - /// - /// Real-time audio methods for UltravoxClient - /// - public partial class UltravoxClient - { - /// - /// Gets the capabilities of the Ultravox real-time audio system. - /// - public Task GetRealtimeCapabilitiesAsync(CancellationToken cancellationToken = default) - { - var capabilities = new RealtimeCapabilities - { - SupportedInputFormats = new List - { - RealtimeAudioFormat.PCM16_8kHz, - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.G711_ULAW, - RealtimeAudioFormat.G711_ALAW - }, - SupportedOutputFormats = new List - { - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.G711_ULAW - }, - MaxSessionDurationSeconds = 86400, // 24 hours - SupportsFunctionCalling = true, - SupportsInterruptions = true, - TurnDetection = new TurnDetectionCapabilities - { - SupportedTypes = new List { TurnDetectionType.ServerVAD }, - MinSilenceThresholdMs = 20, - MaxSilenceThresholdMs = 200, - SupportsCustomParameters = true - } - }; - - return Task.FromResult(capabilities); - } - - /// - /// Creates a new real-time session with Ultravox. - /// - public async Task CreateSessionAsync( - RealtimeSessionConfig config, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - var effectiveApiKey = apiKey ?? PrimaryKeyCredential.ApiKey; - if (string.IsNullOrEmpty(effectiveApiKey)) - { - throw new InvalidOperationException("API key is required for Ultravox"); - } - - try - { - // Create WebSocket connection - var wsBaseUrl = Provider.BaseUrl?.Replace("https://", "wss://").Replace("http://", "ws://") ?? DEFAULT_WS_BASE_URL; - var wsUri = new Uri($"{wsBaseUrl}/realtime?model={Uri.EscapeDataString(config.Model ?? ProviderModelId)}"); - var clientWebSocket = new ClientWebSocket(); - clientWebSocket.Options.SetRequestHeader("Authorization", $"Bearer {effectiveApiKey}"); - clientWebSocket.Options.SetRequestHeader("User-Agent", "ConduitLLM/1.0"); - - await clientWebSocket.ConnectAsync(wsUri, cancellationToken); - - var session = new UltravoxRealtimeSession( - clientWebSocket, - _translator, - Logger, - config); - - // Send initial configuration - await session.ConfigureAsync(config, cancellationToken); - - return session; - } - catch (Exception ex) - { - Logger.LogError(ex, "Failed to create Ultravox real-time session"); - throw new LLMCommunicationException("Failed to establish connection with Ultravox", ex); - } - } - - /// - /// Streams audio bidirectionally with Ultravox. - /// - public IAsyncDuplexStream StreamAudioAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - if (session is not UltravoxRealtimeSession ultravoxSession) - { - throw new ArgumentException("Session must be created by UltravoxClient", nameof(session)); - } - - return ultravoxSession.CreateDuplexStream(); - } - - /// - /// Updates the configuration of an active real-time session. - /// - public Task UpdateSessionAsync( - RealtimeSession session, - RealtimeSessionUpdate updates, - CancellationToken cancellationToken = default) - { - if (session is not UltravoxRealtimeSession ultravoxSession) - { - return Task.FromException( - new ArgumentException("Session must be created by UltravoxClient", nameof(session))); - } - - // Ultravox may support some session updates - // For now, we'll throw not supported - return Task.FromException( - new NotSupportedException("Ultravox does not currently support session updates.")); - } - - /// - /// Closes a real-time session. - /// - public async Task CloseSessionAsync( - RealtimeSession session, - CancellationToken cancellationToken = default) - { - if (session is not UltravoxRealtimeSession ultravoxSession) - { - throw new ArgumentException("Session must be created by UltravoxClient", nameof(session)); - } - - await ultravoxSession.CloseAsync(cancellationToken); - } - - /// - /// Checks if the client supports real-time audio conversations. - /// - public async Task SupportsRealtimeAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // Ultravox specializes in real-time audio - return await Task.FromResult(true); - } - - /// - /// Gets the capabilities of the real-time audio system. - /// - public Task GetCapabilitiesAsync(CancellationToken cancellationToken = default) - { - var capabilities = new RealtimeCapabilities - { - SupportedInputFormats = new List - { - RealtimeAudioFormat.PCM16_8kHz, - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.G711_ULAW, - RealtimeAudioFormat.G711_ALAW - }, - SupportedOutputFormats = new List - { - RealtimeAudioFormat.PCM16_16kHz, - RealtimeAudioFormat.G711_ULAW - }, - SupportsInterruptions = true, - SupportsFunctionCalling = false, - MaxSessionDurationSeconds = 3600, // 1 hour - SupportedLanguages = new List { "en", "es", "fr", "de", "it", "pt", "zh", "ja", "ko" } - }; - - return Task.FromResult(capabilities); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.cs b/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.cs deleted file mode 100644 index 28bb4e57a..000000000 --- a/ConduitLLM.Providers/Providers/Ultravox/UltravoxClient.cs +++ /dev/null @@ -1,201 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Providers.Common.Models; -using ConduitLLM.Providers.Translators; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Ultravox -{ - /// - /// Client implementation for Ultravox real-time voice AI. - /// - /// - /// Ultravox provides low-latency voice AI capabilities optimized for - /// conversational applications including telephone systems. - /// - public partial class UltravoxClient : BaseLLMClient, ILLMClient, IRealtimeAudioClient - { - private const string DEFAULT_BASE_URL = "https://api.ultravox.ai/v1"; - private const string DEFAULT_WS_BASE_URL = "wss://api.ultravox.ai/v1"; - private readonly IRealtimeMessageTranslator _translator; - - /// - /// Initializes a new instance of the class. - /// - public UltravoxClient( - Provider provider, - ProviderKeyCredential keyCredential, - string providerModelId, - ILogger logger, - IHttpClientFactory? httpClientFactory = null, - ProviderDefaultModels? defaultModels = null) - : base(provider, keyCredential, providerModelId, logger, httpClientFactory, "Ultravox", defaultModels) - { - var translatorLogger = logger as ILogger - ?? Microsoft.Extensions.Logging.Abstractions.NullLoggerFactory.Instance.CreateLogger(); - _translator = new UltravoxRealtimeTranslator(translatorLogger); - } - - /// - /// Sends a chat completion request to Ultravox. - /// - public override Task CreateChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // Ultravox is primarily a real-time voice AI provider - // For text chat, we can use their REST API if available - return Task.FromException( - new NotSupportedException("Ultravox does not support text-based chat completion. Use real-time audio instead.")); - } - - /// - /// Streams chat completion responses from Ultravox. - /// - public override IAsyncEnumerable StreamChatCompletionAsync( - ChatCompletionRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - throw new NotSupportedException("Ultravox does not support streaming text chat. Use real-time audio instead."); - } - - - /// - /// Verifies Ultravox authentication by calling the accounts/me endpoint. - /// This is a free API call that validates the API key. - /// - public override async Task VerifyAuthenticationAsync( - string? apiKey = null, - string? baseUrl = null, - CancellationToken cancellationToken = default) - { - try - { - var startTime = DateTime.UtcNow; - var effectiveApiKey = !string.IsNullOrWhiteSpace(apiKey) ? apiKey : PrimaryKeyCredential.ApiKey; - - if (string.IsNullOrWhiteSpace(effectiveApiKey)) - { - return Core.Interfaces.AuthenticationResult.Failure("API key is required"); - } - - using var client = CreateHttpClient(effectiveApiKey); - - // Update base URL to the API endpoint - client.BaseAddress = new Uri("https://api.ultravox.ai/api/"); - - // Use the accounts/me endpoint which is free and validates the API key - var request = new HttpRequestMessage(HttpMethod.Get, "accounts/me"); - request.Headers.Remove("Authorization"); - request.Headers.Add("X-API-Key", effectiveApiKey); - - var response = await client.SendAsync(request, cancellationToken); - var responseTime = (DateTime.UtcNow - startTime).TotalMilliseconds; - - if (response.IsSuccessStatusCode) - { - return Core.Interfaces.AuthenticationResult.Success($"Response time: {responseTime:F0}ms"); - } - - // Check for specific error codes - if (response.StatusCode == System.Net.HttpStatusCode.Unauthorized) - { - return Core.Interfaces.AuthenticationResult.Failure("Invalid API key"); - } - - if (response.StatusCode == System.Net.HttpStatusCode.Forbidden) - { - return Core.Interfaces.AuthenticationResult.Failure("Access denied. Check your API key permissions"); - } - - var errorContent = await response.Content.ReadAsStringAsync(cancellationToken); - return Core.Interfaces.AuthenticationResult.Failure( - $"Ultravox authentication failed: {response.StatusCode}", - errorContent); - } - catch (HttpRequestException ex) - { - return Core.Interfaces.AuthenticationResult.Failure( - $"Network error during authentication: {ex.Message}", - ex.ToString()); - } - catch (TaskCanceledException) - { - return Core.Interfaces.AuthenticationResult.Failure("Authentication request timed out"); - } - catch (Exception ex) - { - Logger.LogError(ex, "Unexpected error during Ultravox authentication verification"); - return Core.Interfaces.AuthenticationResult.Failure( - $"Authentication verification failed: {ex.Message}", - ex.ToString()); - } - } - - /// - /// Gets available models from Ultravox. - /// - public override async Task> GetModelsAsync( - string? apiKey = null, - CancellationToken cancellationToken = default) - { - // Ultravox models are typically accessed via their real-time API - // Return a static list of known models - return await Task.FromResult(new List - { - new ExtendedModelInfo - { - Id = "ultravox-v1", - OwnedBy = "ultravox", - ProviderName = "Ultravox", - Capabilities = new ConduitLLM.Providers.Common.Models.ModelCapabilities - { - RealtimeAudio = true, - SupportedAudioOperations = new List { AudioOperation.Realtime } - } - }, - new ExtendedModelInfo - { - Id = "ultravox-telephony", - OwnedBy = "ultravox", - ProviderName = "Ultravox", - Capabilities = new ConduitLLM.Providers.Common.Models.ModelCapabilities - { - RealtimeAudio = true, - SupportedAudioOperations = new List { AudioOperation.Realtime } - } - } - }); - } - - /// - /// Creates an image from Ultravox. - /// - public override Task CreateImageAsync( - ImageGenerationRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return Task.FromException( - new NotSupportedException("Ultravox does not support image generation. Use real-time audio instead.")); - } - - /// - /// Creates embeddings from Ultravox. - /// - public override Task CreateEmbeddingAsync( - EmbeddingRequest request, - string? apiKey = null, - CancellationToken cancellationToken = default) - { - return Task.FromException( - new NotSupportedException("Ultravox does not support text embeddings. Use real-time audio instead.")); - } - - } -} diff --git a/ConduitLLM.Providers/Providers/Ultravox/UltravoxRealtimeSession.cs b/ConduitLLM.Providers/Providers/Ultravox/UltravoxRealtimeSession.cs deleted file mode 100644 index f97e7ff67..000000000 --- a/ConduitLLM.Providers/Providers/Ultravox/UltravoxRealtimeSession.cs +++ /dev/null @@ -1,254 +0,0 @@ -using System.Net.WebSockets; -using Microsoft.Extensions.Logging; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.Common.Models; - -namespace ConduitLLM.Providers.Ultravox -{ - /// - /// Ultravox-specific implementation of a real-time session. - /// - internal class UltravoxRealtimeSession : RealtimeSession - { - private readonly IRealtimeMessageTranslator _translator; - private readonly ILogger _logger; - private readonly RealtimeSessionConfig _config; - private readonly ClientWebSocket _webSocket; - - public UltravoxRealtimeSession( - ClientWebSocket webSocket, - IRealtimeMessageTranslator translator, - ILogger logger, - RealtimeSessionConfig config) - : base() - { - _webSocket = webSocket ?? throw new ArgumentNullException(nameof(webSocket)); - _translator = translator ?? throw new ArgumentNullException(nameof(translator)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _config = config ?? throw new ArgumentNullException(nameof(config)); - Config = config; - Provider = "Ultravox"; - } - - /// - /// Configures the Ultravox session with initial parameters. - /// - public async Task ConfigureAsync(RealtimeSessionConfig config, CancellationToken cancellationToken) - { - var configMessage = new ProviderRealtimeMessage - { - Type = "session.configure", - Data = new Dictionary - { - ["voice"] = config.Voice ?? "default", - ["language"] = config.Language ?? "en-US", - ["input_format"] = config.InputFormat.ToString().ToLower(), - ["output_format"] = config.OutputFormat.ToString().ToLower(), - ["vad_enabled"] = config.TurnDetection?.Type == TurnDetectionType.ServerVAD, - ["interruption_enabled"] = config.TurnDetection?.Enabled ?? true, - ["system_prompt"] = config.SystemPrompt ?? "You are a helpful AI assistant." - } - }; - - await SendMessageAsync(configMessage, cancellationToken); - } - - /// - /// Sends a message through the Ultravox session. - /// - public async Task SendMessageAsync(ProviderRealtimeMessage message, CancellationToken cancellationToken = default) - { - if (_webSocket?.State != WebSocketState.Open) - throw new InvalidOperationException("WebSocket is not open"); - - // Convert to a concrete RealtimeMessage type for translator - var realtimeMessage = new RealtimeAudioFrame - { - SessionId = message.SessionId, - Timestamp = message.Timestamp - }; - - var jsonMessage = await _translator.TranslateToProviderAsync(realtimeMessage); - var buffer = System.Text.Encoding.UTF8.GetBytes(jsonMessage); - await _webSocket.SendAsync(new ArraySegment(buffer), WebSocketMessageType.Text, true, cancellationToken); - } - - /// - /// Receives messages from the Ultravox session. - /// - public async IAsyncEnumerable ReceiveMessagesAsync( - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - var bufferArray = new byte[4096]; - var buffer = new ArraySegment(bufferArray); - - while (!cancellationToken.IsCancellationRequested && _webSocket?.State == WebSocketState.Open) - { - ProviderRealtimeMessage? messageToYield = null; - bool shouldBreak = false; - - try - { - var result = await _webSocket.ReceiveAsync(buffer, cancellationToken); - if (result.MessageType == WebSocketMessageType.Text) - { - var json = System.Text.Encoding.UTF8.GetString(bufferArray, buffer.Offset, result.Count); - - messageToYield = new ProviderRealtimeMessage - { - Type = "message", - Data = new Dictionary { ["raw"] = json }, - Timestamp = DateTime.UtcNow - }; - } - else if (result.MessageType == WebSocketMessageType.Close) - { - shouldBreak = true; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error receiving message from Ultravox"); - messageToYield = new ProviderRealtimeMessage - { - Type = "error", - Data = new Dictionary - { - ["error"] = "Receive error", - ["details"] = ex.Message - }, - Timestamp = DateTime.UtcNow - }; - shouldBreak = true; - } - - if (messageToYield != null) - { - yield return messageToYield; - } - - if (shouldBreak) - { - break; - } - } - } - - /// - /// Creates a duplex stream for bidirectional communication. - /// - public IAsyncDuplexStream CreateDuplexStream() - { - return new RealtimeDuplexStream(this); - } - - /// - /// Closes the real-time session. - /// - public async Task CloseAsync(CancellationToken cancellationToken = default) - { - if (_webSocket?.State == WebSocketState.Open) - { - await _webSocket.CloseAsync(WebSocketCloseStatus.NormalClosure, "Session closed", cancellationToken); - } - State = SessionState.Closed; - } - - /// - /// Duplex stream implementation for Ultravox. - /// - private class RealtimeDuplexStream : IAsyncDuplexStream - { - private readonly UltravoxRealtimeSession _session; - - public RealtimeDuplexStream(UltravoxRealtimeSession session) - { - _session = session ?? throw new ArgumentNullException(nameof(session)); - } - - public bool IsConnected => _session.State == SessionState.Connected; - - public async ValueTask SendAsync(RealtimeAudioFrame item, CancellationToken cancellationToken = default) - { - var message = new ProviderRealtimeMessage - { - Type = "audio", - Data = new Dictionary - { - ["audio"] = Convert.ToBase64String(item.AudioData), - ["timestamp"] = item.Timestamp - } - }; - await _session.SendMessageAsync(message, cancellationToken); - } - - public async IAsyncEnumerable ReceiveAsync( - [System.Runtime.CompilerServices.EnumeratorCancellation] CancellationToken cancellationToken = default) - { - await foreach (var message in _session.ReceiveMessagesAsync(cancellationToken)) - { - var response = new RealtimeResponse - { - SessionId = message.SessionId, - Timestamp = message.Timestamp, - EventType = RealtimeEventType.AudioDelta - }; - - // Map Ultravox message types to RealtimeResponse - switch (message.Type?.ToLower()) - { - case "audio": - response.Audio = new AudioDelta - { - Data = message.Data?.ContainsKey("audio") == true - ? Convert.FromBase64String(message.Data["audio"].ToString()!) - : Array.Empty(), - IsComplete = false - }; - response.EventType = RealtimeEventType.AudioDelta; - break; - - case "transcript": - response.Transcription = new TranscriptionDelta - { - Text = message.Data?.ContainsKey("text") == true - ? message.Data["text"].ToString() ?? string.Empty - : string.Empty, - IsFinal = message.Data?.ContainsKey("is_final") == true - && bool.Parse(message.Data["is_final"].ToString()!), - Role = "assistant" - }; - response.EventType = RealtimeEventType.TranscriptionDelta; - break; - - case "error": - response.Error = new ErrorInfo - { - Code = message.Data?.ContainsKey("code") == true - ? message.Data["code"].ToString() ?? "UNKNOWN" - : "UNKNOWN", - Message = message.Data?.ContainsKey("message") == true - ? message.Data["message"].ToString() ?? "Unknown error" - : "Unknown error" - }; - response.EventType = RealtimeEventType.Error; - break; - } - - yield return response; - } - } - - public async ValueTask CompleteAsync() - { - await _session.CloseAsync(CancellationToken.None); - } - - public void Dispose() - { - // Cleanup handled by session - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Providers/Translators/ElevenLabsRealtimeTranslator.cs b/ConduitLLM.Providers/Translators/ElevenLabsRealtimeTranslator.cs deleted file mode 100644 index ec31dc4a3..000000000 --- a/ConduitLLM.Providers/Translators/ElevenLabsRealtimeTranslator.cs +++ /dev/null @@ -1,440 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Translators -{ - /// - /// Translates messages between Conduit's unified format and ElevenLabs Conversational AI format. - /// - /// - /// ElevenLabs Conversational AI provides real-time voice interactions with - /// focus on high-quality voice synthesis and natural conversation flow. - /// - public class ElevenLabsRealtimeTranslator : IRealtimeMessageTranslator - { - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - public string Provider => "ElevenLabs"; - - public ElevenLabsRealtimeTranslator(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } - }; - } - - public async Task TranslateToProviderAsync(RealtimeMessage message) - { - // Map Conduit messages to ElevenLabs format - object elevenLabsMessage = message switch - { - RealtimeAudioFrame audioFrame => new - { - type = "audio_input", - audio = new - { - data = Convert.ToBase64String(audioFrame.AudioData), - format = "pcm", - sample_rate = 16000, - channels = 1 - } - }, - - RealtimeTextInput textInput => new - { - type = "text_input", - text = textInput.Text, - metadata = new - { - role = "user" - } - }, - - RealtimeFunctionResponse funcResponse => new - { - type = "tool_response", - tool_call_id = funcResponse.CallId, - output = funcResponse.Output - }, - - RealtimeResponseRequest responseRequest => new - { - type = "generate_response", - config = new - { - instructions = responseRequest.Instructions, - temperature = responseRequest.Temperature ?? 0.8, - voice_settings = new - { - stability = 0.5, - similarity_boost = 0.75 - } - } - }, - - _ => throw new NotSupportedException($"Message type '{message.GetType().Name}' is not supported by ElevenLabs") - }; - - var json = JsonSerializer.Serialize(elevenLabsMessage, _jsonOptions); - _logger.LogDebug("Translated to ElevenLabs: {MessageType} -> {Json}", message.GetType().Name, json); - - return await Task.FromResult(json); - } - - public async Task> TranslateFromProviderAsync(string providerMessage) - { - var messages = new List(); - - try - { - using var doc = JsonDocument.Parse(providerMessage); - var root = doc.RootElement; - - if (!root.TryGetProperty("type", out var typeElement)) - { - throw new InvalidOperationException("ElevenLabs message missing 'type' field"); - } - - var messageType = typeElement.GetString(); - _logger.LogDebug("Translating from ElevenLabs: {MessageType}", messageType); - - switch (messageType) - { - case "conversation_started": - case "conversation_updated": - messages.Add(new RealtimeStatusMessage - { - Status = messageType.Replace("conversation_", "session_"), - Details = providerMessage - }); - break; - - case "audio_output": - if (root.TryGetProperty("audio", out var audio) && - audio.TryGetProperty("data", out var audioData)) - { - var audioBytes = Convert.FromBase64String(audioData.GetString() ?? ""); - messages.Add(new RealtimeAudioFrame - { - AudioData = audioBytes, - IsOutput = true - }); - } - break; - - case "text_output": - if (root.TryGetProperty("text", out var text)) - { - messages.Add(new RealtimeTextOutput - { - Text = text.GetString() ?? "", - IsDelta = root.TryGetProperty("is_partial", out var partial) && partial.GetBoolean() - }); - } - break; - - case "tool_call": - messages.Add(new RealtimeFunctionCall - { - CallId = root.GetProperty("tool_call_id").GetString() ?? "", - Name = root.GetProperty("tool_name").GetString(), - Arguments = root.GetProperty("arguments").GetRawText(), - IsDelta = false - }); - break; - - case "turn_complete": - messages.Add(new RealtimeStatusMessage - { - Status = "response_complete" - }); - - // ElevenLabs includes metrics in turn_complete - if (root.TryGetProperty("metrics", out var metrics)) - { - _logger.LogDebug("ElevenLabs metrics: {Metrics}", metrics.GetRawText()); - - // Extract character count for cost estimation - if (metrics.TryGetProperty("characters_synthesized", out var chars)) - { - // Store this for usage tracking - messages.Add(new RealtimeStatusMessage - { - Status = "usage_update", - Details = JsonSerializer.Serialize(new - { - characters = chars.GetInt32(), - duration_ms = metrics.TryGetProperty("duration_ms", out var duration) ? duration.GetInt32() : 0 - }) - }); - } - } - break; - - case "error": - var error = ParseError(root); - messages.Add(new RealtimeErrorMessage - { - Error = error - }); - break; - - case "interruption": - messages.Add(new RealtimeStatusMessage - { - Status = "interrupted", - Details = providerMessage - }); - break; - - default: - _logger.LogWarning("Unknown ElevenLabs message type: {Type}", messageType); - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing ElevenLabs message: {Message}", providerMessage); - throw new InvalidOperationException("Failed to parse ElevenLabs realtime message", ex); - } - - return await Task.FromResult(messages); - } - - public async Task ValidateSessionConfigAsync(RealtimeSessionConfig config) - { - var result = new TranslationValidationResult { IsValid = true }; - - // Validate model/agent - var supportedAgents = new[] { "conversational-v1", "rachel", "sam", "charlie" }; - if (!string.IsNullOrEmpty(config.Model) && !supportedAgents.Contains(config.Model)) - { - result.Warnings.Add($"Agent '{config.Model}' may not be available. Known agents: {string.Join(", ", supportedAgents)}"); - } - - // Validate voice - var supportedVoices = new[] { "rachel", "sam", "charlie", "emily", "adam", "elli", "josh" }; - if (!string.IsNullOrEmpty(config.Voice) && !supportedVoices.Contains(config.Voice.ToLowerInvariant())) - { - result.Warnings.Add($"Voice '{config.Voice}' may not be available. Known voices: {string.Join(", ", supportedVoices)}"); - } - - // Validate audio formats - var supportedFormats = new[] { RealtimeAudioFormat.PCM16_16kHz }; - if (!supportedFormats.Contains(config.InputFormat)) - { - result.Errors.Add($"Input format '{config.InputFormat}' is not supported by ElevenLabs. Use PCM16 at 16kHz."); - result.IsValid = false; - } - - // ElevenLabs specific requirements - if (config.TurnDetection.Enabled) - { - result.Warnings.Add("ElevenLabs handles turn detection automatically based on voice activity"); - } - - return await Task.FromResult(result); - } - - public async Task TransformSessionConfigAsync(RealtimeSessionConfig config) - { - var elevenLabsConfig = new - { - type = "conversation_config", - config = new - { - agent_id = config.Model ?? "conversational-v1", - voice_id = MapVoiceId(config.Voice), - system_prompt = config.SystemPrompt, - language = config.Language ?? "en", - voice_settings = new - { - stability = 0.5, - similarity_boost = 0.75, - style = 0.0, - use_speaker_boost = true - }, - generation_config = new - { - temperature = config.Temperature ?? 0.8, - response_format = "audio", // or "text" or "both" - enable_ssml = false - }, - audio_config = new - { - input_format = "pcm_16000", - output_format = "pcm_16000", - encoding = "pcm_s16le" - }, - interruption_config = new - { - enabled = true, - threshold_ms = 500 - } - } - }; - - // Add tools/functions if configured - if (config.Tools != null && config.Tools.Count() > 0) - { - var tools = config.Tools.Select(t => new - { - name = t.Function?.Name, - description = t.Function?.Description, - parameters = t.Function?.Parameters - }).ToList(); - - ((dynamic)elevenLabsConfig.config).tools = tools; - } - - return await Task.FromResult(JsonSerializer.Serialize(elevenLabsConfig, _jsonOptions)); - } - - public string? GetRequiredSubprotocol() - { - return null; // ElevenLabs doesn't require a specific subprotocol - } - - public async Task> GetConnectionHeadersAsync(RealtimeSessionConfig config) - { - var headers = new Dictionary - { - ["X-ElevenLabs-Version"] = "v1", - ["X-Client-Info"] = "conduit-llm/1.0" - }; - - return await Task.FromResult(headers); - } - - public async Task> GetInitializationMessagesAsync(RealtimeSessionConfig config) - { - var messages = new List(); - - // Send configuration - var sessionConfig = await TransformSessionConfigAsync(config); - messages.Add(sessionConfig); - - // Start the conversation - messages.Add(JsonSerializer.Serialize(new - { - type = "conversation_start" - }, _jsonOptions)); - - return messages; - } - - public RealtimeError TranslateError(string providerError) - { - try - { - using var doc = JsonDocument.Parse(providerError); - var root = doc.RootElement; - - string? code = null; - string? message = null; - - if (root.TryGetProperty("error", out var errorElement)) - { - code = errorElement.TryGetProperty("code", out var codeElem) ? codeElem.GetString() : null; - message = errorElement.TryGetProperty("message", out var msgElem) ? msgElem.GetString() : null; - } - else if (root.TryGetProperty("code", out var codeElem)) - { - code = codeElem.GetString(); - message = root.TryGetProperty("message", out var msgElem) ? msgElem.GetString() : providerError; - } - - return new RealtimeError - { - Code = code ?? "unknown", - Message = message ?? "Unknown error", - Severity = DetermineErrorSeverity(code), - IsTerminal = IsTerminalError(code), - RetryAfterMs = code == "rate_limit_exceeded" ? 60000 : null - }; - } - catch - { - // If we can't parse it, treat as generic error - } - - return new RealtimeError - { - Code = "provider_error", - Message = providerError, - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false - }; - } - - private string MapVoiceId(string? voiceName) - { - if (string.IsNullOrEmpty(voiceName)) - return "21m00Tcm4TlvDq8ikWAM"; // Default Rachel voice ID - - // Map common voice names to ElevenLabs voice IDs - return voiceName.ToLowerInvariant() switch - { - "rachel" => "21m00Tcm4TlvDq8ikWAM", - "sam" => "yoZ06aMxZJJ28mfd3POQ", - "charlie" => "IKne3meq5aSn9XLyUdCD", - "emily" => "LcfcDJNUP1GQjkzn1xUU", - "adam" => "pNInz6obpgDQGcFmaJgB", - "elli" => "MF3mGyEYCl7XYWbV9V6O", - "josh" => "TxGEqnHWrfWFTfGW9XjX", - _ => "21m00Tcm4TlvDq8ikWAM" // Default to Rachel - }; - } - - private RealtimeError ParseError(JsonElement root) - { - var error = root.TryGetProperty("error", out var errorElem) ? errorElem : root; - - return new RealtimeError - { - Code = error.TryGetProperty("code", out var code) ? code.GetString() ?? "unknown" : "unknown", - Message = error.TryGetProperty("message", out var msg) ? msg.GetString() ?? "Unknown error" : "Unknown error", - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false, - Details = error.TryGetProperty("details", out var details) ? - JsonSerializer.Deserialize>(details.GetRawText()) : null - }; - } - - private Core.Interfaces.ErrorSeverity DetermineErrorSeverity(string? code) - { - return code switch - { - "invalid_request" => Core.Interfaces.ErrorSeverity.Error, - "authentication_error" => Core.Interfaces.ErrorSeverity.Critical, - "rate_limit_exceeded" => Core.Interfaces.ErrorSeverity.Warning, - "server_error" => Core.Interfaces.ErrorSeverity.Critical, - "voice_not_found" => Core.Interfaces.ErrorSeverity.Error, - _ => Core.Interfaces.ErrorSeverity.Error - }; - } - - private bool IsTerminalError(string? code) - { - return code switch - { - "authentication_error" => true, - "invalid_api_key" => true, - "subscription_expired" => true, - "quota_exceeded" => true, - _ => false - }; - } - } -} diff --git a/ConduitLLM.Providers/Translators/OpenAIRealtimeTranslatorV2.cs b/ConduitLLM.Providers/Translators/OpenAIRealtimeTranslatorV2.cs deleted file mode 100644 index a84a9e2bb..000000000 --- a/ConduitLLM.Providers/Translators/OpenAIRealtimeTranslatorV2.cs +++ /dev/null @@ -1,409 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Translators -{ - /// - /// Translates messages between Conduit's unified format and OpenAI's Realtime API format. - /// Simplified version that works with the actual model structure. - /// - public class OpenAIRealtimeTranslatorV2 : IRealtimeMessageTranslator - { - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - public string Provider => "OpenAI"; - - public OpenAIRealtimeTranslatorV2(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.SnakeCaseLower) } - }; - } - - public async Task TranslateToProviderAsync(RealtimeMessage message) - { - // Map common Conduit message types to OpenAI format - object openAiMessage = message switch - { - RealtimeAudioFrame audioFrame => new - { - type = "input_audio_buffer.append", - audio = Convert.ToBase64String(audioFrame.AudioData) - }, - - RealtimeTextInput textInput => new - { - type = "conversation.item.create", - item = new - { - type = "message", - role = "user", - content = new[] - { - new { type = "input_text", text = textInput.Text } - } - } - }, - - RealtimeFunctionResponse funcResponse => new - { - type = "conversation.item.create", - item = new - { - type = "function_call_output", - call_id = funcResponse.CallId, - output = funcResponse.Output - } - }, - - RealtimeResponseRequest responseRequest => new - { - type = "response.create", - response = new - { - modalities = new[] { "text", "audio" }, - instructions = responseRequest.Instructions, - temperature = responseRequest.Temperature - } - }, - - _ => throw new NotSupportedException($"Message type '{message.GetType().Name}' is not supported") - }; - - var json = JsonSerializer.Serialize(openAiMessage, _jsonOptions); - _logger.LogDebug("Translated to OpenAI: {MessageType} -> {Json}", message.GetType().Name, json); - - return await Task.FromResult(json); - } - - public async Task> TranslateFromProviderAsync(string providerMessage) - { - var messages = new List(); - - try - { - using var doc = JsonDocument.Parse(providerMessage); - var root = doc.RootElement; - - if (!root.TryGetProperty("type", out var typeElement)) - { - throw new InvalidOperationException("OpenAI message missing 'type' field"); - } - - var messageType = typeElement.GetString(); - _logger.LogDebug("Translating from OpenAI: {MessageType}", messageType); - - switch (messageType) - { - case "session.created": - case "session.updated": - // Session events - could map to status messages - messages.Add(new RealtimeStatusMessage - { - Status = "session_updated", - Details = providerMessage - }); - break; - - case "response.audio.delta": - // Audio chunk from AI - if (root.TryGetProperty("delta", out var audioDelta)) - { - var audioData = Convert.FromBase64String(audioDelta.GetString() ?? ""); - messages.Add(new RealtimeAudioFrame - { - AudioData = audioData, - IsOutput = true - }); - } - break; - - case "response.text.delta": - // Text chunk from AI - if (root.TryGetProperty("delta", out var textDelta)) - { - messages.Add(new RealtimeTextOutput - { - Text = textDelta.GetString() ?? "", - IsDelta = true - }); - } - break; - - case "response.function_call_arguments.delta": - // Function call in progress - if (root.TryGetProperty("call_id", out var callId) && - root.TryGetProperty("delta", out var argsDelta)) - { - messages.Add(new RealtimeFunctionCall - { - CallId = callId.GetString() ?? "", - Arguments = argsDelta.GetString() ?? "", - IsDelta = true - }); - } - break; - - case "response.done": - // Response completed - messages.Add(new RealtimeStatusMessage - { - Status = "response_complete" - }); - break; - - case "error": - // Error from provider - var error = ParseError(root); - messages.Add(new RealtimeErrorMessage - { - Error = error - }); - break; - - default: - _logger.LogWarning("Unknown OpenAI message type: {Type}", messageType); - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing OpenAI message: {Message}", providerMessage); - throw new InvalidOperationException("Failed to parse OpenAI realtime message", ex); - } - - return await Task.FromResult(messages); - } - - public async Task ValidateSessionConfigAsync(RealtimeSessionConfig config) - { - var result = new TranslationValidationResult { IsValid = true }; - - // Validate model - var supportedModels = new[] { "gpt-4o-realtime-preview", "gpt-4o-realtime-preview-2024-10-01" }; - if (!string.IsNullOrEmpty(config.Model) && !supportedModels.Contains(config.Model)) - { - result.Errors.Add($"Model '{config.Model}' is not supported. Use: {string.Join(", ", supportedModels)}"); - result.IsValid = false; - } - - // Validate voice - var supportedVoices = new[] { "alloy", "echo", "shimmer" }; - if (!string.IsNullOrEmpty(config.Voice) && !supportedVoices.Contains(config.Voice)) - { - result.Warnings.Add($"Voice '{config.Voice}' may not be supported. Recommended: {string.Join(", ", supportedVoices)}"); - } - - // Validate audio formats - var supportedFormats = new[] { RealtimeAudioFormat.PCM16_16kHz, RealtimeAudioFormat.PCM16_24kHz, RealtimeAudioFormat.G711_ULAW }; - if (!supportedFormats.Contains(config.InputFormat)) - { - result.Errors.Add($"Input format '{config.InputFormat}' is not supported by OpenAI"); - result.IsValid = false; - } - - return await Task.FromResult(result); - } - - public async Task TransformSessionConfigAsync(RealtimeSessionConfig config) - { - var openAiConfig = new - { - type = "session.update", - session = new - { - model = config.Model ?? "gpt-4o-realtime-preview", - voice = config.Voice ?? "alloy", - instructions = config.SystemPrompt, - input_audio_format = MapAudioFormat(config.InputFormat), - output_audio_format = MapAudioFormat(config.OutputFormat), - input_audio_transcription = config.Transcription?.EnableUserTranscription == true ? new - { - model = "whisper-1" - } : null, - turn_detection = config.TurnDetection.Enabled ? new - { - type = config.TurnDetection.Type.ToString().ToLowerInvariant(), - threshold = config.TurnDetection.Threshold, - prefix_padding_ms = config.TurnDetection.PrefixPaddingMs, - silence_duration_ms = config.TurnDetection.SilenceThresholdMs - } : null, - temperature = config.Temperature, - modalities = new[] { "text", "audio" } - } - }; - - return await Task.FromResult(JsonSerializer.Serialize(openAiConfig, _jsonOptions)); - } - - public string? GetRequiredSubprotocol() - { - return "openai-beta.realtime-v1"; - } - - public async Task> GetConnectionHeadersAsync(RealtimeSessionConfig config) - { - var headers = new Dictionary - { - ["OpenAI-Beta"] = "realtime=v1" - }; - - return await Task.FromResult(headers); - } - - public async Task> GetInitializationMessagesAsync(RealtimeSessionConfig config) - { - var messages = new List(); - - // Send session configuration as first message - var sessionConfig = await TransformSessionConfigAsync(config); - messages.Add(sessionConfig); - - return messages; - } - - public RealtimeError TranslateError(string providerError) - { - try - { - using var doc = JsonDocument.Parse(providerError); - var root = doc.RootElement; - - if (root.TryGetProperty("error", out var errorElement)) - { - var code = errorElement.TryGetProperty("code", out var codeElem) ? codeElem.GetString() : "unknown"; - var message = errorElement.TryGetProperty("message", out var msgElem) ? msgElem.GetString() : providerError; - - return new RealtimeError - { - Code = code ?? "unknown", - Message = message ?? "Unknown error", - Severity = DetermineErrorSeverity(code), - IsTerminal = IsTerminalError(code) - }; - } - } - catch - { - // If we can't parse it, treat as generic error - } - - return new RealtimeError - { - Code = "provider_error", - Message = providerError, - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false - }; - } - - private string MapAudioFormat(RealtimeAudioFormat format) - { - return format switch - { - RealtimeAudioFormat.PCM16_16kHz => "pcm16", - RealtimeAudioFormat.PCM16_24kHz => "pcm16", - RealtimeAudioFormat.G711_ULAW => "g711_ulaw", - RealtimeAudioFormat.G711_ALAW => "g711_alaw", - _ => "pcm16" // Default - }; - } - - private RealtimeError ParseError(JsonElement root) - { - var error = root.GetProperty("error"); - - return new RealtimeError - { - Code = error.TryGetProperty("code", out var code) ? code.GetString() ?? "unknown" : "unknown", - Message = error.TryGetProperty("message", out var msg) ? msg.GetString() ?? "Unknown error" : "Unknown error", - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false - }; - } - - private Core.Interfaces.ErrorSeverity DetermineErrorSeverity(string? code) - { - return code switch - { - "invalid_request_error" => Core.Interfaces.ErrorSeverity.Error, - "server_error" => Core.Interfaces.ErrorSeverity.Critical, - "rate_limit_error" => Core.Interfaces.ErrorSeverity.Warning, - _ => Core.Interfaces.ErrorSeverity.Error - }; - } - - private bool IsTerminalError(string? code) - { - return code switch - { - "invalid_api_key" => true, - "insufficient_quota" => true, - "server_error" => false, - "rate_limit_error" => false, - _ => false - }; - } - } - - // Additional message types used by the translator - public class RealtimeTextInput : RealtimeMessage - { - public override string Type => "text_input"; - public string Text { get; set; } = ""; - } - - public class RealtimeTextOutput : RealtimeMessage - { - public override string Type => "text_output"; - public string Text { get; set; } = ""; - public bool IsDelta { get; set; } - } - - public class RealtimeFunctionCall : RealtimeMessage - { - public override string Type => "function_call"; - public string CallId { get; set; } = ""; - public string? Name { get; set; } - public string Arguments { get; set; } = ""; - public bool IsDelta { get; set; } - } - - public class RealtimeFunctionResponse : RealtimeMessage - { - public override string Type => "function_response"; - public string CallId { get; set; } = ""; - public string Output { get; set; } = ""; - } - - public class RealtimeResponseRequest : RealtimeMessage - { - public override string Type => "response_request"; - public string? Instructions { get; set; } - public double? Temperature { get; set; } - } - - public class RealtimeStatusMessage : RealtimeMessage - { - public override string Type => "status"; - public string Status { get; set; } = ""; - public string? Details { get; set; } - } - - public class RealtimeErrorMessage : RealtimeMessage - { - public override string Type => "error"; - public RealtimeError Error { get; set; } = new(); - } -} diff --git a/ConduitLLM.Providers/Translators/RealtimeMessageTranslatorFactory.cs b/ConduitLLM.Providers/Translators/RealtimeMessageTranslatorFactory.cs deleted file mode 100644 index d9643bb79..000000000 --- a/ConduitLLM.Providers/Translators/RealtimeMessageTranslatorFactory.cs +++ /dev/null @@ -1,124 +0,0 @@ -using System.Collections.Concurrent; - -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Translators -{ - /// - /// Factory for creating and managing real-time message translators. - /// - public class RealtimeMessageTranslatorFactory : IRealtimeMessageTranslatorFactory - { - private readonly IServiceProvider _serviceProvider; - private readonly ILogger _logger; - private readonly ConcurrentDictionary _translators = new(); - - public RealtimeMessageTranslatorFactory( - IServiceProvider serviceProvider, - ILogger logger) - { - _serviceProvider = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - public IRealtimeMessageTranslator? GetTranslator(string provider) - { - if (string.IsNullOrEmpty(provider)) - { - _logger.LogWarning("Provider name is null or empty"); - return null; - } - - // Normalize provider name - var normalizedProvider = provider.ToLowerInvariant(); - - return _translators.GetOrAdd(normalizedProvider, key => - { - // TODO: This should be data-driven from database configuration - // Provider-to-translator mappings should be registered dynamically - // based on provider configuration, not hardcoded - IRealtimeMessageTranslator? translator = key switch - { - "openai" => CreateTranslator(), - "ultravox" => CreateTranslator(), - "elevenlabs" => CreateTranslator(), - _ => null - }; - - if (translator == null) - { - _logger.LogWarning("No translator found for provider: {Provider}", provider); - } - else - { - _logger.LogInformation("Created translator for provider: {Provider}", provider); - } - - return translator!; - }); - } - - public bool HasTranslator(string provider) - { - if (string.IsNullOrEmpty(provider)) - return false; - - var normalizedProvider = provider.ToLowerInvariant(); - - return normalizedProvider switch - { - "openai" => true, - "ultravox" => true, - "elevenlabs" => true, - _ => false - }; - } - - public void RegisterTranslator(string provider, IRealtimeMessageTranslator translator) - { - if (string.IsNullOrEmpty(provider)) - throw new ArgumentException("Provider name cannot be null or empty", nameof(provider)); - - ArgumentNullException.ThrowIfNull(translator); - - var normalizedProvider = provider.ToLowerInvariant(); - _translators[normalizedProvider] = translator; - - _logger.LogInformation("Registered translator for provider: {Provider}", provider); - } - - public string[] GetRegisteredProviders() - { - // Return built-in providers plus any dynamically registered ones - var builtInProviders = new[] { "openai", "ultravox", "elevenlabs" }; - var registeredProviders = _translators.Keys.ToArray(); - - return builtInProviders.Union(registeredProviders).Distinct().ToArray(); - } - - private T? CreateTranslator() where T : class, IRealtimeMessageTranslator - { - try - { - // Try to get from DI container first - var translator = _serviceProvider.GetService(); - if (translator != null) - return translator; - - // Fall back to creating with logger - var loggerType = typeof(ILogger<>).MakeGenericType(typeof(T)); - var logger = _serviceProvider.GetRequiredService(loggerType); - - return Activator.CreateInstance(typeof(T), logger) as T; - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to create translator of type {Type}", typeof(T).Name); - return null; - } - } - } -} diff --git a/ConduitLLM.Providers/Translators/UltravoxRealtimeTranslator.cs b/ConduitLLM.Providers/Translators/UltravoxRealtimeTranslator.cs deleted file mode 100644 index e068c12f2..000000000 --- a/ConduitLLM.Providers/Translators/UltravoxRealtimeTranslator.cs +++ /dev/null @@ -1,384 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Serialization; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using Microsoft.Extensions.Logging; - -namespace ConduitLLM.Providers.Translators -{ - /// - /// Translates messages between Conduit's unified format and Ultravox's real-time API format. - /// - /// - /// Ultravox uses a different message structure than OpenAI, with focus on - /// low-latency voice interactions and streamlined message types. - /// - public class UltravoxRealtimeTranslator : IRealtimeMessageTranslator - { - private readonly ILogger _logger; - private readonly JsonSerializerOptions _jsonOptions; - - public string Provider => "Ultravox"; - - public UltravoxRealtimeTranslator(ILogger logger) - { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - - _jsonOptions = new JsonSerializerOptions - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull, - Converters = { new JsonStringEnumConverter(JsonNamingPolicy.CamelCase) } - }; - } - - public async Task TranslateToProviderAsync(RealtimeMessage message) - { - // Map Conduit messages to Ultravox format - object ultravoxMessage = message switch - { - RealtimeAudioFrame audioFrame => new - { - type = "audio", - data = new - { - audio = Convert.ToBase64String(audioFrame.AudioData), - sampleRate = 24000, // Default to 24kHz - channels = 1 - } - }, - - RealtimeTextInput textInput => new - { - type = "text", - data = new - { - text = textInput.Text, - role = "user" - } - }, - - RealtimeFunctionResponse funcResponse => new - { - type = "function_result", - data = new - { - callId = funcResponse.CallId, - result = funcResponse.Output - } - }, - - RealtimeResponseRequest responseRequest => new - { - type = "generate", - data = new - { - prompt = responseRequest.Instructions, - temperature = responseRequest.Temperature ?? 0.7, - maxTokens = 4096 - } - }, - - _ => throw new NotSupportedException($"Message type '{message.GetType().Name}' is not supported by Ultravox") - }; - - var json = JsonSerializer.Serialize(ultravoxMessage, _jsonOptions); - _logger.LogDebug("Translated to Ultravox: {MessageType} -> {Json}", message.GetType().Name, json); - - return await Task.FromResult(json); - } - - public async Task> TranslateFromProviderAsync(string providerMessage) - { - var messages = new List(); - - try - { - using var doc = JsonDocument.Parse(providerMessage); - var root = doc.RootElement; - - if (!root.TryGetProperty("type", out var typeElement)) - { - throw new InvalidOperationException("Ultravox message missing 'type' field"); - } - - var messageType = typeElement.GetString(); - _logger.LogDebug("Translating from Ultravox: {MessageType}", messageType); - - switch (messageType) - { - case "session_started": - case "session_updated": - messages.Add(new RealtimeStatusMessage - { - Status = messageType, - Details = providerMessage - }); - break; - - case "audio_chunk": - if (root.TryGetProperty("data", out var audioData) && - audioData.TryGetProperty("audio", out var audioBase64)) - { - var audioBytes = Convert.FromBase64String(audioBase64.GetString() ?? ""); - messages.Add(new RealtimeAudioFrame - { - AudioData = audioBytes, - IsOutput = true - }); - } - break; - - case "text_chunk": - if (root.TryGetProperty("data", out var textData) && - textData.TryGetProperty("text", out var text)) - { - messages.Add(new RealtimeTextOutput - { - Text = text.GetString() ?? "", - IsDelta = true - }); - } - break; - - case "function_call": - if (root.TryGetProperty("data", out var funcData)) - { - messages.Add(new RealtimeFunctionCall - { - CallId = funcData.GetProperty("callId").GetString() ?? "", - Name = funcData.GetProperty("name").GetString(), - Arguments = funcData.GetProperty("arguments").GetRawText(), - IsDelta = false - }); - } - break; - - case "generation_complete": - messages.Add(new RealtimeStatusMessage - { - Status = "response_complete" - }); - - // Check for usage stats - if (root.TryGetProperty("usage", out var usage)) - { - // Ultravox may provide usage differently - _logger.LogDebug("Ultravox usage data: {Usage}", usage.GetRawText()); - } - break; - - case "error": - var error = ParseError(root); - messages.Add(new RealtimeErrorMessage - { - Error = error - }); - break; - - default: - _logger.LogWarning("Unknown Ultravox message type: {Type}", messageType); - break; - } - } - catch (Exception ex) - { - _logger.LogError(ex, "Error parsing Ultravox message: {Message}", providerMessage); - throw new InvalidOperationException("Failed to parse Ultravox realtime message", ex); - } - - return await Task.FromResult(messages); - } - - public async Task ValidateSessionConfigAsync(RealtimeSessionConfig config) - { - var result = new TranslationValidationResult { IsValid = true }; - - // Validate model - var supportedModels = new[] { "ultravox", "ultravox-v2", "ultravox-realtime" }; - if (!string.IsNullOrEmpty(config.Model) && !supportedModels.Contains(config.Model)) - { - result.Errors.Add($"Model '{config.Model}' is not supported. Use: {string.Join(", ", supportedModels)}"); - result.IsValid = false; - } - - // Validate audio formats - var supportedFormats = new[] { RealtimeAudioFormat.PCM16_16kHz, RealtimeAudioFormat.PCM16_24kHz }; - if (!supportedFormats.Contains(config.InputFormat)) - { - result.Errors.Add($"Input format '{config.InputFormat}' is not supported by Ultravox"); - result.IsValid = false; - } - - // Ultravox specific validations - if (config.TurnDetection.Enabled && config.TurnDetection.Type != TurnDetectionType.ServerVAD) - { - result.Warnings.Add("Ultravox only supports server-side VAD turn detection"); - } - - return await Task.FromResult(result); - } - - public async Task TransformSessionConfigAsync(RealtimeSessionConfig config) - { - var ultravoxConfig = new - { - type = "session_config", - data = new - { - model = config.Model ?? "ultravox-v2", - systemPrompt = config.SystemPrompt, - audioConfig = new - { - inputFormat = MapAudioFormat(config.InputFormat), - outputFormat = MapAudioFormat(config.OutputFormat), - sampleRate = GetSampleRate(config.InputFormat), - channels = 1 // Ultravox typically uses mono - }, - turnDetection = config.TurnDetection.Enabled ? new - { - enabled = true, - vadThreshold = config.TurnDetection.Threshold, - silenceDurationMs = config.TurnDetection.SilenceThresholdMs - } : null, - responseConfig = new - { - temperature = config.Temperature, - voice = config.Voice ?? "nova", // Ultravox default voice - speed = 1.0 - } - } - }; - - return await Task.FromResult(JsonSerializer.Serialize(ultravoxConfig, _jsonOptions)); - } - - public string? GetRequiredSubprotocol() - { - return "ultravox.v1"; - } - - public async Task> GetConnectionHeadersAsync(RealtimeSessionConfig config) - { - var headers = new Dictionary - { - ["X-Ultravox-Version"] = "1.0", - ["X-Ultravox-Client"] = "conduit-llm" - }; - - return await Task.FromResult(headers); - } - - public async Task> GetInitializationMessagesAsync(RealtimeSessionConfig config) - { - var messages = new List(); - - // Send session configuration - var sessionConfig = await TransformSessionConfigAsync(config); - messages.Add(sessionConfig); - - // Ultravox starts immediately without additional messages - - return messages; - } - - public RealtimeError TranslateError(string providerError) - { - try - { - using var doc = JsonDocument.Parse(providerError); - var root = doc.RootElement; - - if (root.TryGetProperty("error", out var errorElement) || - root.TryGetProperty("data", out errorElement)) - { - var code = errorElement.TryGetProperty("code", out var codeElem) ? codeElem.GetString() : "unknown"; - var message = errorElement.TryGetProperty("message", out var msgElem) ? msgElem.GetString() : providerError; - - return new RealtimeError - { - Code = code ?? "unknown", - Message = message ?? "Unknown error", - Severity = DetermineErrorSeverity(code), - IsTerminal = IsTerminalError(code) - }; - } - } - catch - { - // If we can't parse it, treat as generic error - } - - return new RealtimeError - { - Code = "provider_error", - Message = providerError, - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false - }; - } - - private string MapAudioFormat(RealtimeAudioFormat format) - { - return format switch - { - RealtimeAudioFormat.PCM16_16kHz => "pcm16", - RealtimeAudioFormat.PCM16_24kHz => "pcm16", - RealtimeAudioFormat.G711_ULAW => "ulaw", - RealtimeAudioFormat.G711_ALAW => "alaw", - _ => "pcm16" // Default - }; - } - - private int GetSampleRate(RealtimeAudioFormat format) - { - return format switch - { - RealtimeAudioFormat.PCM16_16kHz => 16000, - RealtimeAudioFormat.PCM16_24kHz => 24000, - RealtimeAudioFormat.G711_ULAW => 8000, - RealtimeAudioFormat.G711_ALAW => 8000, - _ => 24000 // Default - }; - } - - private RealtimeError ParseError(JsonElement root) - { - var errorData = root.TryGetProperty("data", out var data) ? data : root.GetProperty("error"); - - return new RealtimeError - { - Code = errorData.TryGetProperty("code", out var code) ? code.GetString() ?? "unknown" : "unknown", - Message = errorData.TryGetProperty("message", out var msg) ? msg.GetString() ?? "Unknown error" : "Unknown error", - Severity = Core.Interfaces.ErrorSeverity.Error, - IsTerminal = false - }; - } - - private Core.Interfaces.ErrorSeverity DetermineErrorSeverity(string? code) - { - return code switch - { - "invalid_request" => Core.Interfaces.ErrorSeverity.Error, - "server_error" => Core.Interfaces.ErrorSeverity.Critical, - "rate_limit" => Core.Interfaces.ErrorSeverity.Warning, - "authentication_failed" => Core.Interfaces.ErrorSeverity.Critical, - _ => Core.Interfaces.ErrorSeverity.Error - }; - } - - private bool IsTerminalError(string? code) - { - return code switch - { - "authentication_failed" => true, - "invalid_api_key" => true, - "quota_exceeded" => true, - "model_not_available" => true, - _ => false - }; - } - } -} diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs index 92c7526ca..112b3cfb2 100644 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs +++ b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Mapping.cs @@ -21,18 +21,12 @@ public void Should_Map_Entity_To_ModelCapabilitiesDto_Correctly() SupportsVision = true, SupportsFunctionCalling = true, SupportsStreaming = true, - SupportsAudioTranscription = false, - SupportsTextToSpeech = true, - SupportsRealtimeAudio = false, SupportsImageGeneration = false, SupportsVideoGeneration = false, SupportsEmbeddings = false, MaxTokens = 128000, MinTokens = 1, TokenizerType = TokenizerType.Cl100KBase, - SupportedVoices = "alloy,echo,fable,onyx,nova,shimmer", - SupportedLanguages = "en,es,fr,de,it,pt,ru,zh,ja,ko", - SupportedFormats = "text,json,json_object" }; // Act - simulate the mapping logic from controller @@ -45,18 +39,12 @@ public void Should_Map_Entity_To_ModelCapabilitiesDto_Correctly() dto.SupportsVision.Should().Be(entity.SupportsVision); dto.SupportsFunctionCalling.Should().Be(entity.SupportsFunctionCalling); dto.SupportsStreaming.Should().Be(entity.SupportsStreaming); - dto.SupportsAudioTranscription.Should().Be(entity.SupportsAudioTranscription); - dto.SupportsTextToSpeech.Should().Be(entity.SupportsTextToSpeech); - dto.SupportsRealtimeAudio.Should().Be(entity.SupportsRealtimeAudio); dto.SupportsImageGeneration.Should().Be(entity.SupportsImageGeneration); dto.SupportsVideoGeneration.Should().Be(entity.SupportsVideoGeneration); dto.SupportsEmbeddings.Should().Be(entity.SupportsEmbeddings); dto.MaxTokens.Should().Be(entity.MaxTokens); dto.MinTokens.Should().Be(entity.MinTokens); dto.TokenizerType.Should().Be(entity.TokenizerType); - dto.SupportedVoices.Should().Be(entity.SupportedVoices); - dto.SupportedLanguages.Should().Be(entity.SupportedLanguages); - dto.SupportedFormats.Should().Be(entity.SupportedFormats); } [Fact] @@ -69,18 +57,12 @@ public void Should_Map_CreateCapabilitiesDto_To_Entity() SupportsVision = false, SupportsFunctionCalling = true, SupportsStreaming = true, - SupportsAudioTranscription = false, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, SupportsImageGeneration = false, SupportsVideoGeneration = false, SupportsEmbeddings = false, MaxTokens = 4096, MinTokens = 1, TokenizerType = TokenizerType.P50KBase, - SupportedVoices = null, - SupportedLanguages = "en", - SupportedFormats = "text" }; // Act - simulate controller logic @@ -90,18 +72,12 @@ public void Should_Map_CreateCapabilitiesDto_To_Entity() SupportsVision = createDto.SupportsVision, SupportsFunctionCalling = createDto.SupportsFunctionCalling, SupportsStreaming = createDto.SupportsStreaming, - SupportsAudioTranscription = createDto.SupportsAudioTranscription, - SupportsTextToSpeech = createDto.SupportsTextToSpeech, - SupportsRealtimeAudio = createDto.SupportsRealtimeAudio, SupportsImageGeneration = createDto.SupportsImageGeneration, SupportsVideoGeneration = createDto.SupportsVideoGeneration, SupportsEmbeddings = createDto.SupportsEmbeddings, MaxTokens = createDto.MaxTokens, MinTokens = createDto.MinTokens, TokenizerType = createDto.TokenizerType, - SupportedVoices = createDto.SupportedVoices, - SupportedLanguages = createDto.SupportedLanguages, - SupportedFormats = createDto.SupportedFormats }; // Assert @@ -112,9 +88,6 @@ public void Should_Map_CreateCapabilitiesDto_To_Entity() entity.MaxTokens.Should().Be(4096); entity.MinTokens.Should().Be(1); entity.TokenizerType.Should().Be(TokenizerType.P50KBase); - entity.SupportedVoices.Should().BeNull(); - entity.SupportedLanguages.Should().Be("en"); - entity.SupportedFormats.Should().Be("text"); } [Fact] @@ -128,18 +101,12 @@ public void Should_Apply_UpdateCapabilitiesDto_To_Entity_With_Partial_Updates() SupportsVision = false, SupportsFunctionCalling = false, SupportsStreaming = true, - SupportsAudioTranscription = false, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, SupportsImageGeneration = false, SupportsVideoGeneration = false, SupportsEmbeddings = false, MaxTokens = 4096, MinTokens = 1, TokenizerType = TokenizerType.P50KBase, - SupportedVoices = "alloy", - SupportedLanguages = "en", - SupportedFormats = "text" }; var updateDto = new UpdateCapabilitiesDto @@ -152,9 +119,6 @@ public void Should_Apply_UpdateCapabilitiesDto_To_Entity_With_Partial_Updates() MaxTokens = 128000, // Increase max tokens MinTokens = null, // Don't update TokenizerType = TokenizerType.Cl100KBase, // Update tokenizer - SupportedVoices = null, // Don't update - SupportedLanguages = "en,es,fr", // Add languages - SupportedFormats = null // Don't update }; // Act - simulate controller update logic @@ -172,12 +136,6 @@ public void Should_Apply_UpdateCapabilitiesDto_To_Entity_With_Partial_Updates() existingEntity.MinTokens = updateDto.MinTokens.Value; if (updateDto.TokenizerType.HasValue) existingEntity.TokenizerType = updateDto.TokenizerType.Value; - if (updateDto.SupportedVoices != null) - existingEntity.SupportedVoices = updateDto.SupportedVoices; - if (updateDto.SupportedLanguages != null) - existingEntity.SupportedLanguages = updateDto.SupportedLanguages; - if (updateDto.SupportedFormats != null) - existingEntity.SupportedFormats = updateDto.SupportedFormats; // Assert existingEntity.SupportsChat.Should().BeTrue(); // Unchanged @@ -187,9 +145,6 @@ public void Should_Apply_UpdateCapabilitiesDto_To_Entity_With_Partial_Updates() existingEntity.MaxTokens.Should().Be(128000); // Updated existingEntity.MinTokens.Should().Be(1); // Unchanged existingEntity.TokenizerType.Should().Be(TokenizerType.Cl100KBase); // Updated - existingEntity.SupportedVoices.Should().Be("alloy"); // Unchanged - existingEntity.SupportedLanguages.Should().Be("en,es,fr"); // Updated - existingEntity.SupportedFormats.Should().Be("text"); // Unchanged } [Fact] @@ -199,9 +154,6 @@ public void Should_Handle_Clearing_String_Properties_With_Empty_String() var existingEntity = new ConduitLLM.Configuration.Entities.ModelCapabilities { Id = 1, - SupportedVoices = "alloy,echo,fable", - SupportedLanguages = "en,es,fr", - SupportedFormats = "text,json", MaxTokens = 4096, MinTokens = 1, TokenizerType = TokenizerType.BPE @@ -210,23 +162,11 @@ public void Should_Handle_Clearing_String_Properties_With_Empty_String() var updateDto = new UpdateCapabilitiesDto { Id = 1, - SupportedVoices = "", // Clear voices - SupportedLanguages = "", // Clear languages - SupportedFormats = "" // Clear formats }; // Act - simulate controller update logic - if (updateDto.SupportedVoices != null) - existingEntity.SupportedVoices = updateDto.SupportedVoices; - if (updateDto.SupportedLanguages != null) - existingEntity.SupportedLanguages = updateDto.SupportedLanguages; - if (updateDto.SupportedFormats != null) - existingEntity.SupportedFormats = updateDto.SupportedFormats; // Assert - existingEntity.SupportedVoices.Should().BeEmpty(); - existingEntity.SupportedLanguages.Should().BeEmpty(); - existingEntity.SupportedFormats.Should().BeEmpty(); } [Fact] @@ -236,9 +176,6 @@ public void Should_Not_Update_String_Properties_When_Null() var existingEntity = new ConduitLLM.Configuration.Entities.ModelCapabilities { Id = 1, - SupportedVoices = "alloy,echo,fable", - SupportedLanguages = "en,es,fr", - SupportedFormats = "text,json", MaxTokens = 4096, MinTokens = 1, TokenizerType = TokenizerType.BPE @@ -247,23 +184,11 @@ public void Should_Not_Update_String_Properties_When_Null() var updateDto = new UpdateCapabilitiesDto { Id = 1, - SupportedVoices = null, // Don't update - SupportedLanguages = null, // Don't update - SupportedFormats = null // Don't update }; // Act - simulate controller update logic - if (updateDto.SupportedVoices != null) - existingEntity.SupportedVoices = updateDto.SupportedVoices; - if (updateDto.SupportedLanguages != null) - existingEntity.SupportedLanguages = updateDto.SupportedLanguages; - if (updateDto.SupportedFormats != null) - existingEntity.SupportedFormats = updateDto.SupportedFormats; // Assert - values unchanged - existingEntity.SupportedVoices.Should().Be("alloy,echo,fable"); - existingEntity.SupportedLanguages.Should().Be("en,es,fr"); - existingEntity.SupportedFormats.Should().Be("text,json"); } [Fact] @@ -277,18 +202,12 @@ public void Should_Map_Entity_With_All_Capabilities_To_Dto() SupportsVision = true, SupportsFunctionCalling = true, SupportsStreaming = true, - SupportsAudioTranscription = true, - SupportsTextToSpeech = true, - SupportsRealtimeAudio = true, SupportsImageGeneration = true, SupportsVideoGeneration = true, SupportsEmbeddings = true, MaxTokens = int.MaxValue, MinTokens = 1, TokenizerType = TokenizerType.O200KBase, - SupportedVoices = "all-voices", - SupportedLanguages = "all-languages", - SupportedFormats = "all-formats" }; // Act @@ -299,9 +218,6 @@ public void Should_Map_Entity_With_All_Capabilities_To_Dto() dto.SupportsVision.Should().BeTrue(); dto.SupportsFunctionCalling.Should().BeTrue(); dto.SupportsStreaming.Should().BeTrue(); - dto.SupportsAudioTranscription.Should().BeTrue(); - dto.SupportsTextToSpeech.Should().BeTrue(); - dto.SupportsRealtimeAudio.Should().BeTrue(); dto.SupportsImageGeneration.Should().BeTrue(); dto.SupportsVideoGeneration.Should().BeTrue(); dto.SupportsEmbeddings.Should().BeTrue(); @@ -319,18 +235,12 @@ public void Should_Map_Entity_With_No_Capabilities_To_Dto() SupportsVision = false, SupportsFunctionCalling = false, SupportsStreaming = false, - SupportsAudioTranscription = false, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, SupportsImageGeneration = false, SupportsVideoGeneration = false, SupportsEmbeddings = false, MaxTokens = 0, MinTokens = 0, TokenizerType = TokenizerType.BPE, - SupportedVoices = null, - SupportedLanguages = null, - SupportedFormats = null }; // Act @@ -341,9 +251,6 @@ public void Should_Map_Entity_With_No_Capabilities_To_Dto() dto.SupportsVision.Should().BeFalse(); dto.SupportsFunctionCalling.Should().BeFalse(); dto.SupportsStreaming.Should().BeFalse(); - dto.SupportsAudioTranscription.Should().BeFalse(); - dto.SupportsTextToSpeech.Should().BeFalse(); - dto.SupportsRealtimeAudio.Should().BeFalse(); dto.SupportsImageGeneration.Should().BeFalse(); dto.SupportsVideoGeneration.Should().BeFalse(); dto.SupportsEmbeddings.Should().BeFalse(); @@ -385,18 +292,12 @@ private static ModelCapabilitiesDto MapEntityToDto(ConduitLLM.Configuration.Enti SupportsVision = entity.SupportsVision, SupportsFunctionCalling = entity.SupportsFunctionCalling, SupportsStreaming = entity.SupportsStreaming, - SupportsAudioTranscription = entity.SupportsAudioTranscription, - SupportsTextToSpeech = entity.SupportsTextToSpeech, - SupportsRealtimeAudio = entity.SupportsRealtimeAudio, SupportsImageGeneration = entity.SupportsImageGeneration, SupportsVideoGeneration = entity.SupportsVideoGeneration, SupportsEmbeddings = entity.SupportsEmbeddings, MaxTokens = entity.MaxTokens, MinTokens = entity.MinTokens, TokenizerType = entity.TokenizerType, - SupportedVoices = entity.SupportedVoices, - SupportedLanguages = entity.SupportedLanguages, - SupportedFormats = entity.SupportedFormats }; } } diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs index 1c8d81d44..981852e82 100644 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs +++ b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Serialization.cs @@ -23,18 +23,12 @@ public void ModelCapabilitiesDto_Should_Serialize_All_Boolean_Flags() SupportsVision = false, SupportsFunctionCalling = true, SupportsStreaming = false, - SupportsAudioTranscription = true, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = true, SupportsImageGeneration = false, SupportsVideoGeneration = true, SupportsEmbeddings = false, MaxTokens = 128000, MinTokens = 1, TokenizerType = TokenizerType.Cl100KBase, - SupportedVoices = "alloy,echo", - SupportedLanguages = "en,es,fr", - SupportedFormats = "text,json" }; // Act @@ -47,9 +41,6 @@ public void ModelCapabilitiesDto_Should_Serialize_All_Boolean_Flags() deserialized.SupportsVision.Should().BeFalse(); deserialized.SupportsFunctionCalling.Should().BeTrue(); deserialized.SupportsStreaming.Should().BeFalse(); - deserialized.SupportsAudioTranscription.Should().BeTrue(); - deserialized.SupportsTextToSpeech.Should().BeFalse(); - deserialized.SupportsRealtimeAudio.Should().BeTrue(); deserialized.SupportsImageGeneration.Should().BeFalse(); deserialized.SupportsVideoGeneration.Should().BeTrue(); deserialized.SupportsEmbeddings.Should().BeFalse(); @@ -101,9 +92,6 @@ public void CapabilitiesDto_Should_Handle_Null_String_Properties() MaxTokens = 4096, MinTokens = 1, TokenizerType = TokenizerType.BPE, - SupportedVoices = null, - SupportedLanguages = null, - SupportedFormats = null }; // Act @@ -112,9 +100,6 @@ public void CapabilitiesDto_Should_Handle_Null_String_Properties() // Assert deserialized.Should().NotBeNull(); - deserialized!.SupportedVoices.Should().BeNull(); - deserialized.SupportedLanguages.Should().BeNull(); - deserialized.SupportedFormats.Should().BeNull(); } [Fact] @@ -124,10 +109,6 @@ public void CapabilitiesDto_Should_Serialize_Comma_Separated_Values() var dto = new CapabilitiesDto { Id = 1, - SupportsTextToSpeech = true, - SupportedVoices = "alloy,echo,fable,onyx,nova,shimmer", - SupportedLanguages = "en,es,fr,de,it,pt,ru,zh,ja,ko", - SupportedFormats = "mp3,opus,aac,flac,wav,pcm", TokenizerType = TokenizerType.BPE, MaxTokens = 4096, MinTokens = 1 @@ -139,9 +120,6 @@ public void CapabilitiesDto_Should_Serialize_Comma_Separated_Values() // Assert deserialized.Should().NotBeNull(); - deserialized!.SupportedVoices.Should().Be("alloy,echo,fable,onyx,nova,shimmer"); - deserialized.SupportedLanguages.Should().Be("en,es,fr,de,it,pt,ru,zh,ja,ko"); - deserialized.SupportedFormats.Should().Be("mp3,opus,aac,flac,wav,pcm"); } [Fact] @@ -232,9 +210,6 @@ public void CapabilitiesDto_Should_Handle_Empty_String_Properties() var dto = new CapabilitiesDto { Id = 1, - SupportedVoices = "", - SupportedLanguages = "", - SupportedFormats = "", TokenizerType = TokenizerType.BPE, MaxTokens = 4096, MinTokens = 1 @@ -246,9 +221,6 @@ public void CapabilitiesDto_Should_Handle_Empty_String_Properties() // Assert deserialized.Should().NotBeNull(); - deserialized!.SupportedVoices.Should().BeEmpty(); - deserialized.SupportedLanguages.Should().BeEmpty(); - deserialized.SupportedFormats.Should().BeEmpty(); } [Fact] @@ -281,7 +253,6 @@ public void CapabilitiesDto_Should_Handle_Unicode_In_Supported_Languages() var dto = new CapabilitiesDto { Id = 1, - SupportedLanguages = "中文,日本語,한국어,العربية,עברית,руÑÑкий", TokenizerType = TokenizerType.BPE, MaxTokens = 4096, MinTokens = 1 @@ -293,7 +264,6 @@ public void CapabilitiesDto_Should_Handle_Unicode_In_Supported_Languages() // Assert deserialized.Should().NotBeNull(); - deserialized!.SupportedLanguages.Should().Be("中文,日本語,한국어,العربية,עברית,руÑÑкий"); } [Fact] diff --git a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs index f8f3abe73..35b2451a6 100644 --- a/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs +++ b/ConduitLLM.Tests/Admin/Models/ModelCapabilities/CapabilitiesDtoTests.Validation.cs @@ -21,18 +21,12 @@ public void CreateCapabilitiesDto_Should_Have_Default_Values() dto.SupportsVision.Should().BeFalse(); dto.SupportsFunctionCalling.Should().BeFalse(); dto.SupportsStreaming.Should().BeFalse(); - dto.SupportsAudioTranscription.Should().BeFalse(); - dto.SupportsTextToSpeech.Should().BeFalse(); - dto.SupportsRealtimeAudio.Should().BeFalse(); dto.SupportsImageGeneration.Should().BeFalse(); dto.SupportsVideoGeneration.Should().BeFalse(); dto.SupportsEmbeddings.Should().BeFalse(); dto.MaxTokens.Should().Be(0); dto.MinTokens.Should().Be(1); // Default should be 1 dto.TokenizerType.Should().Be(TokenizerType.Cl100KBase); // Default enum value is 0 - dto.SupportedVoices.Should().BeNull(); - dto.SupportedLanguages.Should().BeNull(); - dto.SupportedFormats.Should().BeNull(); } [Fact] @@ -98,9 +92,6 @@ public void UpdateCapabilitiesDto_Should_Allow_Partial_Updates() MaxTokens = 200000, // Update MinTokens = null, // Don't update TokenizerType = TokenizerType.O200KBase, // Update - SupportedVoices = null, // Don't update - SupportedLanguages = "en,es", // Update - SupportedFormats = null // Don't update }; // Assert - nulls mean "don't update" @@ -112,9 +103,6 @@ public void UpdateCapabilitiesDto_Should_Allow_Partial_Updates() dto.MaxTokens.Should().Be(200000); dto.MinTokens.Should().BeNull(); dto.TokenizerType.Should().Be(TokenizerType.O200KBase); - dto.SupportedVoices.Should().BeNull(); - dto.SupportedLanguages.Should().Be("en,es"); - dto.SupportedFormats.Should().BeNull(); } [Fact] @@ -128,18 +116,12 @@ public void UpdateCapabilitiesDto_Should_Allow_All_Null_For_No_Updates() SupportsVision = null, SupportsFunctionCalling = null, SupportsStreaming = null, - SupportsAudioTranscription = null, - SupportsTextToSpeech = null, - SupportsRealtimeAudio = null, SupportsImageGeneration = null, SupportsVideoGeneration = null, SupportsEmbeddings = null, MaxTokens = null, MinTokens = null, TokenizerType = null, - SupportedVoices = null, - SupportedLanguages = null, - SupportedFormats = null }; // Assert - all nulls is valid (no-op update) @@ -175,58 +157,7 @@ public void CapabilitiesDto_Should_Validate_Conflicting_Capabilities() isInconsistent.Should().BeTrue("Function calling typically requires chat support"); } - [Theory] - [InlineData("alloy")] - [InlineData("alloy,echo")] - [InlineData("alloy,echo,fable,onyx,nova,shimmer")] - [InlineData("voice1|voice2|voice3")] // Different separator - [InlineData("UPPERCASE,lowercase,MixedCase")] - [InlineData("")] // Empty - [InlineData(" ")] // Whitespace - public void CapabilitiesDto_Should_Accept_Various_Voice_Formats(string voices) - { - // Arrange & Act - var dto = new CapabilitiesDto - { - Id = 1, - SupportsTextToSpeech = true, - SupportedVoices = voices, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.BPE - }; - - // Assert - dto.SupportedVoices.Should().Be(voices); - } - - [Fact] - public void CapabilitiesDto_Should_Handle_Very_Long_Supported_Lists() - { - // Arrange - var longVoiceList = string.Join(",", new string[100].Select((_, i) => $"voice{i}")); - var longLanguageList = string.Join(",", new string[200].Select((_, i) => $"lang{i}")); - var longFormatList = string.Join(",", new string[50].Select((_, i) => $"format{i}")); - - var dto = new CapabilitiesDto - { - Id = 1, - SupportedVoices = longVoiceList, - SupportedLanguages = longLanguageList, - SupportedFormats = longFormatList, - MaxTokens = 4096, - MinTokens = 1, - TokenizerType = TokenizerType.BPE - }; - // Act & Assert - dto.SupportedVoices.Should().Contain("voice0"); - dto.SupportedVoices.Should().Contain("voice99"); - dto.SupportedLanguages.Should().Contain("lang0"); - dto.SupportedLanguages.Should().Contain("lang199"); - dto.SupportedFormats.Should().Contain("format0"); - dto.SupportedFormats.Should().Contain("format49"); - } [Fact] public void CreateCapabilitiesDto_Should_Default_MinTokens_To_One() @@ -277,25 +208,13 @@ public void CapabilitiesDto_Should_Validate_Audio_Model_Constraints() var dto = new CapabilitiesDto { Id = 1, - SupportsAudioTranscription = true, - SupportsTextToSpeech = true, - SupportsRealtimeAudio = true, - SupportedVoices = "alloy,echo,fable", - SupportedLanguages = "en,es,fr,de", - SupportedFormats = "mp3,opus,aac", MaxTokens = 0, // Audio models might not have token limits MinTokens = 0, TokenizerType = TokenizerType.BPE }; // Act & Assert - dto.SupportsAudioTranscription.Should().BeTrue(); - dto.SupportsTextToSpeech.Should().BeTrue(); - - // Business validation - TTS requires voices - var hasTTSWithVoices = dto.SupportsTextToSpeech && - !string.IsNullOrEmpty(dto.SupportedVoices); - hasTTSWithVoices.Should().BeTrue("TTS models should specify supported voices"); + dto.Should().NotBeNull(); } [Fact] @@ -305,16 +224,9 @@ public void UpdateCapabilitiesDto_Should_Clear_Lists_With_Empty_String() var dto = new UpdateCapabilitiesDto { Id = 1, - SupportedVoices = "", // Clear voices - SupportedLanguages = "", // Clear languages - SupportedFormats = "" // Clear formats }; // Act & Assert - dto.SupportedVoices.Should().BeEmpty(); - dto.SupportedLanguages.Should().BeEmpty(); - dto.SupportedFormats.Should().BeEmpty(); - dto.SupportedVoices.Should().NotBeNull("Empty string is different from null for updates"); } [Fact] @@ -328,9 +240,6 @@ public void CapabilitiesDto_Should_Handle_All_Capabilities_Enabled() SupportsVision = true, SupportsFunctionCalling = true, SupportsStreaming = true, - SupportsAudioTranscription = true, - SupportsTextToSpeech = true, - SupportsRealtimeAudio = true, SupportsImageGeneration = true, SupportsVideoGeneration = true, SupportsEmbeddings = true, @@ -346,9 +255,6 @@ public void CapabilitiesDto_Should_Handle_All_Capabilities_Enabled() dto.SupportsVision, dto.SupportsFunctionCalling, dto.SupportsStreaming, - dto.SupportsAudioTranscription, - dto.SupportsTextToSpeech, - dto.SupportsRealtimeAudio, dto.SupportsImageGeneration, dto.SupportsVideoGeneration, dto.SupportsEmbeddings diff --git a/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs b/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs index fc914f37a..8d280b752 100644 --- a/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs +++ b/ConduitLLM.Tests/Admin/Models/Models/ModelDtoTests.Mapping.cs @@ -31,9 +31,6 @@ public void Should_Map_Entity_To_ModelDto_Correctly() MaxTokens = 128000, MinTokens = 1, TokenizerType = TokenizerType.Cl100KBase, - SupportedVoices = "alloy,echo,fable", - SupportedLanguages = "en,es,fr,de,ja,zh", - SupportedFormats = "text,json" }, IsActive = true, CreatedAt = new DateTime(2024, 1, 15, 10, 30, 0, DateTimeKind.Utc), @@ -278,18 +275,12 @@ private static ModelCapabilitiesDto MapCapabilitiesToDto(ConduitLLM.Configuratio SupportsVision = capabilities.SupportsVision, SupportsFunctionCalling = capabilities.SupportsFunctionCalling, SupportsStreaming = capabilities.SupportsStreaming, - SupportsAudioTranscription = capabilities.SupportsAudioTranscription, - SupportsTextToSpeech = capabilities.SupportsTextToSpeech, - SupportsRealtimeAudio = capabilities.SupportsRealtimeAudio, SupportsImageGeneration = capabilities.SupportsImageGeneration, SupportsVideoGeneration = capabilities.SupportsVideoGeneration, SupportsEmbeddings = capabilities.SupportsEmbeddings, MaxTokens = capabilities.MaxTokens, MinTokens = capabilities.MinTokens, TokenizerType = capabilities.TokenizerType, - SupportedVoices = capabilities.SupportedVoices, - SupportedLanguages = capabilities.SupportedLanguages, - SupportedFormats = capabilities.SupportedFormats }; } diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByKey.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByKey.cs deleted file mode 100644 index 30f1f91c7..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByKey.cs +++ /dev/null @@ -1,78 +0,0 @@ -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region GetUsageByKeyAsync Tests - - [Fact] - public async Task GetUsageByKeyAsync_WithValidKey_ShouldReturnKeyUsage() - { - // Arrange - var virtualKey = "test-key-hash"; - var logs = CreateSampleAudioUsageLogs(10); - var key = new VirtualKey - { - KeyHash = virtualKey, - KeyName = "Test API Key" - }; - - _mockRepository.Setup(x => x.GetByVirtualKeyAsync(virtualKey, It.IsAny(), It.IsAny())) - .ReturnsAsync(logs); - _mockVirtualKeyRepository.Setup(x => x.GetByKeyHashAsync(virtualKey, It.IsAny())) - .ReturnsAsync(key); - _mockRepository.Setup(x => x.GetOperationBreakdownAsync(It.IsAny(), It.IsAny(), virtualKey)) - .ReturnsAsync(new List - { - new() { OperationType = "transcription", Count = 6, TotalCost = 3.0m }, - new() { OperationType = "tts", Count = 4, TotalCost = 2.0m } - }); - _mockRepository.Setup(x => x.GetProviderBreakdownAsync(It.IsAny(), It.IsAny(), virtualKey)) - .ReturnsAsync(new List - { - new() { ProviderId = 1, ProviderName = "OpenAI Test", Count = 10, TotalCost = 5.0m, SuccessRate = 100 } - }); - - // Act - var result = await _service.GetUsageByKeyAsync(virtualKey); - - // Assert - result.Should().NotBeNull(); - result.VirtualKey.Should().Be(virtualKey); - result.KeyName.Should().Be("Test API Key"); - result.TotalOperations.Should().Be(10); - result.TotalCost.Should().Be(logs.Sum(l => l.Cost)); - result.SuccessRate.Should().Be(90); // 9 out of 10 logs are successful (one has status 500) - } - - [Fact] - public async Task GetUsageByKeyAsync_WithDateRange_ShouldFilterResults() - { - // Arrange - var virtualKey = "test-key-hash"; - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - var logs = CreateSampleAudioUsageLogs(5); - - _mockRepository.Setup(x => x.GetByVirtualKeyAsync(virtualKey, startDate, endDate)) - .ReturnsAsync(logs); - _mockVirtualKeyRepository.Setup(x => x.GetByKeyHashAsync(virtualKey, It.IsAny())) - .ReturnsAsync((VirtualKey?)null); - - // Act - var result = await _service.GetUsageByKeyAsync(virtualKey, startDate, endDate); - - // Assert - result.TotalOperations.Should().Be(5); - result.KeyName.Should().BeEmpty(); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByProvider.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByProvider.cs deleted file mode 100644 index 0a1255818..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.ByProvider.cs +++ /dev/null @@ -1,62 +0,0 @@ -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region GetUsageByProviderAsync Tests - - [Fact] - public async Task GetUsageByProviderAsync_WithValidProvider_ShouldReturnProviderUsage() - { - // Arrange - var providerId = 1; - var logs = new List - { - CreateAudioUsageLog("transcription", "whisper-1", 200), - CreateAudioUsageLog("tts", "tts-1", 200), - CreateAudioUsageLog("realtime", "gpt-4o-realtime", 200), - CreateAudioUsageLog("transcription", "whisper-1", 500) // Failed request - }; - - _mockRepository.Setup(x => x.GetByProviderAsync(providerId, It.IsAny(), It.IsAny())) - .ReturnsAsync(logs); - - // Act - var result = await _service.GetUsageByProviderAsync(providerId); - - // Assert - result.Should().NotBeNull(); - result.ProviderId.Should().Be(providerId); - result.TotalOperations.Should().Be(4); - result.TranscriptionCount.Should().Be(2); - result.TextToSpeechCount.Should().Be(1); - result.RealtimeSessionCount.Should().Be(1); - result.SuccessRate.Should().Be(75); // 3 successful out of 4 - result.MostUsedModel.Should().Be("whisper-1"); - } - - [Fact] - public async Task GetUsageByProviderAsync_WithNoLogs_ShouldReturnZeroMetrics() - { - // Arrange - var providerId = 2; - _mockRepository.Setup(x => x.GetByProviderAsync(providerId, It.IsAny(), It.IsAny())) - .ReturnsAsync(new List()); - - // Act - var result = await _service.GetUsageByProviderAsync(providerId); - - // Assert - result.TotalOperations.Should().Be(0); - result.SuccessRate.Should().Be(0); - result.MostUsedModel.Should().BeNull(); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Cleanup.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Cleanup.cs deleted file mode 100644 index a4a601260..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Cleanup.cs +++ /dev/null @@ -1,33 +0,0 @@ -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region Cleanup Tests - - [Fact] - public async Task CleanupOldLogsAsync_ShouldDeleteOldLogs() - { - // Arrange - var retentionDays = 30; - var expectedCutoffDate = DateTime.UtcNow.AddDays(-retentionDays); - var deletedCount = 100; - - _mockRepository.Setup(x => x.DeleteOldLogsAsync(It.Is(d => - d.Date == expectedCutoffDate.Date))) - .ReturnsAsync(deletedCount); - - // Act - var result = await _service.CleanupOldLogsAsync(retentionDays); - - // Assert - result.Should().Be(deletedCount); - _mockRepository.Verify(x => x.DeleteOldLogsAsync(It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Export.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Export.cs deleted file mode 100644 index bef1ad49e..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Export.cs +++ /dev/null @@ -1,101 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region Export Tests - - [Fact] - public async Task ExportUsageDataAsync_AsCsv_ShouldReturnCsvData() - { - // Arrange - var query = new AudioUsageQueryDto { Page = 1, PageSize = 10 }; - var logs = CreateSampleAudioUsageLogs(3); - var pagedResult = new PagedResult - { - Items = logs, - TotalCount = 3, - Page = 1, - PageSize = int.MaxValue, - TotalPages = 1 - }; - - _mockRepository.Setup(x => x.GetPagedAsync(It.IsAny())) - .ReturnsAsync(pagedResult); - - // Act - var result = await _service.ExportUsageDataAsync(query, "csv"); - - // Assert - result.Should().NotBeNullOrEmpty(); - result.Should().Contain("Timestamp"); - result.Should().Contain("VirtualKey"); - result.Should().Contain("ProviderId"); - result.Should().Contain("1"); // Provider ID 1 in CSV - } - - [Fact] - public async Task ExportUsageDataAsync_AsJson_ShouldReturnJsonData() - { - // Arrange - var query = new AudioUsageQueryDto { Page = 1, PageSize = 10 }; - var logs = CreateSampleAudioUsageLogs(2); - var pagedResult = new PagedResult - { - Items = logs, - TotalCount = 2, - Page = 1, - PageSize = int.MaxValue, - TotalPages = 1 - }; - - _mockRepository.Setup(x => x.GetPagedAsync(It.IsAny())) - .ReturnsAsync(pagedResult); - - // Act - var result = await _service.ExportUsageDataAsync(query, "json"); - - // Assert - result.Should().NotBeNullOrEmpty(); - result.Should().Contain("\"virtualKey\""); - result.Should().Contain("\"providerId\""); - result.Should().Contain("\"providerId\": 1"); // Provider ID 1 in JSON (with space) - - // Should be valid JSON - var json = System.Text.Json.JsonDocument.Parse(result); - json.RootElement.ValueKind.Should().Be(System.Text.Json.JsonValueKind.Array); - } - - [Fact] - public async Task ExportUsageDataAsync_WithUnsupportedFormat_ShouldThrowException() - { - // Arrange - var query = new AudioUsageQueryDto { Page = 1, PageSize = 10 }; - var logs = CreateSampleAudioUsageLogs(3); - var pagedResult = new PagedResult - { - Items = logs, - TotalCount = 3, - Page = 1, - PageSize = int.MaxValue, - TotalPages = 1 - }; - - _mockRepository.Setup(x => x.GetPagedAsync(It.IsAny())) - .ReturnsAsync(pagedResult); - - // Act & Assert - await Assert.ThrowsAsync(() => - _service.ExportUsageDataAsync(query, "xml")); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.RealtimeSessions.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.RealtimeSessions.cs deleted file mode 100644 index 19bcf4d80..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.RealtimeSessions.cs +++ /dev/null @@ -1,157 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; - -using FluentAssertions; - -using Microsoft.Extensions.DependencyInjection; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region Realtime Session Tests - - [Fact] - public async Task GetRealtimeSessionMetricsAsync_WithActiveSessions_ShouldReturnMetrics() - { - // Arrange - var sessions = CreateSampleRealtimeSessions(5); - _mockSessionStore.Setup(x => x.GetActiveSessionsAsync(It.IsAny())) - .ReturnsAsync(sessions); - - // Act - var result = await _service.GetRealtimeSessionMetricsAsync(); - - // Assert - result.Should().NotBeNull(); - result.ActiveSessions.Should().Be(5); - result.SessionsByProvider.Should().ContainKey("openai"); - result.SessionsByProvider["openai"].Should().Be(3); - result.SessionsByProvider["ultravox"].Should().Be(2); - result.SuccessRate.Should().Be(80); // 4 successful out of 5 - result.AverageTurnsPerSession.Should().BeGreaterThan(0); - } - - [Fact] - public async Task GetRealtimeSessionMetricsAsync_WithNoSessionStore_ShouldReturnEmptyMetrics() - { - // Arrange - var mockScopedProvider = new Mock(); - mockScopedProvider.Setup(x => x.GetService(typeof(IRealtimeSessionStore))) - .Returns(null); - - var mockScope = new Mock(); - mockScope.Setup(x => x.ServiceProvider).Returns(mockScopedProvider.Object); - - var mockScopeFactory = new Mock(); - mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object); - - _mockServiceProvider.Setup(x => x.GetService(typeof(IServiceScopeFactory))) - .Returns(mockScopeFactory.Object); - - // Act - var result = await _service.GetRealtimeSessionMetricsAsync(); - - // Assert - result.ActiveSessions.Should().Be(0); - result.SessionsByProvider.Should().BeEmpty(); - result.SuccessRate.Should().Be(100); - } - - [Fact] - public async Task GetActiveSessionsAsync_ShouldReturnSessionDtos() - { - // Arrange - var sessions = CreateSampleRealtimeSessions(3); - _mockSessionStore.Setup(x => x.GetActiveSessionsAsync(It.IsAny())) - .ReturnsAsync(sessions); - - // Act - var result = await _service.GetActiveSessionsAsync(); - - // Assert - result.Should().HaveCount(3); - result.First().SessionId.Should().Be("session-1"); - result.First().ProviderId.Should().Be(18); // First session (i=0) uses provider ID 18 based on CreateSampleRealtimeSessions logic - result.First().State.Should().Be(SessionState.Connected.ToString()); - } - - [Fact] - public async Task GetSessionDetailsAsync_WithValidSessionId_ShouldReturnSession() - { - // Arrange - var sessionId = "session-123"; - var session = CreateRealtimeSession(sessionId, "openai"); - - _mockSessionStore.Setup(x => x.GetSessionAsync(sessionId, It.IsAny())) - .ReturnsAsync(session); - - // Act - var result = await _service.GetSessionDetailsAsync(sessionId); - - // Assert - result.Should().NotBeNull(); - result!.SessionId.Should().Be(sessionId); - result.ProviderId.Should().Be(1); // Provider ID 1 for OpenAI - } - - [Fact] - public async Task GetSessionDetailsAsync_WithInvalidSessionId_ShouldReturnNull() - { - // Arrange - var sessionId = "non-existent"; - _mockSessionStore.Setup(x => x.GetSessionAsync(sessionId, It.IsAny())) - .ReturnsAsync((RealtimeSession?)null); - - // Act - var result = await _service.GetSessionDetailsAsync(sessionId); - - // Assert - result.Should().BeNull(); - } - - [Fact] - public async Task TerminateSessionAsync_WithValidSession_ShouldTerminate() - { - // Arrange - var sessionId = "session-to-terminate"; - var session = CreateRealtimeSession(sessionId, "openai"); - - _mockSessionStore.Setup(x => x.GetSessionAsync(sessionId, It.IsAny())) - .ReturnsAsync(session); - _mockSessionStore.Setup(x => x.UpdateSessionAsync(It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - _mockSessionStore.Setup(x => x.RemoveSessionAsync(sessionId, It.IsAny())) - .ReturnsAsync(true); - - // Act - var result = await _service.TerminateSessionAsync(sessionId); - - // Assert - result.Should().BeTrue(); - _mockSessionStore.Verify(x => x.UpdateSessionAsync(It.Is(s => - s.State == SessionState.Closed), It.IsAny()), Times.Once); - _mockSessionStore.Verify(x => x.RemoveSessionAsync(sessionId, It.IsAny()), Times.Once); - } - - [Fact] - public async Task TerminateSessionAsync_WithNonExistentSession_ShouldReturnFalse() - { - // Arrange - var sessionId = "non-existent"; - _mockSessionStore.Setup(x => x.GetSessionAsync(sessionId, It.IsAny())) - .ReturnsAsync((RealtimeSession?)null); - - // Act - var result = await _service.TerminateSessionAsync(sessionId); - - // Assert - result.Should().BeFalse(); - _mockSessionStore.Verify(x => x.RemoveSessionAsync(It.IsAny(), It.IsAny()), Times.Never); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Setup.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Setup.cs deleted file mode 100644 index 0942da009..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Setup.cs +++ /dev/null @@ -1,149 +0,0 @@ -using ConduitLLM.Admin.Services; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - private readonly Mock _mockRepository; - private readonly Mock _mockVirtualKeyRepository; - private readonly Mock> _mockLogger; - private readonly Mock _mockServiceProvider; - private readonly Mock _mockSessionStore; - private readonly Mock _mockCostCalculationService; - private readonly AdminAudioUsageService _service; - private readonly ITestOutputHelper _output; - - public AdminAudioUsageServiceTests(ITestOutputHelper output) - { - _output = output; - _mockRepository = new Mock(); - _mockVirtualKeyRepository = new Mock(); - _mockLogger = new Mock>(); - _mockServiceProvider = new Mock(); - _mockSessionStore = new Mock(); - _mockCostCalculationService = new Mock(); - - // Setup service provider to return session store - var mockScope = new Mock(); - var mockScopeFactory = new Mock(); - var mockScopedProvider = new Mock(); - - mockScope.Setup(x => x.ServiceProvider).Returns(mockScopedProvider.Object); - mockScopeFactory.Setup(x => x.CreateScope()).Returns(mockScope.Object); - _mockServiceProvider.Setup(x => x.GetService(typeof(IServiceScopeFactory))).Returns(mockScopeFactory.Object); - mockScopedProvider.Setup(x => x.GetService(typeof(IRealtimeSessionStore))).Returns(_mockSessionStore.Object); - - _service = new AdminAudioUsageService( - _mockRepository.Object, - _mockVirtualKeyRepository.Object, - _mockLogger.Object, - _mockServiceProvider.Object, - _mockCostCalculationService.Object); - } - - #region Helper Methods - - private List CreateSampleAudioUsageLogs(int count) - { - var logs = new List(); - for (int i = 0; i < count; i++) - { - logs.Add(new AudioUsageLog - { - Id = i + 1, - VirtualKey = $"key-{i % 3}", - ProviderId = i % 2 == 0 ? 1 : 2, // Alternate between provider 1 and 2 - OperationType = i % 3 == 0 ? "transcription" : i % 3 == 1 ? "tts" : "realtime", - Model = i % 2 == 0 ? "whisper-1" : "tts-1", - RequestId = Guid.NewGuid().ToString(), - DurationSeconds = 10 + i, - Cost = 0.05m + (i * 0.01m), - StatusCode = i % 10 == 0 ? 500 : 200, - Timestamp = DateTime.UtcNow.AddHours(-i) - }); - } - return logs; - } - - private AudioUsageLog CreateAudioUsageLog(string operationType, string model, int statusCode) - { - return new AudioUsageLog - { - Id = 1, - VirtualKey = "test-key", - ProviderId = 1, // Provider ID 1 for OpenAI - OperationType = operationType, - Model = model, - RequestId = Guid.NewGuid().ToString(), - DurationSeconds = 5, - Cost = 0.10m, - StatusCode = statusCode, - Timestamp = DateTime.UtcNow - }; - } - - private List CreateSampleRealtimeSessions(int count) - { - var sessions = new List(); - for (int i = 0; i < count; i++) - { - sessions.Add(CreateRealtimeSession($"session-{i + 1}", i % 3 == 0 ? "ultravox" : "openai", i == 0)); - } - return sessions; - } - - private RealtimeSession CreateRealtimeSession(string sessionId, string provider, bool hasErrors = false) - { - var config = new RealtimeSessionConfig - { - Model = provider == "openai" ? "gpt-4o-realtime" : "ultravox-v0.2", - Voice = "alloy", - Language = "en-US" - }; - - // Map provider names to ProviderType IDs - var providerId = provider.ToLowerInvariant() switch - { - "openai" => 1, // ProviderType.OpenAI - "ultravox" => 18, // ProviderType.Ultravox - _ => 1 // Default to OpenAI - }; - - var session = new RealtimeSession - { - Id = sessionId, - Provider = provider, - Config = config, - State = SessionState.Connected, - CreatedAt = DateTime.UtcNow.AddMinutes(-30), - Metadata = new Dictionary - { - { "VirtualKey", "test-key-hash" }, - { "IpAddress", "192.168.1.1" }, - { "UserAgent", "Mozilla/5.0" }, - { "ProviderId", providerId } - } - }; - - session.Statistics.Duration = TimeSpan.FromMinutes(25); - session.Statistics.TurnCount = 10; - session.Statistics.InputTokens = 1000; - session.Statistics.OutputTokens = 2000; - session.Statistics.InputAudioDuration = TimeSpan.FromMinutes(5); - session.Statistics.OutputAudioDuration = TimeSpan.FromMinutes(10); - session.Statistics.ErrorCount = hasErrors ? 2 : 0; - - return session; - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Summary.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Summary.cs deleted file mode 100644 index 34406977c..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.Summary.cs +++ /dev/null @@ -1,70 +0,0 @@ -using ConduitLLM.Configuration.DTOs.Audio; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region GetUsageSummaryAsync Tests - - [Fact] - public async Task GetUsageSummaryAsync_WithValidParameters_ShouldReturnSummary() - { - // Arrange - var startDate = DateTime.UtcNow.AddDays(-30); - var endDate = DateTime.UtcNow; - var expectedSummary = new AudioUsageSummaryDto - { - TotalOperations = 100, - TotalCost = 50.5m, - TotalDurationSeconds = 3600, - SuccessfulOperations = 95, - FailedOperations = 5, - TotalCharacters = 10000, - TotalInputTokens = 5000, - TotalOutputTokens = 4000 - }; - - _mockRepository.Setup(x => x.GetUsageSummaryAsync(startDate, endDate, It.IsAny(), It.IsAny())) - .ReturnsAsync(expectedSummary); - - // Act - var result = await _service.GetUsageSummaryAsync(startDate, endDate); - - // Assert - result.Should().BeEquivalentTo(expectedSummary); - } - - [Fact] - public async Task GetUsageSummaryAsync_WithVirtualKeyFilter_ShouldReturnFilteredSummary() - { - // Arrange - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - var virtualKey = "test-key-hash"; - - var expectedSummary = new AudioUsageSummaryDto - { - TotalOperations = 20, - TotalCost = 10.5m, - SuccessfulOperations = 20, - FailedOperations = 0 - }; - - _mockRepository.Setup(x => x.GetUsageSummaryAsync(startDate, endDate, virtualKey, It.IsAny())) - .ReturnsAsync(expectedSummary); - - // Act - var result = await _service.GetUsageSummaryAsync(startDate, endDate, virtualKey); - - // Assert - result.TotalOperations.Should().Be(20); - result.TotalCost.Should().Be(10.5m); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.UsageLogs.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.UsageLogs.cs deleted file mode 100644 index 49c0c4f37..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.UsageLogs.cs +++ /dev/null @@ -1,90 +0,0 @@ -using ConduitLLM.Configuration.DTOs; -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Moq; - -namespace ConduitLLM.Tests.Admin.Services -{ - public partial class AdminAudioUsageServiceTests - { - #region GetUsageLogsAsync Tests - - [Fact] - public async Task GetUsageLogsAsync_WithValidQuery_ShouldReturnPagedResults() - { - // Arrange - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - ProviderId = 1 - }; - - var logs = CreateSampleAudioUsageLogs(15); - var pagedResult = new PagedResult - { - Items = logs.Take(10).ToList(), - TotalCount = 15, - Page = 1, - PageSize = 10, - TotalPages = 2 - }; - - _mockRepository.Setup(x => x.GetPagedAsync(query)) - .ReturnsAsync(pagedResult); - - // Act - var result = await _service.GetUsageLogsAsync(query); - - // Assert - result.Should().NotBeNull(); - result.Items.Should().HaveCount(10); - result.TotalCount.Should().Be(15); - result.TotalPages.Should().Be(2); - result.Items.First().ProviderId.Should().Be(1); - } - - [Fact] - public async Task GetUsageLogsAsync_WithDateRange_ShouldFilterResults() - { - // Arrange - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - StartDate = startDate, - EndDate = endDate - }; - - var logs = CreateSampleAudioUsageLogs(5); - var pagedResult = new PagedResult - { - Items = logs, - TotalCount = 5, - Page = 1, - PageSize = 10, - TotalPages = 1 - }; - - _mockRepository.Setup(x => x.GetPagedAsync(It.Is(q => - q.StartDate == startDate && q.EndDate == endDate))) - .ReturnsAsync(pagedResult); - - // Act - var result = await _service.GetUsageLogsAsync(query); - - // Assert - result.Items.Should().HaveCount(5); - _mockRepository.Verify(x => x.GetPagedAsync(It.Is(q => - q.StartDate == startDate && q.EndDate == endDate)), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.cs b/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.cs deleted file mode 100644 index 20399e668..000000000 --- a/ConduitLLM.Tests/Admin/Services/AdminAudioUsageServiceTests.cs +++ /dev/null @@ -1,21 +0,0 @@ -namespace ConduitLLM.Tests.Admin.Services -{ - /// - /// Unit tests for the AdminAudioUsageService class. - /// This partial class contains tests split across multiple files for better organization. - /// - [Trait("Category", "Unit")] - [Trait("Component", "AudioUsage")] - public partial class AdminAudioUsageServiceTests - { - // The implementation is split across the partial class files: - // - AdminAudioUsageServiceTests.Setup.cs: Constructor and helper methods - // - AdminAudioUsageServiceTests.UsageLogs.cs: GetUsageLogsAsync tests - // - AdminAudioUsageServiceTests.Summary.cs: GetUsageSummaryAsync tests - // - AdminAudioUsageServiceTests.ByKey.cs: GetUsageByKeyAsync tests - // - AdminAudioUsageServiceTests.ByProvider.cs: GetUsageByProviderAsync tests - // - AdminAudioUsageServiceTests.RealtimeSessions.cs: Realtime session tests - // - AdminAudioUsageServiceTests.Export.cs: Export tests - // - AdminAudioUsageServiceTests.Cleanup.cs: Cleanup tests - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs b/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs index 3ee29d3be..e190daa1b 100644 --- a/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs +++ b/ConduitLLM.Tests/Architecture/ModelArchitectureTests.cs @@ -73,9 +73,6 @@ public void ModelCapabilities_ShouldHaveAllExpectedProperties() { "SupportsChat", "SupportsVision", - "SupportsAudioTranscription", - "SupportsTextToSpeech", - "SupportsRealtimeAudio", "SupportsImageGeneration", "SupportsVideoGeneration", "SupportsEmbeddings", @@ -165,9 +162,6 @@ public void ModelProviderMapping_ShouldDeriveAllCapabilitiesFromModel() SupportsEmbeddings = false, SupportsFunctionCalling = true, SupportsStreaming = true, - SupportsAudioTranscription = true, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, SupportsImageGeneration = false, SupportsVideoGeneration = false, MaxTokens = 8192, @@ -187,9 +181,6 @@ public void ModelProviderMapping_ShouldDeriveAllCapabilitiesFromModel() Assert.Equal(model.Capabilities.SupportsEmbeddings, mapping.SupportsEmbeddings); Assert.Equal(model.Capabilities.SupportsFunctionCalling, mapping.SupportsFunctionCalling); Assert.Equal(model.Capabilities.SupportsStreaming, mapping.SupportsStreaming); - Assert.Equal(model.Capabilities.SupportsAudioTranscription, mapping.SupportsAudioTranscription); - Assert.Equal(model.Capabilities.SupportsTextToSpeech, mapping.SupportsTextToSpeech); - Assert.Equal(model.Capabilities.SupportsRealtimeAudio, mapping.SupportsRealtimeAudio); Assert.Equal(model.Capabilities.SupportsImageGeneration, mapping.SupportsImageGeneration); Assert.Equal(model.Capabilities.SupportsVideoGeneration, mapping.SupportsVideoGeneration); Assert.Equal(model.Capabilities.MaxTokens, mapping.MaxContextTokens); diff --git a/ConduitLLM.Tests/Configuration/Migrations/AudioProviderTypeMigrationTests.cs b/ConduitLLM.Tests/Configuration/Migrations/AudioProviderTypeMigrationTests.cs deleted file mode 100644 index 356cb3e99..000000000 --- a/ConduitLLM.Tests/Configuration/Migrations/AudioProviderTypeMigrationTests.cs +++ /dev/null @@ -1,223 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Configuration.Repositories; - -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Tests.Configuration.Migrations -{ - /// - /// Tests for the AudioProviderType migration to ensure entities work correctly with ProviderType enum. - /// - public class AudioProviderTypeMigrationTests : IDisposable - { - private readonly ConduitDbContext _context; - private readonly AudioCostRepository _costRepository; - private readonly AudioUsageLogRepository _usageRepository; - - public AudioProviderTypeMigrationTests() - { - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) - .ConfigureWarnings(warnings => warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) - .Options; - - _context = new ConduitDbContext(options); - _context.IsTestEnvironment = true; - _costRepository = new AudioCostRepository(_context); - _usageRepository = new AudioUsageLogRepository(_context); - } - - [Fact] - public async Task AudioCost_Should_Store_And_Retrieve_ProviderType() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "OpenAI" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var cost = new AudioCost - { - ProviderId = provider.Id, - OperationType = "transcription", - Model = "whisper-1", - CostUnit = "minute", - CostPerUnit = 0.006m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow - }; - - // Act - var created = await _costRepository.CreateAsync(cost); - var retrieved = await _costRepository.GetByIdAsync(created.Id); - - // Assert - Assert.NotNull(retrieved); - Assert.Equal(provider.Id, retrieved.ProviderId); - Assert.Equal("transcription", retrieved.OperationType); - } - - [Fact] - public async Task AudioUsageLog_Should_Store_And_Retrieve_ProviderType() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.ElevenLabs, ProviderName = "ElevenLabs" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var usageLog = new AudioUsageLog - { - VirtualKey = "test-key-123", - ProviderId = provider.Id, - OperationType = "tts", - Model = "eleven_monolingual_v1", - CharacterCount = 1000, - Cost = 0.18m, - StatusCode = 200, - Timestamp = DateTime.UtcNow - }; - - // Act - _context.AudioUsageLogs.Add(usageLog); - await _context.SaveChangesAsync(); - - var retrieved = await _context.AudioUsageLogs - .FirstOrDefaultAsync(l => l.VirtualKey == "test-key-123"); - - // Assert - Assert.NotNull(retrieved); - Assert.Equal(provider.Id, retrieved.ProviderId); - Assert.Equal("tts", retrieved.OperationType); - Assert.Equal(0.18m, retrieved.Cost); - } - - [Fact] - public async Task Repository_Should_Query_By_ProviderType() - { - // Arrange - var openAiProvider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "OpenAI" }; - var googleProvider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "Google Cloud" }; - _context.Providers.AddRange(openAiProvider, googleProvider); - await _context.SaveChangesAsync(); - - var costs = new[] - { - new AudioCost - { - ProviderId = openAiProvider.Id, - OperationType = "transcription", - Model = "whisper-1", - CostUnit = "minute", - CostPerUnit = 0.006m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow - }, - new AudioCost - { - ProviderId = googleProvider.Id, - OperationType = "transcription", - Model = "default", - CostUnit = "minute", - CostPerUnit = 0.016m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow - }, - new AudioCost - { - ProviderId = openAiProvider.Id, - OperationType = "tts", - Model = "tts-1", - CostUnit = "character", - CostPerUnit = 0.000015m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow - } - }; - - foreach (var cost in costs) - { - await _costRepository.CreateAsync(cost); - } - - // Act - var openAiCosts = await _costRepository.GetByProviderAsync(openAiProvider.Id); - var googleCosts = await _costRepository.GetByProviderAsync(googleProvider.Id); - - // Assert - Assert.Equal(2, openAiCosts.Count); - Assert.Single(googleCosts); - Assert.All(openAiCosts, c => Assert.Equal(openAiProvider.Id, c.ProviderId)); - Assert.All(googleCosts, c => Assert.Equal(googleProvider.Id, c.ProviderId)); - } - - [Fact] - public async Task GetCurrentCost_Should_Work_With_ProviderType() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "AWS Transcribe" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var cost = new AudioCost - { - ProviderId = provider.Id, - OperationType = "transcription", - Model = "standard", - CostUnit = "second", - CostPerUnit = 0.00040m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow.AddDays(-1), - EffectiveTo = null - }; - - await _costRepository.CreateAsync(cost); - - // Act - var currentCost = await _costRepository.GetCurrentCostAsync( - provider.Id, - "transcription", - "standard" - ); - - // Assert - Assert.NotNull(currentCost); - Assert.Equal(provider.Id, currentCost.ProviderId); - Assert.Equal(0.00040m, currentCost.CostPerUnit); - } - - [Theory] - [InlineData(ProviderType.OpenAI)] - [InlineData(ProviderType.ElevenLabs)] - public async Task All_Audio_Providers_Should_Be_Supported(ProviderType providerType) - { - // Arrange - var provider = new Provider { ProviderType = providerType, ProviderName = providerType.ToString() }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var cost = new AudioCost - { - ProviderId = provider.Id, - OperationType = "test", - Model = "test-model", - CostUnit = "unit", - CostPerUnit = 0.01m, - IsActive = true, - EffectiveFrom = DateTime.UtcNow - }; - - // Act - var created = await _costRepository.CreateAsync(cost); - var retrieved = await _costRepository.GetByIdAsync(created.Id); - - // Assert - Assert.NotNull(retrieved); - Assert.Equal(provider.Id, retrieved.ProviderId); - } - - public void Dispose() - { - _context?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.CreateAsync.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.CreateAsync.cs deleted file mode 100644 index 522a07eac..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.CreateAsync.cs +++ /dev/null @@ -1,79 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - public partial class AudioUsageLogRepositoryTests - { - #region CreateAsync Tests - - [Fact] - public async Task CreateAsync_WithValidLog_ShouldPersistLog() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "OpenAI" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var log = new AudioUsageLog - { - VirtualKey = "test-key-hash", - ProviderId = provider.Id, - OperationType = "transcription", - Model = "whisper-1", - RequestId = Guid.NewGuid().ToString(), - DurationSeconds = 15.5, - CharacterCount = 1000, - Cost = 0.15m, - Language = "en", - StatusCode = 200 - }; - - // Act - var result = await _repository.CreateAsync(log); - - // Assert - result.Should().NotBeNull(); - result.Id.Should().BeGreaterThan(0); - result.Timestamp.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(5)); - - var savedLog = await _context.AudioUsageLogs.FindAsync(result.Id); - savedLog.Should().NotBeNull(); - savedLog!.VirtualKey.Should().Be("test-key-hash"); - savedLog.ProviderId.Should().Be(provider.Id); - } - - [Fact] - public async Task CreateAsync_WithErrorLog_ShouldPersistWithError() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "Azure OpenAI" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - var log = new AudioUsageLog - { - VirtualKey = "test-key-hash", - ProviderId = provider.Id, - OperationType = "tts", - Model = "tts-1", - RequestId = Guid.NewGuid().ToString(), - StatusCode = 500, - ErrorMessage = "Internal server error", - Cost = 0m - }; - - // Act - var result = await _repository.CreateAsync(log); - - // Assert - result.StatusCode.Should().Be(500); - result.ErrorMessage.Should().Be("Internal server error"); - result.Cost.Should().Be(0); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetPagedAsync.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetPagedAsync.cs deleted file mode 100644 index 99a97cc78..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetPagedAsync.cs +++ /dev/null @@ -1,195 +0,0 @@ -using ConduitLLM.Configuration.DTOs.Audio; - -using FluentAssertions; - -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - public partial class AudioUsageLogRepositoryTests - { - #region GetPagedAsync Tests - - [Fact] - public async Task GetPagedAsync_WithNoFilters_ShouldReturnAllLogs() - { - // Arrange - await SeedTestDataAsync(15); - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10 - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Should().NotBeNull(); - result.Items.Should().HaveCount(10); - result.TotalCount.Should().Be(15); - result.TotalPages.Should().Be(2); - result.Page.Should().Be(1); - result.PageSize.Should().Be(10); - } - - [Fact] - public async Task GetPagedAsync_WithVirtualKeyFilter_ShouldReturnFilteredResults() - { - // Arrange - await SeedTestDataAsync(10); - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - VirtualKey = "key-1" - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().OnlyContain(log => log.VirtualKey == "key-1"); - result.TotalCount.Should().BeGreaterThan(0); - } - - [Fact] - public async Task GetPagedAsync_WithProviderFilter_ShouldReturnFilteredResults() - { - // Arrange - await SeedTestDataAsync(10); - - // Get one of the providers created by SeedTestDataAsync - var provider = await _context.Providers.FirstAsync(); - - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - ProviderId = provider.Id - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().NotBeEmpty(); - result.Items.Should().OnlyContain(log => log.ProviderId == provider.Id); - } - - [Fact] - public async Task GetPagedAsync_WithDateRange_ShouldReturnFilteredResults() - { - // Arrange - await SeedTestDataAsync(10, maxDaysAgo: 10); // Spread data across 10 days to ensure some fall in the date range - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow.AddDays(-3); - - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - StartDate = startDate, - EndDate = endDate - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().OnlyContain(log => - log.Timestamp >= startDate && log.Timestamp <= endDate); - } - - [Fact] - public async Task GetPagedAsync_WithOnlyErrors_ShouldReturnErrorLogs() - { - // Arrange - await SeedTestDataAsync(10); - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - OnlyErrors = true - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().OnlyContain(log => - log.StatusCode == null || log.StatusCode >= 400); - } - - [Fact] - public async Task GetPagedAsync_WithMultipleFilters_ShouldApplyAllFilters() - { - // Arrange - await SeedTestDataAsync(20); - - // Get one of the providers created by SeedTestDataAsync - var provider = await _context.Providers.FirstAsync(); - - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10, - ProviderId = provider.Id, - OperationType = "transcription", - StartDate = DateTime.UtcNow.AddDays(-5) - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - // Some logs should match these criteria (not all, since we're filtering) - if (result.Items.Count() > 0) - { - result.Items.Should().OnlyContain(log => - log.ProviderId == provider.Id && - log.OperationType.ToLower() == "transcription" && - log.Timestamp >= query.StartDate); - } - } - - [Fact] - public async Task GetPagedAsync_WithPageSizeExceedingMax_ShouldCapPageSize() - { - // Arrange - await SeedTestDataAsync(2000); - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 2000 // Exceeds max of 1000 - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().HaveCount(1000); // Should be capped at max - result.PageSize.Should().Be(1000); - } - - [Fact] - public async Task GetPagedAsync_ShouldOrderByTimestampDescending() - { - // Arrange - await SeedTestDataAsync(5); - var query = new AudioUsageQueryDto - { - Page = 1, - PageSize = 10 - }; - - // Act - var result = await _repository.GetPagedAsync(query); - - // Assert - result.Items.Should().BeInDescendingOrder(log => log.Timestamp); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetUsageSummary.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetUsageSummary.cs deleted file mode 100644 index 663f2db9a..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.GetUsageSummary.cs +++ /dev/null @@ -1,53 +0,0 @@ -using Microsoft.EntityFrameworkCore; -using FluentAssertions; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - public partial class AudioUsageLogRepositoryTests - { - #region GetUsageSummaryAsync Tests - - [Fact] - public async Task GetUsageSummaryAsync_WithNoFilters_ShouldReturnFullSummary() - { - // Arrange - await SeedTestDataAsync(50); - var startDate = DateTime.UtcNow.AddDays(-30); - var endDate = DateTime.UtcNow; - - // Act - var result = await _repository.GetUsageSummaryAsync(startDate, endDate); - - // Assert - result.Should().NotBeNull(); - result.TotalOperations.Should().BeGreaterThan(0); - result.TotalCost.Should().BeGreaterThan(0); - result.SuccessfulOperations.Should().BeGreaterThan(0); - result.OperationBreakdown.Should().NotBeEmpty(); - result.ProviderBreakdown.Should().NotBeEmpty(); - result.VirtualKeyBreakdown.Should().NotBeEmpty(); - } - - [Fact] - public async Task GetUsageSummaryAsync_WithVirtualKeyFilter_ShouldReturnFilteredSummary() - { - // Arrange - await SeedTestDataAsync(20, maxDaysAgo: 6); // Keep all data within 6 days for 7-day window query - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - - // Act - var result = await _repository.GetUsageSummaryAsync(startDate, endDate, "key-1"); - - // Assert - result.TotalOperations.Should().BeGreaterThan(0); - // Should only count operations for key-1 - var key1Logs = await _context.AudioUsageLogs - .Where(l => l.VirtualKey == "key-1" && l.Timestamp >= startDate && l.Timestamp <= endDate) - .ToListAsync(); - result.TotalOperations.Should().Be(key1Logs.Count); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Helpers.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Helpers.cs deleted file mode 100644 index 7e8bd1aa3..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Helpers.cs +++ /dev/null @@ -1,57 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - public partial class AudioUsageLogRepositoryTests - { - #region Helper Methods - - private async Task SeedTestDataAsync(int count, int maxDaysAgo = 30) - { - // Create test providers - var openAiProvider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "OpenAI" }; - var azureProvider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "Azure OpenAI" }; - _context.Providers.AddRange(openAiProvider, azureProvider); - await _context.SaveChangesAsync(); - - var logs = new List(); - var random = new Random(42); // Use fixed seed for deterministic behavior - - for (int i = 0; i < count; i++) - { - var operationType = i % 3 == 0 ? "transcription" : i % 3 == 1 ? "tts" : "realtime"; - var provider = i % 2 == 0 ? openAiProvider : azureProvider; - var statusCode = i % 10 == 0 ? 500 : 200; - - // Distribute timestamps evenly across the time range to ensure all operation types appear in any window - var daysAgo = (i * maxDaysAgo) / count; - - logs.Add(new AudioUsageLog - { - VirtualKey = $"key-{i % 3}", - ProviderId = provider.Id, - OperationType = operationType, - Model = provider.ProviderType == ProviderType.OpenAI ? "whisper-1" : "azure-tts", - RequestId = Guid.NewGuid().ToString(), - SessionId = operationType == "realtime" ? Guid.NewGuid().ToString() : null, - DurationSeconds = random.Next(1, 60), - CharacterCount = random.Next(100, 5000), - Cost = (decimal)(random.NextDouble() * 2), - Language = "en", - Voice = operationType == "tts" ? "alloy" : null, - StatusCode = statusCode, - ErrorMessage = statusCode >= 400 ? "Error occurred" : null, - IpAddress = $"192.168.1.{i % 255}", - UserAgent = "Test/1.0", - Timestamp = DateTime.UtcNow.AddDays(-daysAgo) - }); - } - - _context.AudioUsageLogs.AddRange(logs); - await _context.SaveChangesAsync(); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.OtherMethods.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.OtherMethods.cs deleted file mode 100644 index d968d63b3..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.OtherMethods.cs +++ /dev/null @@ -1,138 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; - -using FluentAssertions; - -using Microsoft.EntityFrameworkCore; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - public partial class AudioUsageLogRepositoryTests - { - #region Other Repository Methods Tests - - [Fact] - public async Task GetByVirtualKeyAsync_ShouldReturnLogsForKey() - { - // Arrange - await SeedTestDataAsync(10); - - // Act - var result = await _repository.GetByVirtualKeyAsync("key-1"); - - // Assert - result.Should().NotBeEmpty(); - result.Should().OnlyContain(log => log.VirtualKey == "key-1"); - } - - [Fact] - public async Task GetByProviderAsync_ShouldReturnLogsForProvider() - { - // Arrange - // SeedTestDataAsync creates its own providers, so we need to get one of those - await SeedTestDataAsync(10); - - // Get the first provider that was created by SeedTestDataAsync - var provider = await _context.Providers.FirstAsync(); - - // Act - var result = await _repository.GetByProviderAsync(provider.Id); - - // Assert - result.Should().NotBeEmpty(); - result.Should().OnlyContain(log => log.ProviderId == provider.Id); - } - - [Fact] - public async Task GetOperationBreakdownAsync_ShouldReturnCorrectCounts() - { - // Arrange - await SeedTestDataAsync(30, maxDaysAgo: 6); // Keep all data within 6 days for 7-day window query - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - - // Act - var result = await _repository.GetOperationBreakdownAsync(startDate, endDate); - - // Assert - result.Should().NotBeEmpty(); - result.Should().Contain(b => b.OperationType == "transcription"); - result.Should().Contain(b => b.OperationType == "tts"); - result.Should().Contain(b => b.OperationType == "realtime"); - result.Sum(b => b.Count).Should().BeGreaterThan(0); - } - - [Fact] - public async Task GetProviderBreakdownAsync_ShouldReturnCorrectCounts() - { - // Arrange - await SeedTestDataAsync(30, maxDaysAgo: 6); // Keep all data within 6 days for 7-day window query - var startDate = DateTime.UtcNow.AddDays(-7); - var endDate = DateTime.UtcNow; - - // Act - var result = await _repository.GetProviderBreakdownAsync(startDate, endDate); - - // Assert - result.Should().NotBeEmpty(); - result.Should().Contain(b => b.ProviderName.ToLower().Contains("openai")); - result.Should().Contain(b => b.ProviderName.ToLower().Contains("azure")); - result.Sum(b => b.Count).Should().BeGreaterThan(0); - } - - [Fact] - public async Task DeleteOldLogsAsync_ShouldDeleteLogsBeforeCutoff() - { - // Arrange - var provider = new Provider { ProviderType = ProviderType.OpenAI, ProviderName = "OpenAI" }; - _context.Providers.Add(provider); - await _context.SaveChangesAsync(); - - // Add some old logs - var oldLogs = new List(); - for (int i = 0; i < 10; i++) - { - oldLogs.Add(new AudioUsageLog - { - VirtualKey = "old-key", - ProviderId = provider.Id, - OperationType = "transcription", - Model = "whisper-1", - Timestamp = DateTime.UtcNow.AddDays(-60), - Cost = 0.1m - }); - } - _context.AudioUsageLogs.AddRange(oldLogs); - - // Add some recent logs - var recentLogs = new List(); - for (int i = 0; i < 5; i++) - { - recentLogs.Add(new AudioUsageLog - { - VirtualKey = "recent-key", - ProviderId = provider.Id, - OperationType = "transcription", - Model = "whisper-1", - Timestamp = DateTime.UtcNow.AddDays(-5), - Cost = 0.1m - }); - } - _context.AudioUsageLogs.AddRange(recentLogs); - await _context.SaveChangesAsync(); - - var cutoffDate = DateTime.UtcNow.AddDays(-30); - - // Act - var deletedCount = await _repository.DeleteOldLogsAsync(cutoffDate); - - // Assert - deletedCount.Should().Be(10); - var remainingLogs = await _context.AudioUsageLogs.ToListAsync(); - remainingLogs.Should().HaveCount(5); - remainingLogs.Should().OnlyContain(log => log.Timestamp > cutoffDate); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Setup.cs b/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Setup.cs deleted file mode 100644 index d41010564..000000000 --- a/ConduitLLM.Tests/Configuration/Repositories/AudioUsageLogRepositoryTests.Setup.cs +++ /dev/null @@ -1,40 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Repositories; - -using Microsoft.EntityFrameworkCore; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Configuration.Repositories -{ - /// - /// Unit tests for the AudioUsageLogRepository class - Setup and common infrastructure. - /// - [Trait("Category", "Unit")] - [Trait("Component", "Repository")] - public partial class AudioUsageLogRepositoryTests : IDisposable - { - private readonly ConduitDbContext _context; - private readonly AudioUsageLogRepository _repository; - private readonly ITestOutputHelper _output; - - public AudioUsageLogRepositoryTests(ITestOutputHelper output) - { - _output = output; - - var options = new DbContextOptionsBuilder() - .UseInMemoryDatabase(databaseName: Guid.NewGuid().ToString()) - .ConfigureWarnings(warnings => warnings.Ignore(Microsoft.EntityFrameworkCore.Diagnostics.InMemoryEventId.TransactionIgnoredWarning)) - .Options; - - _context = new ConduitDbContext(options); - _context.IsTestEnvironment = true; - _repository = new AudioUsageLogRepository(_context); - } - - public void Dispose() - { - _context?.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Models/UsageSerializationTests.cs b/ConduitLLM.Tests/Core/Models/UsageSerializationTests.cs index c3cb5c674..6e045ed0d 100644 --- a/ConduitLLM.Tests/Core/Models/UsageSerializationTests.cs +++ b/ConduitLLM.Tests/Core/Models/UsageSerializationTests.cs @@ -29,7 +29,6 @@ public void Serialize_AllFieldsPopulated_ShouldSerializeCorrectly() InferenceSteps = 30, VideoDurationSeconds = 60.5, VideoResolution = "1920x1080", - AudioDurationSeconds = 120.75m, SearchUnits = 5, SearchMetadata = new SearchUsageMetadata { @@ -62,7 +61,6 @@ public void Serialize_AllFieldsPopulated_ShouldSerializeCorrectly() deserialized.InferenceSteps.Should().Be(30); deserialized.VideoDurationSeconds.Should().Be(60.5); deserialized.VideoResolution.Should().Be("1920x1080"); - deserialized.AudioDurationSeconds.Should().Be(120.75m); deserialized.SearchUnits.Should().Be(5); deserialized.IsBatch.Should().BeTrue(); @@ -98,7 +96,6 @@ public void Serialize_NullFields_ShouldOmitFromJson() json.Should().NotContain("inference_steps"); json.Should().NotContain("video_duration_seconds"); json.Should().NotContain("video_resolution"); - json.Should().NotContain("audio_duration_seconds"); json.Should().NotContain("search_units"); json.Should().NotContain("search_metadata"); json.Should().NotContain("is_batch"); @@ -129,7 +126,6 @@ public void Deserialize_OldFormat_ShouldHandleBackwardCompatibility() usage.CachedWriteTokens.Should().BeNull(); usage.ImageQuality.Should().BeNull(); usage.InferenceSteps.Should().BeNull(); - usage.AudioDurationSeconds.Should().BeNull(); usage.SearchUnits.Should().BeNull(); usage.SearchMetadata.Should().BeNull(); usage.Metadata.Should().BeNull(); @@ -162,7 +158,6 @@ public void Deserialize_PartialNewFields_ShouldHandleCorrectly() // Other fields should be null usage.CachedWriteTokens.Should().BeNull(); - usage.AudioDurationSeconds.Should().BeNull(); usage.SearchUnits.Should().BeNull(); } @@ -177,7 +172,6 @@ public void Serialize_CompleteUsageObject_ShouldUseCorrectPropertyNames() TotalTokens = 150, CachedInputTokens = 30, CachedWriteTokens = 20, - AudioDurationSeconds = 60.5m, SearchUnits = 2, InferenceSteps = 30, ImageQuality = "hd" @@ -194,7 +188,6 @@ public void Serialize_CompleteUsageObject_ShouldUseCorrectPropertyNames() root.GetProperty("total_tokens").GetInt32().Should().Be(150); root.GetProperty("cached_input_tokens").GetInt32().Should().Be(30); root.GetProperty("cached_write_tokens").GetInt32().Should().Be(20); - root.GetProperty("audio_duration_seconds").GetDecimal().Should().Be(60.5m); root.GetProperty("search_units").GetInt32().Should().Be(2); root.GetProperty("inference_steps").GetInt32().Should().Be(30); root.GetProperty("image_quality").GetString().Should().Be("hd"); @@ -221,7 +214,6 @@ public void Deserialize_EmptyUsage_ShouldHandleAllNullFields() usage.InferenceSteps.Should().BeNull(); usage.VideoDurationSeconds.Should().BeNull(); usage.VideoResolution.Should().BeNull(); - usage.AudioDurationSeconds.Should().BeNull(); usage.SearchUnits.Should().BeNull(); usage.SearchMetadata.Should().BeNull(); usage.IsBatch.Should().BeNull(); diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.EdgeCases.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.EdgeCases.cs deleted file mode 100644 index f268ab65d..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.EdgeCases.cs +++ /dev/null @@ -1,139 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - public partial class AudioCostCalculationServiceTests - { - [Fact] - public async Task CalculateAudioCostAsync_WithTranscription_DelegatesToTranscriptionMethod() - { - // Arrange - var provider = "groq"; - var model = "whisper-large-v3"; - var durationSeconds = 600.0; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateAudioCostAsync( - provider, "transcription", model, durationSeconds, 0); - - // Assert - result.Operation.Should().Be("transcription"); - result.RatePerUnit.Should().Be(0.0001m); // Groq rate - } - - [Fact] - public async Task CalculateAudioCostAsync_WithTextToSpeech_DelegatesToTTSMethod() - { - // Arrange - var provider = "openai"; - var model = "tts-1-hd"; - var characterCount = 1000; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateAudioCostAsync( - provider, "text-to-speech", model, 0, characterCount); - - // Assert - result.Operation.Should().Be("text-to-speech"); - result.RatePerUnit.Should().Be(0.00003m); - } - - [Fact] - public async Task CalculateAudioCostAsync_WithRealtime_DelegatesToRealtimeMethod() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var durationSeconds = 120.0; // Split evenly between input/output - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateAudioCostAsync( - provider, "realtime", model, durationSeconds, 0); - - // Assert - result.Operation.Should().Be("realtime"); - // 60s input (1 min) * 0.10 + 60s output (1 min) * 0.20 = 0.10 + 0.20 = 0.30 - result.TotalCost.Should().Be(0.30); - } - - [Fact] - public async Task CalculateAudioCostAsync_WithUnknownOperation_ThrowsArgumentException() - { - // Arrange - var provider = "openai"; - var model = "some-model"; - - // Act - var act = () => _service.CalculateAudioCostAsync( - provider, "unknown-operation", model, 0, 0); - - // Assert - await act.Should().ThrowAsync() - .WithMessage("Unknown operation: unknown-operation"); - } - - [Theory] - [InlineData("transcription", "transcription")] - [InlineData("text-to-speech", "text-to-speech")] - [InlineData("tts", "text-to-speech")] - [InlineData("realtime", "realtime")] - [InlineData("TRANSCRIPTION", "transcription")] - [InlineData("TTS", "text-to-speech")] - public async Task CalculateAudioCostAsync_WithVariousOperations_MapsCorrectly( - string inputOperation, string expectedOperation) - { - // Arrange - var provider = "openai"; - var model = "test-model"; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateAudioCostAsync( - provider, inputOperation, model, 60, 1000); - - // Assert - result.Operation.Should().Be(expectedOperation); - } - - [Fact] - public async Task CalculateAudioCostAsync_WithNegativeDurationForRealtime_SplitsEvenly() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var durationSeconds = -120.0; // -2 minutes total - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateAudioCostAsync( - provider, "realtime", model, durationSeconds, 0); - - // Assert - // Should split evenly: -60s input, -60s output (-1 min each) - // (-1 * 0.10) + (-1 * 0.20) = -0.10 - 0.20 = -0.30 - result.TotalCost.Should().Be(-0.30); - result.DetailedBreakdown!["input_minutes"].Should().Be(-1.0); - result.DetailedBreakdown!["output_minutes"].Should().Be(-1.0); - } - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Realtime.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Realtime.cs deleted file mode 100644 index 95a31dccf..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Realtime.cs +++ /dev/null @@ -1,171 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - public partial class AudioCostCalculationServiceTests - { - [Fact] - public async Task CalculateRealtimeCostAsync_WithOpenAI_CalculatesAudioAndTokenCosts() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var inputAudioSeconds = 300.0; // 5 minutes - var outputAudioSeconds = 180.0; // 3 minutes - var inputTokens = 1000; - var outputTokens = 2000; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds, inputTokens, outputTokens); - - // Assert - result.Operation.Should().Be("realtime"); - result.UnitCount.Should().Be(8.0); // 5 + 3 minutes - - // Audio cost: (5 * 0.10) + (3 * 0.20) = 0.5 + 0.6 = 1.1 - // Token cost: (1000 * 0.000005) + (2000 * 0.000015) = 0.005 + 0.03 = 0.035 - // Total: 1.1 + 0.035 = 1.135 - result.TotalCost.Should().BeApproximately(1.135, 0.0001); - - result.DetailedBreakdown.Should().NotBeNull(); - result.DetailedBreakdown!["audio_cost"].Should().BeApproximately(1.1, 0.0001); - result.DetailedBreakdown["token_cost"].Should().BeApproximately(0.035, 0.0001); - result.DetailedBreakdown["input_minutes"].Should().Be(5.0); - result.DetailedBreakdown["output_minutes"].Should().Be(3.0); - result.DetailedBreakdown["input_tokens"].Should().Be(1000); - result.DetailedBreakdown["output_tokens"].Should().Be(2000); - } - - [Fact] - public async Task CalculateRealtimeCostAsync_WithUltravox_AppliesMinimumDuration() - { - // Arrange - var provider = "ultravox"; - var model = "fixie-ai/ultravox-70b"; - var inputAudioSeconds = 30.0; // 0.5 minutes - var outputAudioSeconds = 15.0; // 0.25 minutes - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds); - - // Assert - // Minimum duration is 1 minute for each, so: - // (1 * 0.001) + (1 * 0.001) = 0.002 - result.TotalCost.Should().Be(0.002); - } - - [Fact] - public async Task CalculateRealtimeCostAsync_WithNoTokenRates_CalculatesOnlyAudioCost() - { - // Arrange - var provider = "ultravox"; - var model = "fixie-ai/ultravox-70b"; - var inputAudioSeconds = 120.0; // 2 minutes - var outputAudioSeconds = 180.0; // 3 minutes - var inputTokens = 1000; - var outputTokens = 2000; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds, inputTokens, outputTokens); - - // Assert - // Only audio cost: (2 * 0.001) + (3 * 0.001) = 0.005 - result.TotalCost.Should().Be(0.005); - result.DetailedBreakdown!["token_cost"].Should().Be(0.0); - } - - [Fact] - public async Task CalculateRealtimeCostAsync_WithNegativeAudioSeconds_HandlesCorrectly() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var inputAudioSeconds = -120.0; // -2 minutes - var outputAudioSeconds = 60.0; // 1 minute - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds); - - // Assert - result.UnitCount.Should().Be(-1.0); // -2 + 1 = -1 minute - // Audio cost: (-2 * 0.10) + (1 * 0.20) = -0.20 + 0.20 = 0.00 - result.TotalCost.Should().Be(0.00); - } - - [Fact] - public async Task CalculateRealtimeCostAsync_WithNegativeTokens_CalculatesNegativeTokenCost() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var inputAudioSeconds = 60.0; - var outputAudioSeconds = 60.0; - var inputTokens = -1000; // Negative tokens - var outputTokens = 500; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds, inputTokens, outputTokens); - - // Assert - result.UnitCount.Should().Be(2.0); // 1 + 1 = 2 minutes - // Audio cost: (1 * 0.10) + (1 * 0.20) = 0.10 + 0.20 = 0.30 - // Token cost: (-1000 * 0.000005) + (500 * 0.000015) = -0.005 + 0.0075 = 0.0025 - // Total: 0.30 + 0.0025 = 0.3025 - result.TotalCost.Should().BeApproximately(0.3025, 0.0001); - } - - [Fact] - public async Task CalculateRealtimeCostAsync_WithAllNegativeValues_CalculatesNegativeTotal() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var inputAudioSeconds = -180.0; // -3 minutes - var outputAudioSeconds = -120.0; // -2 minutes - var inputTokens = -1000; - var outputTokens = -2000; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "realtime", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateRealtimeCostAsync( - provider, model, inputAudioSeconds, outputAudioSeconds, inputTokens, outputTokens); - - // Assert - result.UnitCount.Should().Be(-5.0); // -3 + -2 = -5 minutes - // Audio cost: (-3 * 0.10) + (-2 * 0.20) = -0.30 - 0.40 = -0.70 - // Token cost: (-1000 * 0.000005) + (-2000 * 0.000015) = -0.005 - 0.03 = -0.035 - // Total: -0.70 - 0.035 = -0.735 - result.TotalCost.Should().BeApproximately(-0.735, 0.0001); - } - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Refunds.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Refunds.cs deleted file mode 100644 index 2ee356ac0..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Refunds.cs +++ /dev/null @@ -1,276 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - public partial class AudioCostCalculationServiceTests - { - #region Refund Method Tests - - [Fact] - public async Task CalculateTranscriptionRefundAsync_WithValidInputs_CalculatesCorrectRefund() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var originalDurationSeconds = 600.0; // 10 minutes - var refundDurationSeconds = 300.0; // 5 minutes - var refundReason = "Transcription quality issue"; - var originalTransactionId = "txn_audio_123"; - - // Act - var result = await _service.CalculateTranscriptionRefundAsync( - provider, model, originalDurationSeconds, refundDurationSeconds, - refundReason, originalTransactionId, "test-key"); - - // Assert - result.Should().NotBeNull(); - result.Provider.Should().Be(provider); - result.Operation.Should().Be("transcription"); - result.Model.Should().Be(model); - result.OriginalAmount.Should().Be(10.0); // 600/60 = 10 minutes - result.RefundAmount.Should().Be(5.0); // 300/60 = 5 minutes - result.TotalRefund.Should().Be(0.03); // 5 * 0.006 - result.RefundReason.Should().Be(refundReason); - result.OriginalTransactionId.Should().Be(originalTransactionId); - result.VirtualKey.Should().Be("test-key"); - result.IsPartialRefund.Should().BeFalse(); - result.ValidationMessages.Should().BeEmpty(); - } - - [Fact] - public async Task CalculateTranscriptionRefundAsync_WithEmptyRefundReason_ReturnsValidationError() - { - // Act - var result = await _service.CalculateTranscriptionRefundAsync( - "openai", "whisper-1", 600.0, 300.0, ""); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund reason is required."); - } - - [Fact] - public async Task CalculateTranscriptionRefundAsync_WithNegativeDuration_ReturnsValidationError() - { - // Act - var result = await _service.CalculateTranscriptionRefundAsync( - "openai", "whisper-1", 600.0, -300.0, "Test refund"); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund duration must be non-negative."); - } - - [Fact] - public async Task CalculateTranscriptionRefundAsync_WithRefundExceedingOriginal_CapsRefund() - { - // Act - var result = await _service.CalculateTranscriptionRefundAsync( - "openai", "whisper-1", 600.0, 900.0, "Excessive refund test"); - - // Assert - result.Should().NotBeNull(); - result.IsPartialRefund.Should().BeTrue(); - result.ValidationMessages.Should().Contain(m => m.Contains("Refund duration (900s) cannot exceed original duration (600s)")); - result.TotalRefund.Should().Be(0.06); // Capped at 10 minutes * 0.006 - } - - [Fact] - public async Task CalculateTextToSpeechRefundAsync_WithValidInputs_CalculatesCorrectRefund() - { - // Arrange - var provider = "openai"; - var model = "tts-1-hd"; - var originalCharacterCount = 10000; - var refundCharacterCount = 5000; - var refundReason = "Poor audio quality"; - var voice = "alloy"; - - // Act - var result = await _service.CalculateTextToSpeechRefundAsync( - provider, model, originalCharacterCount, refundCharacterCount, - refundReason, "txn_tts_123", voice, "test-key"); - - // Assert - result.Should().NotBeNull(); - result.Provider.Should().Be(provider); - result.Operation.Should().Be("text-to-speech"); - result.Model.Should().Be(model); - result.OriginalAmount.Should().Be(10.0); // 10k chars - result.RefundAmount.Should().Be(5.0); // 5k chars - result.TotalRefund.Should().Be(0.15); // 5 * 0.03 (0.00003 * 1000) - result.Voice.Should().Be(voice); - result.IsPartialRefund.Should().BeFalse(); - } - - [Fact] - public async Task CalculateTextToSpeechRefundAsync_WithNegativeCharacters_ReturnsValidationError() - { - // Act - var result = await _service.CalculateTextToSpeechRefundAsync( - "openai", "tts-1", 10000, -5000, "Test refund"); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund character count must be non-negative."); - } - - [Fact] - public async Task CalculateRealtimeRefundAsync_WithValidInputs_CalculatesAudioAndTokenRefunds() - { - // Arrange - var provider = "openai"; - var model = "gpt-4o-realtime-preview"; - var originalInputAudioSeconds = 300.0; - var refundInputAudioSeconds = 150.0; - var originalOutputAudioSeconds = 200.0; - var refundOutputAudioSeconds = 100.0; - var originalInputTokens = 1000; - var refundInputTokens = 500; - var originalOutputTokens = 2000; - var refundOutputTokens = 1000; - var refundReason = "Connection dropped"; - - // Act - var result = await _service.CalculateRealtimeRefundAsync( - provider, model, - originalInputAudioSeconds, refundInputAudioSeconds, - originalOutputAudioSeconds, refundOutputAudioSeconds, - originalInputTokens, refundInputTokens, - originalOutputTokens, refundOutputTokens, - refundReason, "txn_realtime_123", "test-key"); - - // Assert - result.Should().NotBeNull(); - result.Provider.Should().Be(provider); - result.Operation.Should().Be("realtime"); - result.Model.Should().Be(model); - - // Audio refund: (150/60 * 0.1) + (100/60 * 0.2) = 0.25 + 0.333... = 0.583... - // Token refund: (500 * 0.000005) + (1000 * 0.000015) = 0.0025 + 0.015 = 0.0175 - // Total: ~0.601 - result.TotalRefund.Should().BeApproximately(0.601, 0.001); - - result.DetailedBreakdown.Should().NotBeNull(); - result.DetailedBreakdown!["audio_refund"].Should().BeApproximately(0.583, 0.001); - result.DetailedBreakdown["token_refund"].Should().BeApproximately(0.0175, 0.0001); - result.DetailedBreakdown["refund_input_minutes"].Should().Be(2.5); - result.DetailedBreakdown["refund_output_minutes"].Should().BeApproximately(1.667, 0.001); - result.DetailedBreakdown["refund_input_tokens"].Should().Be(500); - result.DetailedBreakdown["refund_output_tokens"].Should().Be(1000); - } - - [Fact] - public async Task CalculateRealtimeRefundAsync_WithNegativeAudioSeconds_ReturnsValidationError() - { - // Act - var result = await _service.CalculateRealtimeRefundAsync( - "openai", "gpt-4o-realtime-preview", - 300.0, -150.0, 200.0, 100.0, - refundReason: "Test refund"); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund audio durations must be non-negative."); - } - - [Fact] - public async Task CalculateRealtimeRefundAsync_WithExcessiveTokens_CapsAndMarksPartial() - { - // Act - var result = await _service.CalculateRealtimeRefundAsync( - "openai", "gpt-4o-realtime-preview", - 300.0, 150.0, 200.0, 100.0, - 1000, 2000, 2000, 3000, // Refund tokens exceed original - "Excessive refund test"); - - // Assert - result.Should().NotBeNull(); - result.IsPartialRefund.Should().BeTrue(); - result.ValidationMessages.Should().HaveCount(2); - result.ValidationMessages.Should().Contain(m => m.Contains("Refund input tokens (2000) cannot exceed original (1000)")); - result.ValidationMessages.Should().Contain(m => m.Contains("Refund output tokens (3000) cannot exceed original (2000)")); - } - - [Fact] - public async Task CalculateTranscriptionRefundAsync_WithCustomRate_UsesCustomPricing() - { - // Arrange - var provider = "custom-provider"; - var model = "custom-model"; - var customRate = 0.02m; // $0.02 per minute - - _audioCostRepositoryMock.Setup(r => r.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync(new AudioCost - { - Provider = provider, - OperationType = "transcription", - Model = model, - CostPerUnit = customRate, - IsActive = true - }); - - // Act - var result = await _service.CalculateTranscriptionRefundAsync( - provider, model, 600.0, 300.0, "Custom rate refund"); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0.1); // 5 minutes * 0.02 - } - - [Fact] - public async Task CalculateTextToSpeechRefundAsync_WithUnknownModel_UsesDefaultRate() - { - // Act - var result = await _service.CalculateTextToSpeechRefundAsync( - "unknown-provider", "unknown-model", 10000, 5000, "Default rate test"); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0.15); // 5 * 0.03 (default rate) - } - - [Fact] - public async Task CalculateRealtimeRefundAsync_WithoutTokenSupport_CalculatesAudioOnly() - { - // Arrange - var provider = "ultravox"; - var model = "fixie-ai/ultravox-70b"; - - // Act - var result = await _service.CalculateRealtimeRefundAsync( - provider, model, - 300.0, 150.0, 200.0, 100.0, - refundReason: "Audio-only refund"); - - // Assert - result.Should().NotBeNull(); - result.DetailedBreakdown!["audio_refund"].Should().BeApproximately(0.0042, 0.0001); // ~4.17 minutes * 0.001 - result.DetailedBreakdown.Should().NotContainKey("token_refund"); - } - - [Fact] - public async Task CalculateRealtimeRefundAsync_WithEmptyRefundReason_ReturnsValidationError() - { - // Act - var result = await _service.CalculateRealtimeRefundAsync( - "openai", "gpt-4o-realtime-preview", - 300.0, 150.0, 200.0, 100.0, - refundReason: ""); - - // Assert - result.Should().NotBeNull(); - result.TotalRefund.Should().Be(0); - result.ValidationMessages.Should().Contain("Refund reason is required."); - } - - #endregion - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Setup.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Setup.cs deleted file mode 100644 index 89e368b15..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Setup.cs +++ /dev/null @@ -1,149 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - [Trait("Category", "Unit")] - [Trait("Phase", "2")] - [Trait("Component", "Core")] - public partial class AudioCostCalculationServiceTests : TestBase - { - private readonly Mock _serviceProviderMock; - private readonly Mock> _loggerMock; - private readonly Mock _audioCostRepositoryMock; - private readonly Mock _serviceScopeMock; - private readonly Mock _serviceScopeFactoryMock; - private readonly AudioCostCalculationService _service; - - public AudioCostCalculationServiceTests(ITestOutputHelper output) : base(output) - { - _serviceProviderMock = new Mock(); - _loggerMock = CreateLogger(); - _audioCostRepositoryMock = new Mock(); - _serviceScopeMock = new Mock(); - _serviceScopeFactoryMock = new Mock(); - - // Setup service scope - var scopedServiceProvider = new Mock(); - scopedServiceProvider - .Setup(x => x.GetService(typeof(IAudioCostRepository))) - .Returns(_audioCostRepositoryMock.Object); - - _serviceScopeMock - .Setup(x => x.ServiceProvider) - .Returns(scopedServiceProvider.Object); - - _serviceScopeFactoryMock - .Setup(x => x.CreateScope()) - .Returns(_serviceScopeMock.Object); - - _serviceProviderMock - .Setup(x => x.GetService(typeof(IServiceScopeFactory))) - .Returns(_serviceScopeFactoryMock.Object); - - _service = new AudioCostCalculationService(_serviceProviderMock.Object, _loggerMock.Object); - } - - [Fact] - public void Constructor_WithNullServiceProvider_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioCostCalculationService(null!, _loggerMock.Object); - act.Should().Throw().WithParameterName("serviceProvider"); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioCostCalculationService(_serviceProviderMock.Object, null!); - act.Should().Throw().WithParameterName("logger"); - } - - [Fact] - public async Task ServiceScope_IsProperlyDisposed() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 60.0; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - _serviceScopeMock.Verify(x => x.Dispose(), Times.Once); - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_VerifiesCancellationTokenPropagation() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 60.0; - using var cts = new CancellationTokenSource(); - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - await _service.CalculateTranscriptionCostAsync( - provider, model, durationSeconds, null, cts.Token); - - // Assert - Just verify it doesn't throw - _serviceScopeMock.Verify(x => x.Dispose(), Times.Once); - } - - [Fact] - public async Task GetCustomRateAsync_WithRepositoryException_ReturnsNull() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 60.0; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(It.IsAny(), It.IsAny(), It.IsAny())) - .ThrowsAsync(new Exception("Database error")); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(0.006m); // Falls back to built-in rate - _loggerMock.VerifyLog(LogLevel.Error, "Failed to get custom rate"); - } - - [Fact] - public async Task GetCustomRateAsync_WithNoRepository_UsesBuiltInRate() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 60.0; - - // Setup service provider to return null for repository - var scopedServiceProvider = new Mock(); - scopedServiceProvider - .Setup(x => x.GetService(typeof(IAudioCostRepository))) - .Returns(null); - - _serviceScopeMock - .Setup(x => x.ServiceProvider) - .Returns(scopedServiceProvider.Object); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(0.006m); // Built-in rate - } - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.TextToSpeech.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.TextToSpeech.cs deleted file mode 100644 index e7c812b13..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.TextToSpeech.cs +++ /dev/null @@ -1,150 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - public partial class AudioCostCalculationServiceTests - { - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithOpenAITTS1_CalculatesCorrectly() - { - // Arrange - var provider = "openai"; - var model = "tts-1"; - var characterCount = 1000000; // 1M characters - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync(provider, model, characterCount); - - // Assert - result.Provider.Should().Be(provider); - result.Operation.Should().Be("text-to-speech"); - result.Model.Should().Be(model); - result.UnitCount.Should().Be(1000000); - result.UnitType.Should().Be("characters"); - result.RatePerUnit.Should().Be(0.000015m); // $15 per 1M characters - result.TotalCost.Should().Be(15.0); // 1M * 0.000015 - } - - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithElevenLabs_CalculatesCorrectly() - { - // Arrange - var provider = "elevenlabs"; - var model = "eleven_multilingual_v2"; - var characterCount = 500000; // 500K characters - var voice = "Rachel"; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync(provider, model, characterCount, voice); - - // Assert - result.RatePerUnit.Should().Be(0.00006m); // $60 per 1M characters - result.TotalCost.Should().Be(30.0); // 500K * 0.00006 - result.Voice.Should().Be(voice); - } - - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithVirtualKey_IncludesInResult() - { - // Arrange - var provider = "openai"; - var model = "tts-1"; - var characterCount = 1000; - var virtualKey = "test-virtual-key"; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync( - provider, model, characterCount, null, virtualKey); - - // Assert - result.VirtualKey.Should().Be(virtualKey); - } - - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithNegativeCharacterCount_CalculatesNegativeCost() - { - // Arrange - var provider = "elevenlabs"; - var model = "eleven_multilingual_v2"; - var characterCount = -100000; // -100K characters - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync(provider, model, characterCount); - - // Assert - result.UnitCount.Should().Be(-100000); // Characters as-is - result.RatePerUnit.Should().Be(0.00006m); - result.TotalCost.Should().Be(-6.0); // -100000 * 0.00006 - } - - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithCustomRatePerThousandChars_CalculatesCorrectly() - { - // Arrange - var provider = "custom"; - var model = "custom-tts"; - var characterCount = 5000; - var customRatePerThousand = 0.05m; // $0.05 per 1K chars - - var customCost = new AudioCost - { - Provider = provider, - OperationType = "text-to-speech", - Model = model, - CostPerUnit = customRatePerThousand, - IsActive = true - }; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync(customCost); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync(provider, model, characterCount); - - // Assert - result.UnitCount.Should().Be(5.0); // 5K chars / 1K - result.UnitType.Should().Be("1k-characters"); - result.RatePerUnit.Should().Be(customRatePerThousand); - result.TotalCost.Should().Be(0.25); // 5 * 0.05 - } - - [Fact] - public async Task CalculateTextToSpeechCostAsync_WithZeroCharacters_ReturnsZeroCost() - { - // Arrange - var provider = "openai"; - var model = "tts-1"; - var characterCount = 0; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "text-to-speech", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTextToSpeechCostAsync(provider, model, characterCount); - - // Assert - result.UnitCount.Should().Be(0.0); - result.TotalCost.Should().Be(0.0); - } - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Transcription.cs b/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Transcription.cs deleted file mode 100644 index fda68fc3c..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioCostCalculationServiceTests.Transcription.cs +++ /dev/null @@ -1,238 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - // TODO: AudioCostCalculationService does not exist in Core project yet - // This test file is commented out until the service is implemented - /* - public partial class AudioCostCalculationServiceTests - { - [Fact] - public async Task CalculateTranscriptionCostAsync_WithBuiltInOpenAIRate_CalculatesCorrectly() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 300.0; // 5 minutes - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.Should().NotBeNull(); - result.Provider.Should().Be(provider); - result.Operation.Should().Be("transcription"); - result.Model.Should().Be(model); - result.UnitCount.Should().Be(5.0); // 5 minutes - result.UnitType.Should().Be("minutes"); - result.RatePerUnit.Should().Be(0.006m); // $0.006 per minute - result.TotalCost.Should().Be(0.03); // 5 * 0.006 - result.IsEstimate.Should().BeFalse(); - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithCustomRate_UsesCustomRate() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 600.0; // 10 minutes - var customRate = 0.01m; - - var customCost = new AudioCost - { - Provider = provider, - OperationType = "transcription", - Model = model, - CostPerUnit = customRate, - IsActive = true - }; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync(customCost); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(customRate); - result.TotalCost.Should().Be(0.1); // 10 * 0.01 - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithUnknownProvider_UsesDefaultRate() - { - // Arrange - var provider = "unknown-provider"; - var model = "unknown-model"; - var durationSeconds = 120.0; // 2 minutes - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(0.01m); // Default rate - result.TotalCost.Should().Be(0.02); // 2 * 0.01 - result.IsEstimate.Should().BeTrue(); - _loggerMock.VerifyLog(LogLevel.Warning, "No pricing found"); - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithDeepgram_UsesCorrectRate() - { - // Arrange - var provider = "deepgram"; - var model = "nova-2-medical"; - var durationSeconds = 300.0; // 5 minutes - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(0.0145m); // Medical rate - result.TotalCost.Should().Be(0.0725); // 5 * 0.0145 - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithInactiveCustomCost_UsesBuiltInRate() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = 60.0; - - var inactiveCost = new AudioCost - { - Provider = provider, - OperationType = "transcription", - Model = model, - CostPerUnit = 0.01m, - IsActive = false // Inactive - }; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync(inactiveCost); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.RatePerUnit.Should().Be(0.006m); // Built-in rate, not custom - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithNegativeDuration_HandlesAsRefund() - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - var durationSeconds = -300.0; // -5 minutes (refund scenario) - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.UnitCount.Should().Be(-5.0); // -5 minutes - result.RatePerUnit.Should().Be(0.006m); - result.TotalCost.Should().Be(-0.03); // -5 * 0.006 - result.IsEstimate.Should().BeFalse(); - } - - [Fact] - public async Task CalculateTranscriptionCostAsync_WithNegativeMinimumCharge_AppliesCorrectly() - { - // Arrange - var provider = "custom"; - var model = "custom-stt"; - var durationSeconds = -30.0; // -0.5 minutes - - var customCost = new AudioCost - { - Provider = provider, - OperationType = "transcription", - Model = model, - CostPerUnit = 0.01m, - MinimumCharge = 0.10m, // Minimum charge for positive values - IsActive = true - }; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync(customCost); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - // For negative values, minimum charge should not apply - result.TotalCost.Should().Be(-0.005); // -0.5 * 0.01 = -0.005 - } - - [Theory] - [InlineData(60.0, 1.0)] // 60 seconds = 1 minute - [InlineData(90.0, 1.5)] // 90 seconds = 1.5 minutes - [InlineData(30.0, 0.5)] // 30 seconds = 0.5 minutes - [InlineData(3600.0, 60.0)] // 1 hour = 60 minutes - [InlineData(0.0, 0.0)] // 0 seconds = 0 minutes - public async Task CalculateTranscriptionCostAsync_ConvertsSecondsToMinutesCorrectly( - double durationSeconds, double expectedMinutes) - { - // Arrange - var provider = "openai"; - var model = "whisper-1"; - - _audioCostRepositoryMock - .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - .ReturnsAsync((AudioCost?)null); - - // Act - var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - - // Assert - result.UnitCount.Should().Be(expectedMinutes); - } - - // TODO: Fix decimal/double conversion issues in this test - // [Theory] - // [InlineData(-60.0, -1.0, 0.006, -0.006)] // -1 minute - // [InlineData(-3600.0, -60.0, 0.006, -0.36)] // -1 hour - // [InlineData(0.0, 0.0, 0.006, 0.0)] // Zero duration - // [InlineData(-0.5, -0.00833333, 0.006, -0.00005)] // Very small negative - // public async Task CalculateTranscriptionCostAsync_WithVariousNegativeDurations_CalculatesCorrectly( - // double durationSeconds, double expectedMinutes, decimal rate, decimal expectedCost) - // { - // // Arrange - // var provider = "openai"; - // var model = "whisper-1"; - // - // _audioCostRepositoryMock - // .Setup(x => x.GetCurrentCostAsync(provider, "transcription", model)) - // .ReturnsAsync((AudioCost?)null); - // - // // Act - // var result = await _service.CalculateTranscriptionCostAsync(provider, model, durationSeconds); - // - // // Assert - // Math.Abs(result.UnitCount - expectedMinutes).Should().BeLessThan(0.00001); - // result.RatePerUnit.Should().Be(rate); - // result.TotalCost.Should().Be(expectedCost); - // } - } - */ -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Concurrency.cs b/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Concurrency.cs deleted file mode 100644 index e29a734a5..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Concurrency.cs +++ /dev/null @@ -1,405 +0,0 @@ -using System.Collections.Concurrent; -using System.Text; - -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; - -using FluentAssertions; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioEncryptionServiceTests - { - [Fact] - public async Task EncryptAudioAsync_ConcurrentCalls_ProducesUniqueResults() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data for concurrent encryption"); - var taskCount = 100; - var tasks = new Task[taskCount]; - - // Act - for (int i = 0; i < taskCount; i++) - { - tasks[i] = _service.EncryptAudioAsync(audioData); - } - var results = await Task.WhenAll(tasks); - - // Assert - results.Should().HaveCount(taskCount); - - // Each encryption should have unique IV - var uniqueIVs = results.Select(r => Convert.ToBase64String(r.IV)).Distinct().Count(); - uniqueIVs.Should().Be(taskCount, "each encryption should have a unique IV"); - - // Each encryption should produce different ciphertext - var uniqueCiphertexts = results.Select(r => Convert.ToBase64String(r.EncryptedBytes)).Distinct().Count(); - uniqueCiphertexts.Should().Be(taskCount, "each encryption should produce unique ciphertext"); - } - - [Fact] - public async Task DecryptAudioAsync_ConcurrentDecryption_AllSucceed() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test data for concurrent decryption"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - var taskCount = 50; - var tasks = new Task[taskCount]; - - // Act - for (int i = 0; i < taskCount; i++) - { - tasks[i] = _service.DecryptAudioAsync(encryptedData); - } - var results = await Task.WhenAll(tasks); - - // Assert - results.Should().HaveCount(taskCount); - foreach (var result in results) - { - result.Should().BeEquivalentTo(originalData); - } - } - - [Fact] - public async Task GenerateKeyAsync_ConcurrentGeneration_ProducesUniqueKeys() - { - // Arrange - var taskCount = 100; - var tasks = new Task[taskCount]; - - // Act - for (int i = 0; i < taskCount; i++) - { - tasks[i] = _service.GenerateKeyAsync(); - } - var keyIds = await Task.WhenAll(tasks); - - // Assert - keyIds.Should().HaveCount(taskCount); - keyIds.Distinct().Count().Should().Be(taskCount, "all generated key IDs should be unique"); - } - - [Fact] - public async Task EncryptDecrypt_ConcurrentMixedOperations_AllSucceed() - { - // Arrange - var operationCount = 200; - var tasks = new List(); - var encryptedDataList = new List(); - var semaphore = new SemaphoreSlim(1, 1); - - // Act - Mix of encrypt and decrypt operations - for (int i = 0; i < operationCount; i++) - { - if (i % 2 == 0) - { - // Encrypt operation - var data = Encoding.UTF8.GetBytes($"Test data {i}"); - var task = Task.Run(async () => - { - var encrypted = await _service.EncryptAudioAsync(data); - await semaphore.WaitAsync(); - try - { - encryptedDataList.Add(encrypted); - } - finally - { - semaphore.Release(); - } - }); - tasks.Add(task); - } - else if (encryptedDataList.Count() > 0) - { - // Decrypt operation - await semaphore.WaitAsync(); - EncryptedAudioData? dataToDecrypt = null; - try - { - if (encryptedDataList.Count() > 0) - { - dataToDecrypt = encryptedDataList[0]; - } - } - finally - { - semaphore.Release(); - } - - if (dataToDecrypt != null) - { - var task = Task.Run(async () => - { - var decrypted = await _service.DecryptAudioAsync(dataToDecrypt); - decrypted.Should().NotBeNull(); - }); - tasks.Add(task); - } - } - } - - // Assert - await Task.WhenAll(tasks); - tasks.Should().NotBeEmpty(); - } - - [Fact] - public async Task ValidateIntegrityAsync_ConcurrentValidation_AllReturnCorrectResults() - { - // Arrange - var validData = await _service.EncryptAudioAsync(Encoding.UTF8.GetBytes("Valid data")); - var tamperedData = await _service.EncryptAudioAsync(Encoding.UTF8.GetBytes("Tampered data")); - tamperedData.AuthTag[0] ^= 0xFF; // Tamper with auth tag - - var taskCount = 50; - var validTasks = new Task[taskCount]; - var tamperedTasks = new Task[taskCount]; - - // Act - for (int i = 0; i < taskCount; i++) - { - validTasks[i] = _service.ValidateIntegrityAsync(validData); - tamperedTasks[i] = _service.ValidateIntegrityAsync(tamperedData); - } - - var validResults = await Task.WhenAll(validTasks); - var tamperedResults = await Task.WhenAll(tamperedTasks); - - // Assert - validResults.Should().AllBeEquivalentTo(true); - tamperedResults.Should().AllBeEquivalentTo(false); - } - - [Fact] - public async Task EncryptAudioAsync_ConcurrentWithDifferentMetadata_HandlesCorrectly() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio with metadata"); - var taskCount = 50; - var tasks = new Task[taskCount]; - - // Act - for (int i = 0; i < taskCount; i++) - { - var metadata = new AudioEncryptionMetadata - { - Format = $"format-{i}", - OriginalSize = i * 100, - DurationSeconds = i * 0.5, - VirtualKey = $"key-{i}", - CustomProperties = new() { [$"prop-{i}"] = $"value-{i}" } - }; - tasks[i] = _service.EncryptAudioAsync(audioData, metadata); - } - var results = await Task.WhenAll(tasks); - - // Assert - results.Should().HaveCount(taskCount); - var uniqueMetadata = results.Select(r => r.EncryptedMetadata).Distinct().Count(); - uniqueMetadata.Should().Be(taskCount, "each encryption should have unique metadata"); - } - - [Fact] - public async Task EncryptDecrypt_HighConcurrency_MaintainsDataIntegrity() - { - // Arrange - var concurrencyLevel = 500; - var dataSize = 1024; // 1KB per operation - var tasks = new List>(); - - // Act - for (int i = 0; i < concurrencyLevel; i++) - { - var index = i; - var task = Task.Run(async () => - { - try - { - // Generate unique data for each task - var originalData = new byte[dataSize]; - new Random(index).NextBytes(originalData); - - // Encrypt - var encrypted = await _service.EncryptAudioAsync(originalData); - - // Decrypt - var decrypted = await _service.DecryptAudioAsync(encrypted); - - // Verify - return decrypted.SequenceEqual(originalData); - } - catch - { - return false; - } - }); - tasks.Add(task); - } - - var results = await Task.WhenAll(tasks); - - // Assert - results.Should().AllBeEquivalentTo(true); - var successCount = results.Count(r => r); - successCount.Should().Be(concurrencyLevel, $"all {concurrencyLevel} operations should succeed"); - } - - [Fact] - public async Task GenerateKeyAsync_RapidKeyGeneration_NoDuplicates() - { - // Arrange - var keyCount = 1000; - var tasks = new Task[keyCount]; - var parallelOptions = new ParallelOptions - { - MaxDegreeOfParallelism = Environment.ProcessorCount * 2 - }; - - // Act - Generate keys as fast as possible - Parallel.ForEach(Enumerable.Range(0, keyCount), parallelOptions, (i) => - { - tasks[i] = _service.GenerateKeyAsync(); - }); - - var keyIds = await Task.WhenAll(tasks); - - // Assert - var uniqueKeyIds = new HashSet(keyIds); - uniqueKeyIds.Count.Should().Be(keyCount, "all generated keys should be unique even under high concurrency"); - } - - [Fact] - public async Task EncryptAudioAsync_ConcurrentLargeData_HandlesMemoryPressure() - { - // Arrange - var concurrentOps = 20; - var dataSize = 5 * 1024 * 1024; // 5MB per operation - var tasks = new Task[concurrentOps]; - - // Act - for (int i = 0; i < concurrentOps; i++) - { - var largeData = new byte[dataSize]; - new Random(i).NextBytes(largeData); - tasks[i] = _service.EncryptAudioAsync(largeData); - } - - var results = await Task.WhenAll(tasks); - - // Assert - results.Should().HaveCount(concurrentOps); - results.Should().AllSatisfy(r => - { - r.Should().NotBeNull(); - r.EncryptedBytes.Length.Should().Be(dataSize); - }); - } - - [Theory] - [InlineData(10, 100)] // 10 threads, 100 operations each - [InlineData(50, 20)] // 50 threads, 20 operations each - [InlineData(100, 10)] // 100 threads, 10 operations each - public async Task EncryptDecrypt_VariousConcurrencyPatterns_AllSucceed(int threadCount, int operationsPerThread) - { - // Arrange - var tasks = new Task[threadCount]; - var errors = new ConcurrentBag(); - - // Act - for (int t = 0; t < threadCount; t++) - { - var threadIndex = t; - tasks[t] = Task.Run(async () => - { - for (int op = 0; op < operationsPerThread; op++) - { - try - { - var data = Encoding.UTF8.GetBytes($"Thread {threadIndex} Operation {op}"); - var encrypted = await _service.EncryptAudioAsync(data); - var decrypted = await _service.DecryptAudioAsync(encrypted); - - if (!decrypted.SequenceEqual(data)) - { - throw new InvalidOperationException($"Data mismatch in thread {threadIndex} operation {op}"); - } - } - catch (Exception ex) - { - errors.Add(ex); - } - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - errors.Should().BeEmpty("no errors should occur during concurrent operations"); - } - - [Fact] - public async Task ThreadSafety_DemonstrateKeyStorageRaceCondition() - { - // This test demonstrates the thread safety issue in the current implementation - // The Dictionary _keyStore is not thread-safe - // When multiple threads try to create the "default" key simultaneously, - // they may end up with different keys, causing decryption failures - - // Arrange - var freshService = new AudioEncryptionService(_loggerMock.Object); - var iterations = 20; // Reduced from 100 to prevent overwhelming the test runner - var encryptTasks = new Task[iterations]; - var data = Encoding.UTF8.GetBytes("Test data"); - - // Use a cancellation token to prevent hanging - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - - // Act - Force race condition by starting all encryptions at once - // Use SemaphoreSlim to control concurrency instead of Barrier - var startSignal = new TaskCompletionSource(); - - for (int i = 0; i < iterations; i++) - { - encryptTasks[i] = Task.Run(async () => - { - await startSignal.Task; // Wait for signal to start - return await freshService.EncryptAudioAsync(data); - }); - } - - // Signal all tasks to start - startSignal.SetResult(true); - - var encryptedResults = await Task.WhenAll(encryptTasks); - - // Try to decrypt all with the same service instance - var decryptionFailures = 0; - foreach (var encrypted in encryptedResults) - { - try - { - var decrypted = await freshService.DecryptAudioAsync(encrypted); - if (!decrypted.SequenceEqual(data)) - { - decryptionFailures++; - } - } - catch - { - decryptionFailures++; - } - } - - // Assert - Due to race condition, some decryptions may fail - // This documents the thread safety issue - Log($"Decryption failures due to race condition: {decryptionFailures}/{iterations}"); - - // If there are failures, it proves the race condition exists - // If there are no failures, it doesn't prove thread safety (just lucky timing) - // The Dictionary implementation is definitively not thread-safe - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Core.cs b/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Core.cs deleted file mode 100644 index a54c03c88..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Core.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ConduitLLM.Core.Services; - -using FluentAssertions; - -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Core.Services -{ - [Trait("Category", "Unit")] - [Trait("Phase", "2")] - [Trait("Component", "Core")] - public partial class AudioEncryptionServiceTests : TestBase - { - private readonly Mock> _loggerMock; - private readonly AudioEncryptionService _service; - - public AudioEncryptionServiceTests(ITestOutputHelper output) : base(output) - { - _loggerMock = CreateLogger(); - _service = new AudioEncryptionService(_loggerMock.Object); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioEncryptionService(null!); - act.Should().Throw().WithParameterName("logger"); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Decrypt.cs b/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Decrypt.cs deleted file mode 100644 index 1484b6164..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Decrypt.cs +++ /dev/null @@ -1,156 +0,0 @@ -using System.Text; - -using ConduitLLM.Core.Interfaces; - -using FluentAssertions; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioEncryptionServiceTests - { - [Fact] - public async Task DecryptAudioAsync_WithValidEncryptedData_ReturnsOriginalData() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data for encryption and decryption"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Act - var decryptedData = await _service.DecryptAudioAsync(encryptedData); - - // Assert - decryptedData.Should().BeEquivalentTo(originalData); - Encoding.UTF8.GetString(decryptedData).Should().Be("Test audio data for encryption and decryption"); - } - - [Fact] - public async Task DecryptAudioAsync_WithMetadata_PreservesAssociatedData() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio with metadata"); - var metadata = new AudioEncryptionMetadata - { - Format = "wav", - OriginalSize = 2048 - }; - var encryptedData = await _service.EncryptAudioAsync(originalData, metadata); - - // Act - var decryptedData = await _service.DecryptAudioAsync(encryptedData); - - // Assert - decryptedData.Should().BeEquivalentTo(originalData); - } - - [Fact] - public async Task DecryptAudioAsync_WithNullData_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => _service.DecryptAudioAsync(null!); - await act.Should().ThrowAsync() - .WithParameterName("encryptedData"); - } - - [Fact] - public async Task DecryptAudioAsync_WithTamperedData_ThrowsInvalidOperationException() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Tamper with the encrypted data - encryptedData.EncryptedBytes[0] ^= 0xFF; - - // Act & Assert - var act = () => _service.DecryptAudioAsync(encryptedData); - await act.Should().ThrowAsync() - .WithMessage("*decryption failed*data may be tampered*"); - } - - [Fact] - public async Task DecryptAudioAsync_WithTamperedAuthTag_ThrowsInvalidOperationException() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Tamper with the auth tag - encryptedData.AuthTag[0] ^= 0xFF; - - // Act & Assert - var act = () => _service.DecryptAudioAsync(encryptedData); - await act.Should().ThrowAsync() - .WithMessage("*decryption failed*data may be tampered*"); - } - - [Fact] - public async Task DecryptAudioAsync_WithInvalidKeyId_ThrowsInvalidOperationException() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - encryptedData.KeyId = "non-existent-key"; - - // Act & Assert - var act = () => _service.DecryptAudioAsync(encryptedData); - var exception = await act.Should().ThrowAsync(); - exception.Which.Message.Should().Be("Audio decryption failed"); - exception.Which.InnerException.Should().BeOfType(); - exception.Which.InnerException!.Message.Should().Be("Key not found: non-existent-key"); - } - - [Fact] - public async Task DecryptAudioAsync_LogsInformation() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Act - await _service.DecryptAudioAsync(encryptedData); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Decrypted audio data")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task EncryptDecrypt_WithLargeData_WorksCorrectly() - { - // Arrange - var largeData = new byte[1024 * 1024]; // 1MB - new Random().NextBytes(largeData); - - // Act - var encryptedData = await _service.EncryptAudioAsync(largeData); - var decryptedData = await _service.DecryptAudioAsync(encryptedData); - - // Assert - decryptedData.Should().BeEquivalentTo(largeData); - } - - [Fact] - public async Task EncryptDecrypt_WithGeneratedKey_WorksCorrectly() - { - // Arrange - var keyId = await _service.GenerateKeyAsync(); - var audioData = Encoding.UTF8.GetBytes("Test with generated key"); - - // Need to use reflection or other means to set the key ID in encrypted data - // For this test, we'll just verify that key generation works - - // Act & Assert - keyId.Should().NotBeNullOrEmpty(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Encrypt.cs b/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Encrypt.cs deleted file mode 100644 index 2a6fbc150..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.Encrypt.cs +++ /dev/null @@ -1,170 +0,0 @@ -using System.Text; - -using ConduitLLM.Core.Interfaces; - -using FluentAssertions; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioEncryptionServiceTests - { - [Fact] - public async Task EncryptAudioAsync_WithValidData_ReturnsEncryptedData() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data for encryption"); - - // Act - var result = await _service.EncryptAudioAsync(audioData); - - // Assert - result.Should().NotBeNull(); - result.EncryptedBytes.Should().NotBeEmpty(); - result.EncryptedBytes.Length.Should().Be(audioData.Length); - result.IV.Should().NotBeEmpty(); - result.IV.Length.Should().Be(12); // AES-GCM nonce is 12 bytes - result.AuthTag.Should().NotBeEmpty(); - result.AuthTag.Length.Should().Be(16); // AES-GCM tag is 16 bytes - result.KeyId.Should().Be("default"); - result.Algorithm.Should().Be("AES-256-GCM"); - result.EncryptedAt.Should().BeCloseTo(DateTime.UtcNow, TimeSpan.FromSeconds(1)); - } - - [Fact] - public async Task EncryptAudioAsync_WithMetadata_IncludesEncryptedMetadata() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data"); - var metadata = new AudioEncryptionMetadata - { - Format = "mp3", - OriginalSize = 1024, - DurationSeconds = 10.5, - VirtualKey = "test-key", - CustomProperties = new() { ["artist"] = "Test Artist" } - }; - - // Act - var result = await _service.EncryptAudioAsync(audioData, metadata); - - // Assert - result.EncryptedMetadata.Should().NotBeNullOrEmpty(); - - // Verify metadata can be decoded - var decodedMetadata = Convert.FromBase64String(result.EncryptedMetadata); - var metadataJson = Encoding.UTF8.GetString(decodedMetadata); - metadataJson.Should().Contain("mp3"); - metadataJson.Should().Contain("1024"); - metadataJson.Should().Contain("test-key"); - } - - [Fact] - public async Task EncryptAudioAsync_WithNullData_ThrowsArgumentException() - { - // Act & Assert - var act = () => _service.EncryptAudioAsync(null!); - await act.Should().ThrowAsync() - .WithParameterName("audioData") - .WithMessage("*cannot be null or empty*"); - } - - [Fact] - public async Task EncryptAudioAsync_WithEmptyData_ThrowsArgumentException() - { - // Act & Assert - var act = () => _service.EncryptAudioAsync(Array.Empty()); - await act.Should().ThrowAsync() - .WithParameterName("audioData") - .WithMessage("*cannot be null or empty*"); - } - - [Fact] - public async Task EncryptAudioAsync_LogsInformation() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data"); - - // Act - await _service.EncryptAudioAsync(audioData); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Encrypted audio data")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task EncryptAudioAsync_ProducesUniqueIVsEachTime() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data"); - - // Act - var result1 = await _service.EncryptAudioAsync(audioData); - var result2 = await _service.EncryptAudioAsync(audioData); - var result3 = await _service.EncryptAudioAsync(audioData); - - // Assert - result1.IV.Should().NotBeEquivalentTo(result2.IV); - result2.IV.Should().NotBeEquivalentTo(result3.IV); - result1.IV.Should().NotBeEquivalentTo(result3.IV); - } - - [Fact] - public async Task EncryptAudioAsync_WithSameData_ProducesDifferentCiphertext() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data"); - - // Act - var result1 = await _service.EncryptAudioAsync(audioData); - var result2 = await _service.EncryptAudioAsync(audioData); - - // Assert - result1.EncryptedBytes.Should().NotBeEquivalentTo(result2.EncryptedBytes); - } - - [Fact] - public async Task EncryptAudioAsync_WithCancellationToken_RespectsCancellation() - { - // Arrange - var audioData = Encoding.UTF8.GetBytes("Test audio data"); - using var cts = new CancellationTokenSource(); - - // Act - Note: Current implementation doesn't actually check cancellation token - // but we test the interface compliance - var result = await _service.EncryptAudioAsync(audioData, null, cts.Token); - - // Assert - result.Should().NotBeNull(); - } - - [Theory] - [InlineData(1)] - [InlineData(100)] - [InlineData(1000)] - [InlineData(10000)] - public async Task EncryptDecrypt_WithVariousSizes_WorksCorrectly(int size) - { - // Arrange - var audioData = new byte[size]; - new Random().NextBytes(audioData); - - // Act - var encryptedData = await _service.EncryptAudioAsync(audioData); - var decryptedData = await _service.DecryptAudioAsync(encryptedData); - - // Assert - decryptedData.Should().BeEquivalentTo(audioData); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.KeyAndIntegrity.cs b/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.KeyAndIntegrity.cs deleted file mode 100644 index 36cd1808e..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioEncryptionServiceTests.KeyAndIntegrity.cs +++ /dev/null @@ -1,108 +0,0 @@ -using System.Text; - -using FluentAssertions; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioEncryptionServiceTests - { - [Fact] - public async Task GenerateKeyAsync_ReturnsNewKeyId() - { - // Act - var keyId1 = await _service.GenerateKeyAsync(); - var keyId2 = await _service.GenerateKeyAsync(); - - // Assert - keyId1.Should().NotBeNullOrEmpty(); - keyId2.Should().NotBeNullOrEmpty(); - keyId1.Should().NotBe(keyId2); - Guid.TryParse(keyId1, out _).Should().BeTrue(); - Guid.TryParse(keyId2, out _).Should().BeTrue(); - } - - [Fact] - public async Task GenerateKeyAsync_LogsInformation() - { - // Act - await _service.GenerateKeyAsync(); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Generated new encryption key")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task ValidateIntegrityAsync_WithValidData_ReturnsTrue() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Act - var isValid = await _service.ValidateIntegrityAsync(encryptedData); - - // Assert - isValid.Should().BeTrue(); - } - - [Fact] - public async Task ValidateIntegrityAsync_WithTamperedData_ReturnsFalse() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - - // Tamper with the data - encryptedData.EncryptedBytes[0] ^= 0xFF; - - // Act - var isValid = await _service.ValidateIntegrityAsync(encryptedData); - - // Assert - isValid.Should().BeFalse(); - } - - [Fact] - public async Task ValidateIntegrityAsync_WithNullData_ReturnsFalse() - { - // Act - var isValid = await _service.ValidateIntegrityAsync(null!); - - // Assert - isValid.Should().BeFalse(); - } - - [Fact] - public async Task ValidateIntegrityAsync_LogsDebugOnFailure() - { - // Arrange - var originalData = Encoding.UTF8.GetBytes("Test audio data"); - var encryptedData = await _service.EncryptAudioAsync(originalData); - encryptedData.AuthTag[0] ^= 0xFF; // Tamper with auth tag - - // Act - await _service.ValidateIntegrityAsync(encryptedData); - - // Assert - _loggerMock.Verify( - x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Audio integrity validation failed")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Advanced.cs b/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Advanced.cs deleted file mode 100644 index bbe9a9031..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Advanced.cs +++ /dev/null @@ -1,369 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioMetricsCollectorTests - { - #region Thread Safety Tests - - [Fact] - public async Task RecordMetrics_ConcurrentOperations_HandlesCorrectly() - { - // Arrange - const int threadCount = 10; - const int metricsPerThread = 100; - var tasks = new Task[threadCount]; - - // Act - for (int i = 0; i < threadCount; i++) - { - var threadId = i; - tasks[i] = Task.Run(async () => - { - for (int j = 0; j < metricsPerThread; j++) - { - if (j % 3 == 0) - { - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = $"Provider{threadId}", - Success = true, - DurationMs = 100 + j, - AudioFormat = "mp3", - AudioDurationSeconds = 10 - }); - } - else if (j % 3 == 1) - { - await _collector.RecordTtsMetricAsync(new TtsMetric - { - Provider = $"Provider{threadId}", - Success = true, - DurationMs = 200 + j, - CharacterCount = 100, - Voice = "test-voice", - OutputFormat = "mp3" - }); - } - else - { - await _collector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = $"Provider{threadId}", - SessionId = $"session-{threadId}-{j}", - Success = true, - DurationMs = 300 + j, - SessionDurationSeconds = 60, - TurnCount = 5 - }); - } - } - }); - } - - await Task.WhenAll(tasks); - - // Assert - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - var totalExpected = threadCount * metricsPerThread; - var expectedPerType = totalExpected / 3; - - // Allow for some rounding differences - Assert.InRange(aggregated.Transcription.TotalRequests, expectedPerType - 10, expectedPerType + 10); - Assert.InRange(aggregated.TextToSpeech.TotalRequests, expectedPerType - 10, expectedPerType + 10); - Assert.InRange(aggregated.Realtime.TotalSessions, expectedPerType - 10, expectedPerType + 10); - } - - #endregion - - #region Provider Statistics Tests - - [Fact] - public async Task AggregateProviderStats_MultipleProviders_GroupsCorrectly() - { - // Arrange - var providers = new[] { "OpenAI", "Azure", "Google" }; - - foreach (var provider in providers) - { - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = provider, - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = provider, - Success = false, - DurationMs = 2000, - ErrorCode = "RATE_LIMIT", - AudioFormat = "wav", - AudioDurationSeconds = 20 - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(3, aggregated.ProviderStats.Count); - - foreach (var provider in providers) - { - Assert.True(aggregated.ProviderStats.ContainsKey(provider)); - var stats = aggregated.ProviderStats[provider]; - Assert.Equal(2, stats.RequestCount); - Assert.Equal(0.5, stats.SuccessRate); // 1 success, 1 failure - Assert.Equal(1500, stats.AverageLatencyMs); // (1000 + 2000) / 2 - Assert.True(stats.ErrorBreakdown.ContainsKey("RATE_LIMIT")); - Assert.Equal(1, stats.ErrorBreakdown["RATE_LIMIT"]); - } - } - - [Fact] - public async Task ProviderUptime_CalculatedCorrectly() - { - // Arrange - for (int i = 0; i < 10; i++) - { - await _collector.RecordProviderHealthMetricAsync(new ProviderHealthMetric - { - Provider = "OpenAI", - IsHealthy = i < 8, // 8 healthy, 2 unhealthy - ResponseTimeMs = 100, - ErrorRate = i < 8 ? 0.01 : 0.5 - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - // Provider stats might not contain OpenAI if no audio metrics were recorded - // Only check if it exists - if (aggregated.ProviderStats.ContainsKey("OpenAI")) - { - Assert.Equal(80, aggregated.ProviderStats["OpenAI"].UptimePercentage); // 8/10 * 100 - } - } - - #endregion - - #region Cache Hit Rate Tests - - [Fact] - public async Task CacheHitRate_TranscriptionMetrics_CalculatedCorrectly() - { - // Arrange - for (int i = 0; i < 10; i++) - { - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 100, - ServedFromCache = i % 2 == 0, // Half from cache - AudioFormat = "mp3", - AudioDurationSeconds = 10 - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(0.5, aggregated.Transcription.CacheHitRate); - } - - [Fact] - public async Task CacheHitRate_TtsMetrics_CalculatedCorrectly() - { - // Arrange - for (int i = 0; i < 8; i++) - { - await _collector.RecordTtsMetricAsync(new TtsMetric - { - Provider = "ElevenLabs", - Success = true, - DurationMs = 200, - ServedFromCache = i < 6, // 6 from cache, 2 not - CharacterCount = 100, - Voice = "Rachel", - OutputFormat = "mp3" - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(0.75, aggregated.TextToSpeech.CacheHitRate); // 6/8 - } - - #endregion - - #region Data Size Tracking Tests - - [Fact] - public async Task TotalDataBytes_Transcription_CalculatedCorrectly() - { - // Arrange - var sizes = new long[] { 1000000, 2000000, 3000000 }; - - foreach (var size in sizes) - { - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - FileSizeBytes = size, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(6000000, aggregated.Transcription.TotalDataBytes); - } - - [Fact] - public async Task TotalDataBytes_Tts_CalculatedCorrectly() - { - // Arrange - var sizes = new long[] { 500000, 750000, 1000000 }; - - foreach (var size in sizes) - { - await _collector.RecordTtsMetricAsync(new TtsMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1500, - OutputSizeBytes = size, - CharacterCount = 1000, - Voice = "alloy", - OutputFormat = "mp3" - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(2250000, aggregated.TextToSpeech.TotalDataBytes); - } - - #endregion - - #region Edge Cases and Error Scenarios - - [Fact] - public async Task GetAggregatedMetricsAsync_NoMetrics_ReturnsEmptyAggregation() - { - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(0, aggregated.Transcription.TotalRequests); - Assert.Equal(0, aggregated.TextToSpeech.TotalRequests); - Assert.Equal(0, aggregated.Realtime.TotalSessions); - Assert.Empty(aggregated.ProviderStats); - Assert.Equal(0m, aggregated.Costs.TotalCost); - } - - [Fact] - public async Task GetAggregatedMetricsAsync_FutureDateRange_ReturnsEmpty() - { - // Arrange - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddDays(1), - DateTime.UtcNow.AddDays(2)); - - // Assert - Assert.Equal(0, aggregated.Transcription.TotalRequests); - } - - [Fact] - public async Task RecordMetrics_NullAlertingService_HandlesGracefully() - { - // Arrange - var collector = new AudioMetricsCollector( - _loggerMock.Object, - Options.Create(_options), - null); // No alerting service - - var metric = new ProviderHealthMetric - { - Provider = "OpenAI", - IsHealthy = false, - ErrorRate = 0.9 - }; - - // Act & Assert - should not throw - await collector.RecordProviderHealthMetricAsync(metric); - } - - [Fact] - public async Task Percentile_SingleValue_ReturnsValue() - { - // Arrange - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(1000, aggregated.Transcription.P95DurationMs); - Assert.Equal(1000, aggregated.Transcription.P99DurationMs); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Aggregation.cs b/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Aggregation.cs deleted file mode 100644 index 314bfde30..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Aggregation.cs +++ /dev/null @@ -1,376 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Options; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioMetricsCollectorTests - { - #region GetAggregatedMetricsAsync Tests - - [Fact] - public async Task GetAggregatedMetricsAsync_MultipleMetrics_AggregatesCorrectly() - { - // Arrange - var now = DateTime.UtcNow; - - // Record various metrics - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioDurationSeconds = 60, - FileSizeBytes = 1000000, - WordCount = 100, - AudioFormat = "mp3" - }); - - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = false, - DurationMs = 2000, - ErrorCode = "TIMEOUT", - AudioDurationSeconds = 30, - AudioFormat = "wav" - }); - - await _collector.RecordTtsMetricAsync(new TtsMetric - { - Provider = "ElevenLabs", - Success = true, - DurationMs = 1500, - CharacterCount = 500, - OutputSizeBytes = 50000, - Voice = "Rachel", - OutputFormat = "mp3" - }); - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - now.AddMinutes(-5), - now.AddMinutes(5)); - - // Assert - Assert.Equal(2, aggregated.Transcription.TotalRequests); - Assert.Equal(1, aggregated.Transcription.SuccessfulRequests); - Assert.Equal(1, aggregated.Transcription.FailedRequests); - Assert.Equal(1, aggregated.TextToSpeech.TotalRequests); - Assert.Equal(1, aggregated.TextToSpeech.SuccessfulRequests); - Assert.True(aggregated.Transcription.AverageDurationMs > 0); - } - - [Fact] - public async Task GetAggregatedMetricsAsync_WithProviderFilter_FiltersCorrectly() - { - // Arrange - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "Azure", - Success = true, - DurationMs = 1500, - AudioFormat = "wav", - AudioDurationSeconds = 45 - }); - - // Act - var openAiMetrics = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5), - "OpenAI"); - - // Assert - Assert.Equal(1, openAiMetrics.Transcription.TotalRequests); - Assert.True(openAiMetrics.ProviderStats.ContainsKey("OpenAI")); - Assert.False(openAiMetrics.ProviderStats.ContainsKey("Azure")); - } - - [Fact] - public async Task GetAggregatedMetricsAsync_CalculatesPercentiles_Correctly() - { - // Arrange - var durations = new[] { 100, 200, 300, 400, 500, 600, 700, 800, 900, 1000 }; - - foreach (var duration in durations) - { - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = duration, - AudioFormat = "mp3", - AudioDurationSeconds = 10 - }); - } - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(550, aggregated.Transcription.AverageDurationMs); - Assert.Equal(1000, aggregated.Transcription.P95DurationMs); // 95th percentile of 10 values (ceiling calculation) - Assert.Equal(1000, aggregated.Transcription.P99DurationMs); - } - - [Fact] - public async Task GetAggregatedMetricsAsync_RealtimeMetrics_AggregatesSessionData() - { - // Arrange - await _collector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = "OpenAI", - SessionId = "session-1", - Success = true, - DurationMs = 5000, - SessionDurationSeconds = 300, - TurnCount = 10, - TotalAudioSentSeconds = 120, - TotalAudioReceivedSeconds = 150, - AverageLatencyMs = 100, - DisconnectReason = "user_disconnected" - }); - - await _collector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = "OpenAI", - SessionId = "session-2", - Success = true, - DurationMs = 3000, - SessionDurationSeconds = 180, - TurnCount = 5, - TotalAudioSentSeconds = 60, - TotalAudioReceivedSeconds = 80, - AverageLatencyMs = 120, - DisconnectReason = "timeout" - }); - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(2, aggregated.Realtime.TotalSessions); - Assert.Equal(240, aggregated.Realtime.AverageSessionDurationSeconds); // (300+180)/2 - Assert.InRange(aggregated.Realtime.TotalAudioMinutes, 6.8, 6.9); // (120+150+60+80)/60 - Assert.Equal(110, aggregated.Realtime.AverageLatencyMs); // (100+120)/2 - Assert.Equal(2, aggregated.Realtime.DisconnectReasons.Count); - Assert.Equal(1, aggregated.Realtime.DisconnectReasons["user_disconnected"]); - Assert.Equal(1, aggregated.Realtime.DisconnectReasons["timeout"]); - } - - [Fact] - public async Task GetAggregatedMetricsAsync_CostCalculation_CalculatesCorrectly() - { - // Arrange - // Transcription: $0.006/minute - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioDurationSeconds = 600, // 10 minutes - AudioFormat = "mp3" - }); - - // TTS: $16/1M chars - await _collector.RecordTtsMetricAsync(new TtsMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 2000, - CharacterCount = 10000, - Voice = "alloy", - OutputFormat = "mp3" - }); - - // Realtime: $0.06/minute - await _collector.RecordRealtimeMetricAsync(new RealtimeMetric - { - Provider = "OpenAI", - SessionId = "session-1", - Success = true, - DurationMs = 5000, - SessionDurationSeconds = 300, // 5 minutes - TurnCount = 10 - }); - - // Act - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-5), - DateTime.UtcNow.AddMinutes(5)); - - // Assert - Assert.Equal(0.06m, aggregated.Costs.TranscriptionCost); // 10 * 0.006 - Assert.Equal(0.16m, aggregated.Costs.TextToSpeechCost); // 10000 * 0.000016 = 0.16 - Assert.Equal(0.3m, aggregated.Costs.RealtimeCost); // 5 * 0.06 - Assert.Equal(0.52m, aggregated.Costs.TotalCost); // 0.06 + 0.16 + 0.3 - } - - #endregion - - #region GetCurrentSnapshotAsync Tests - - [Fact] - public async Task GetCurrentSnapshotAsync_ReturnsCurrentState() - { - // Arrange - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - await _collector.RecordProviderHealthMetricAsync(new ProviderHealthMetric - { - Provider = "OpenAI", - IsHealthy = true, - ErrorRate = 0.01 - }); - - await _collector.RecordProviderHealthMetricAsync(new ProviderHealthMetric - { - Provider = "Azure", - IsHealthy = false, - ErrorRate = 0.5 - }); - - // Act - var snapshot = await _collector.GetCurrentSnapshotAsync(); - - // Assert - Assert.NotNull(snapshot); - Assert.True(snapshot.Timestamp <= DateTime.UtcNow); - Assert.True(snapshot.ProviderHealth.ContainsKey("OpenAI")); - Assert.True(snapshot.ProviderHealth["OpenAI"]); - Assert.False(snapshot.ProviderHealth["Azure"]); - Assert.NotNull(snapshot.Resources); - } - - [Fact] - public async Task GetCurrentSnapshotAsync_CalculatesRequestRate() - { - // Arrange - var tasks = new List(); - - for (int i = 0; i < 10; i++) - { - tasks.Add(_collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 100, - AudioFormat = "mp3", - AudioDurationSeconds = 5 - })); - } - - await Task.WhenAll(tasks); - await Task.Delay(100); // Ensure some time passes - - // Act - var snapshot = await _collector.GetCurrentSnapshotAsync(); - - // Assert - Assert.True(snapshot.RequestsPerSecond >= 0); // Can be 0 if time calculation is too fast - } - - [Fact] - public async Task GetCurrentSnapshotAsync_CalculatesErrorRate() - { - // Arrange - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - await _collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = false, - DurationMs = 2000, - ErrorCode = "TIMEOUT", - AudioFormat = "wav", - AudioDurationSeconds = 20 - }); - - // Act - var snapshot = await _collector.GetCurrentSnapshotAsync(); - - // Assert - Assert.Equal(0.5, snapshot.CurrentErrorRate); // 1 failure out of 2 requests - } - - #endregion - - #region Cleanup and Retention Tests - - [Fact(Skip = "Flaky timing test - uses aggressive 50ms timer intervals that cause race conditions in concurrent test runs")] - public async Task AggregationTimer_CleansUpOldBuckets() - { - // Arrange - var shortRetentionOptions = new AudioMetricsOptions - { - AggregationInterval = TimeSpan.FromMilliseconds(50), - RetentionPeriod = TimeSpan.FromMilliseconds(100), - TranscriptionLatencyThreshold = 5000, - RealtimeLatencyThreshold = 200 - }; - - var collector = new AudioMetricsCollector( - _loggerMock.Object, - Options.Create(shortRetentionOptions), - null); - - try - { - // Record a metric - await collector.RecordTranscriptionMetricAsync(new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 1000, - Timestamp = DateTime.UtcNow.AddMilliseconds(-200), // Old metric - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }); - - // Act - wait for cleanup - await Task.Delay(150); - - // Assert - old metrics should be cleaned up - var aggregated = await collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddSeconds(-10), - DateTime.UtcNow); - - Assert.Equal(0, aggregated.Transcription.TotalRequests); - } - finally - { - collector.Dispose(); - } - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Core.cs b/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Core.cs deleted file mode 100644 index 11f542b51..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.Core.cs +++ /dev/null @@ -1,41 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioMetricsCollectorTests : IDisposable - { - private readonly Mock> _loggerMock; - private readonly Mock _alertingServiceMock; - private readonly AudioMetricsOptions _options; - private readonly AudioMetricsCollector _collector; - - public AudioMetricsCollectorTests() - { - _loggerMock = new Mock>(); - _alertingServiceMock = new Mock(); - _options = new AudioMetricsOptions - { - AggregationInterval = TimeSpan.FromMilliseconds(100), - RetentionPeriod = TimeSpan.FromMinutes(5), - TranscriptionLatencyThreshold = 5000, - RealtimeLatencyThreshold = 200 - }; - - _collector = new AudioMetricsCollector( - _loggerMock.Object, - Options.Create(_options), - _alertingServiceMock.Object); - } - - public void Dispose() - { - _collector.Dispose(); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.RealtimeRouting.cs b/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.RealtimeRouting.cs deleted file mode 100644 index 417d4cd7e..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.RealtimeRouting.cs +++ /dev/null @@ -1,175 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioMetricsCollectorTests - { - #region RecordRealtimeMetricAsync Tests - - [Fact] - public async Task RecordRealtimeMetricAsync_ValidMetric_RecordsSuccessfully() - { - // Arrange - var metric = new RealtimeMetric - { - Provider = "OpenAI", - SessionId = "session-123", - Success = true, - DurationMs = 5000, - SessionDurationSeconds = 300, - TurnCount = 10, - TotalAudioSentSeconds = 120, - TotalAudioReceivedSeconds = 150, - AverageLatencyMs = 150, - DisconnectReason = "user_disconnected" - }; - - // Act - await _collector.RecordRealtimeMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Recorded realtime metric")), - null, - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task RecordRealtimeMetricAsync_HighLatency_LogsWarning() - { - // Arrange - var metric = new RealtimeMetric - { - Provider = "OpenAI", - SessionId = "session-456", - Success = true, - DurationMs = 5000, - AverageLatencyMs = 250, // Above threshold of 200ms - SessionDurationSeconds = 60, - TurnCount = 5 - }; - - // Act - await _collector.RecordRealtimeMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("High realtime latency detected")), - null, - It.IsAny>()), - Times.Once); - } - - #endregion - - #region RecordRoutingMetricAsync Tests - - [Fact] - public async Task RecordRoutingMetricAsync_ValidMetric_RecordsSuccessfully() - { - // Arrange - var metric = new RoutingMetric - { - Provider = "OpenAI", - Operation = AudioOperation.Transcription, - RoutingStrategy = "least-cost", - SelectedProvider = "OpenAI", - CandidateProviders = new List { "OpenAI", "Azure", "Google" }, - DecisionTimeMs = 50, - RoutingReason = "Lowest cost provider available", - Success = true, - DurationMs = 100 - }; - - // Act - await _collector.RecordRoutingMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Recorded routing metric")), - null, - It.IsAny>()), - Times.Once); - } - - #endregion - - #region RecordProviderHealthMetricAsync Tests - - [Fact] - public async Task RecordProviderHealthMetricAsync_HealthyProvider_RecordsSuccessfully() - { - // Arrange - var metric = new ProviderHealthMetric - { - Provider = "OpenAI", - IsHealthy = true, - ResponseTimeMs = 100, - ErrorRate = 0.01, - SuccessRate = 0.99, - ActiveConnections = 10, - HealthDetails = new Dictionary - { - ["api_version"] = "v1", - ["region"] = "us-east-1" - } - }; - - // Act - await _collector.RecordProviderHealthMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Recorded provider health")), - null, - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task RecordProviderHealthMetricAsync_UnhealthyProvider_TriggersAlerting() - { - // Arrange - var metric = new ProviderHealthMetric - { - Provider = "Azure", - IsHealthy = false, - ResponseTimeMs = 5000, - ErrorRate = 0.5, - SuccessRate = 0.5, - ActiveConnections = 0 - }; - - _alertingServiceMock.Setup(x => x.EvaluateMetricsAsync( - It.IsAny(), - It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - await _collector.RecordProviderHealthMetricAsync(metric); - - // Assert - wait longer for async alerting to complete - // The alerting is done in a fire-and-forget Task.Run which needs time to execute - await Task.Delay(500); // Increased from 100ms to 500ms - _alertingServiceMock.Verify(x => x.EvaluateMetricsAsync( - It.IsAny(), - It.IsAny()), - Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.TranscriptionTts.cs b/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.TranscriptionTts.cs deleted file mode 100644 index bb9e2a466..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioMetricsCollectorTests.TranscriptionTts.cs +++ /dev/null @@ -1,195 +0,0 @@ -using ConduitLLM.Core.Interfaces; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class AudioMetricsCollectorTests - { - #region RecordTranscriptionMetricAsync Tests - - [Fact] - public async Task RecordTranscriptionMetricAsync_ValidMetric_RecordsSuccessfully() - { - // Arrange - var metric = new TranscriptionMetric - { - Provider = "OpenAI", - VirtualKey = "test-key", - Success = true, - DurationMs = 1500, - AudioFormat = "mp3", - AudioDurationSeconds = 60, - FileSizeBytes = 1024000, - DetectedLanguage = "en", - Confidence = 0.95, - WordCount = 150, - ServedFromCache = false - }; - - // Act - await _collector.RecordTranscriptionMetricAsync(metric); - - // Assert - var snapshot = await _collector.GetCurrentSnapshotAsync(); - Assert.True(snapshot.ActiveTranscriptions >= 0); - _loggerMock.Verify(x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Recorded transcription metric")), - null, - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task RecordTranscriptionMetricAsync_HighLatency_LogsWarning() - { - // Arrange - var metric = new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 6000, // Above threshold - AudioFormat = "wav", - AudioDurationSeconds = 120 - }; - - // Act - await _collector.RecordTranscriptionMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Warning, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("High transcription latency detected")), - null, - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task RecordTranscriptionMetricAsync_WithCacheHit_IncrementsCacheCounter() - { - // Arrange - var metric = new TranscriptionMetric - { - Provider = "OpenAI", - Success = true, - DurationMs = 100, - ServedFromCache = true, - AudioFormat = "mp3", - AudioDurationSeconds = 30 - }; - - // Act - await _collector.RecordTranscriptionMetricAsync(metric); - await _collector.RecordTranscriptionMetricAsync(metric); - - // Assert - var aggregated = await _collector.GetAggregatedMetricsAsync( - DateTime.UtcNow.AddMinutes(-1), - DateTime.UtcNow.AddMinutes(1)); - - Assert.Equal(1.0, aggregated.Transcription.CacheHitRate); // Both served from cache - } - - [Fact] - public async Task RecordTranscriptionMetricAsync_ExceptionDuringRecording_HandlesGracefully() - { - // Arrange - var metric = new TranscriptionMetric - { - Provider = null!, // Will cause NullReferenceException - Success = true, - DurationMs = 1000 - }; - - // Act - await _collector.RecordTranscriptionMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Error recording transcription metric")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - #endregion - - #region RecordTtsMetricAsync Tests - - [Fact] - public async Task RecordTtsMetricAsync_ValidMetric_RecordsSuccessfully() - { - // Arrange - var metric = new TtsMetric - { - Provider = "ElevenLabs", - Voice = "Rachel", - Success = true, - DurationMs = 2000, - CharacterCount = 500, - OutputFormat = "mp3", - GeneratedDurationSeconds = 30, - OutputSizeBytes = 512000, - ServedFromCache = false, - UploadedToCdn = true - }; - - // Act - await _collector.RecordTtsMetricAsync(metric); - - // Assert - _loggerMock.Verify(x => x.Log( - LogLevel.Debug, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Recorded TTS metric")), - null, - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task RecordTtsMetricAsync_WithCdnUpload_TracksCdnUploads() - { - // Arrange - var metric1 = new TtsMetric - { - Provider = "OpenAI", - Voice = "alloy", - Success = true, - DurationMs = 1000, - CharacterCount = 100, - OutputFormat = "mp3", - UploadedToCdn = true - }; - - var metric2 = new TtsMetric - { - Provider = "OpenAI", - Voice = "nova", - Success = true, - DurationMs = 1500, - CharacterCount = 200, - OutputFormat = "mp3", - UploadedToCdn = false - }; - - // Act - await _collector.RecordTtsMetricAsync(metric1); - await _collector.RecordTtsMetricAsync(metric2); - - // Assert - verify CDN upload was tracked (implementation specific) - var snapshot = await _collector.GetCurrentSnapshotAsync(); - Assert.True(snapshot.ActiveTtsOperations >= 0); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/AudioStreamCacheTests.cs b/ConduitLLM.Tests/Core/Services/AudioStreamCacheTests.cs deleted file mode 100644 index 3822acdc3..000000000 --- a/ConduitLLM.Tests/Core/Services/AudioStreamCacheTests.cs +++ /dev/null @@ -1,428 +0,0 @@ -using AutoFixture; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Core.Services; -using ConduitLLM.Tests.TestHelpers; -using FluentAssertions; -using Microsoft.Extensions.Caching.Memory; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using Moq; -using Xunit.Abstractions; -using ConduitLLM.Configuration.Interfaces; - -namespace ConduitLLM.Tests.Core.Services -{ - [Trait("Category", "Unit")] - [Trait("Phase", "2")] - [Trait("Component", "Core")] - public class AudioStreamCacheTests : TestBase - { - private readonly Mock> _loggerMock; - private readonly Mock _memoryCacheMock; - private readonly Mock _distributedCacheMock; - private readonly Mock> _optionsMock; - private readonly AudioCacheOptions _options; - private readonly AudioStreamCache _cache; - private readonly Fixture _fixture; - - public AudioStreamCacheTests(ITestOutputHelper output) : base(output) - { - _loggerMock = CreateLogger(); - _memoryCacheMock = new Mock().SetupWorkingCache(); - _distributedCacheMock = MockBuilders.BuildCacheService() - .WithGetBehavior() - .WithSetBehavior() - .Build(); - - _options = new AudioCacheOptions - { - DefaultTranscriptionTtl = TimeSpan.FromMinutes(30), - DefaultTtsTtl = TimeSpan.FromMinutes(60), - MemoryCacheTtl = TimeSpan.FromMinutes(5), - MaxMemoryCacheSizeBytes = 1024 * 1024 * 100, // 100MB - StreamingChunkSizeBytes = 64 * 1024 // 64KB - }; - - _optionsMock = new Mock>(); - _optionsMock.Setup(x => x.Value).Returns(_options); - - _cache = new AudioStreamCache( - _loggerMock.Object, - _memoryCacheMock.Object, - _distributedCacheMock.Object, - _optionsMock.Object); - - _fixture = new Fixture(); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioStreamCache(null!, _memoryCacheMock.Object, _distributedCacheMock.Object, _optionsMock.Object); - act.Should().Throw().WithParameterName("logger"); - } - - [Fact] - public void Constructor_WithNullMemoryCache_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioStreamCache(_loggerMock.Object, null!, _distributedCacheMock.Object, _optionsMock.Object); - act.Should().Throw().WithParameterName("memoryCache"); - } - - [Fact] - public void Constructor_WithNullDistributedCache_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioStreamCache(_loggerMock.Object, _memoryCacheMock.Object, null!, _optionsMock.Object); - act.Should().Throw().WithParameterName("distributedCache"); - } - - [Fact] - public void Constructor_WithNullOptions_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new AudioStreamCache(_loggerMock.Object, _memoryCacheMock.Object, _distributedCacheMock.Object, null!); - act.Should().Throw().WithParameterName("options"); - } - - [Fact] - public void Constructor_WithNullOptionsValue_ThrowsArgumentNullException() - { - // Arrange - var badOptionsMock = new Mock>(); - badOptionsMock.Setup(x => x.Value).Returns((AudioCacheOptions)null!); - - // Act & Assert - var act = () => new AudioStreamCache(_loggerMock.Object, _memoryCacheMock.Object, _distributedCacheMock.Object, badOptionsMock.Object); - act.Should().Throw().WithParameterName("options"); - } - - [Fact] - public async Task CacheTranscriptionAsync_StoresInBothCaches() - { - // Arrange - var request = CreateTranscriptionRequest(); - var response = CreateTranscriptionResponse(); - var ttl = TimeSpan.FromMinutes(15); - - // Act - await _cache.CacheTranscriptionAsync(request, response, ttl); - - // Assert - _memoryCacheMock.Verify(x => x.CreateEntry(It.IsAny()), Times.Once); - _distributedCacheMock.Verify(x => x.Set( - It.IsAny(), - It.Is(r => r == response), - ttl, - It.IsAny()), Times.Once); - } - - [Fact] - public async Task CacheTranscriptionAsync_UsesDefaultTtlWhenNotSpecified() - { - // Arrange - var request = CreateTranscriptionRequest(); - var response = CreateTranscriptionResponse(); - - // Act - await _cache.CacheTranscriptionAsync(request, response); - - // Assert - _memoryCacheMock.Verify(x => x.CreateEntry(It.IsAny()), Times.Once); - _distributedCacheMock.Verify(x => x.Set( - It.IsAny(), - It.IsAny(), - _options.DefaultTranscriptionTtl, - It.IsAny()), Times.Once); - } - - [Fact] - public async Task CacheTranscriptionAsync_LogsDebugMessage() - { - // Arrange - var request = CreateTranscriptionRequest(); - var response = CreateTranscriptionResponse(); - - // Act - await _cache.CacheTranscriptionAsync(request, response); - - // Assert - _loggerMock.VerifyLog(LogLevel.Debug, "Cached transcription with key"); - } - - [Fact] - public async Task GetCachedTranscriptionAsync_WithMemoryCacheHit_ReturnsFromMemory() - { - // Arrange - var request = CreateTranscriptionRequest(); - var expectedResponse = CreateTranscriptionResponse(); - SetupMemoryCacheHit(expectedResponse); - - // Act - var result = await _cache.GetCachedTranscriptionAsync(request); - - // Assert - result.Should().NotBeNull(); - result.Should().BeSameAs(expectedResponse); - _distributedCacheMock.Verify(x => x.Get(It.IsAny()), Times.Never); - } - - [Fact] - public async Task GetCachedTranscriptionAsync_WithMemoryCacheMissButDistributedHit_ReturnsFromDistributed() - { - // Arrange - var request = CreateTranscriptionRequest(); - var expectedResponse = CreateTranscriptionResponse(); - SetupMemoryCacheMiss(); - SetupDistributedCacheHit(expectedResponse); - - // Act - var result = await _cache.GetCachedTranscriptionAsync(request); - - // Assert - result.Should().NotBeNull(); - result.Should().BeEquivalentTo(expectedResponse); - _memoryCacheMock.Verify(x => x.CreateEntry(It.IsAny()), Times.Once); // Should populate memory cache - } - - [Fact] - public async Task GetCachedTranscriptionAsync_WithBothCacheMiss_ReturnsNull() - { - // Arrange - var request = CreateTranscriptionRequest(); - SetupMemoryCacheMiss(); - SetupDistributedCacheMiss(); - - // Act - var result = await _cache.GetCachedTranscriptionAsync(request); - - // Assert - result.Should().BeNull(); - } - - [Fact] - public async Task GetCachedTranscriptionAsync_LogsAppropriateMessages() - { - // Arrange - var request = CreateTranscriptionRequest(); - SetupMemoryCacheMiss(); - SetupDistributedCacheMiss(); - - // Act - await _cache.GetCachedTranscriptionAsync(request); - - // Assert - _loggerMock.VerifyLog(LogLevel.Debug, "Transcription cache miss"); - } - - [Fact] - public async Task CacheTtsAudioAsync_StoresInBothCaches() - { - // Arrange - var request = CreateTtsRequest(); - var response = CreateTtsResponse(); - var ttl = TimeSpan.FromMinutes(45); - - // Act - await _cache.CacheTtsAudioAsync(request, response, ttl); - - // Assert - _memoryCacheMock.Verify(x => x.CreateEntry(It.IsAny()), Times.Once); - // The implementation stores TtsCacheEntry, not TextToSpeechResponse directly - _distributedCacheMock.Verify(x => x.Set( - It.IsAny(), - It.IsAny(), // Use object since TtsCacheEntry is internal - ttl, - It.IsAny()), Times.Once); - } - - [Fact] - public async Task GetStatisticsAsync_ReturnsAccurateStatistics() - { - // Arrange - // Set up some cache hits and misses - var request1 = CreateTranscriptionRequest(); - var request2 = CreateTranscriptionRequest(); - var response = CreateTranscriptionResponse(); - - SetupMemoryCacheHit(response); - await _cache.GetCachedTranscriptionAsync(request1); // Hit - - SetupMemoryCacheMiss(); - SetupDistributedCacheMiss(); - await _cache.GetCachedTranscriptionAsync(request2); // Miss - - // Act - var stats = await _cache.GetStatisticsAsync(); - - // Assert - stats.Should().NotBeNull(); - stats.TranscriptionHits.Should().Be(1); - stats.TranscriptionMisses.Should().Be(1); - stats.TranscriptionHitRate.Should().Be(0.5); - } - - [Fact] - public async Task StreamCachedAudioAsync_YieldsAudioChunks() - { - // Arrange - var cacheKey = "test-audio-key"; - var audioData = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; - var audioResponse = new TextToSpeechResponse - { - AudioData = audioData, - Duration = 1.0, - Format = "mp3" - }; - - // Create a TtsCacheEntry (now public) - var cacheEntry = new TtsCacheEntry - { - Response = audioResponse, - CachedAt = DateTime.UtcNow, - SizeBytes = audioData.Length - }; - - _distributedCacheMock.Setup(x => x.Get(cacheKey)) - .Returns(cacheEntry); - - // Act - var chunks = new List(); - await foreach (var chunk in _cache.StreamCachedAudioAsync(cacheKey)) - { - chunks.Add(chunk); - } - - // Assert - chunks.Should().NotBeEmpty(); - var reassembled = chunks.SelectMany(c => c.Data).ToArray(); - reassembled.Should().BeEquivalentTo(audioData); - } - - [Fact] - public async Task StreamCachedAudioAsync_WithNonExistentKey_YieldsEmpty() - { - // Arrange - var cacheKey = "non-existent-key"; - _distributedCacheMock.Setup(x => x.Get(cacheKey)) - .Returns((TextToSpeechResponse?)null); - - // Act - var chunks = new List(); - await foreach (var chunk in _cache.StreamCachedAudioAsync(cacheKey)) - { - chunks.Add(chunk); - } - - // Assert - chunks.Should().BeEmpty(); - } - - [Fact] - public async Task ClearExpiredAsync_ClearsExpiredEntries() - { - // Act - var result = await _cache.ClearExpiredAsync(); - - // Assert - result.Should().BeGreaterThanOrEqualTo(0); - } - - [Fact] - public async Task PreloadContentAsync_PreloadsSpecifiedContent() - { - // Arrange - var content = new PreloadContent - { - CommonPhrases = new List - { - new PreloadTtsItem - { - Text = "Hello, world!", - Voice = "alloy", - Language = "en-US", - Ttl = TimeSpan.FromHours(24) - } - } - }; - - // Act - await _cache.PreloadContentAsync(content); - - // Assert - _loggerMock.VerifyLog(LogLevel.Information, "Preloading"); - } - - private AudioTranscriptionRequest CreateTranscriptionRequest() - { - return new AudioTranscriptionRequest - { - AudioData = _fixture.Create(), - FileName = "test.mp3", - Language = "en-US" - }; - } - - private AudioTranscriptionResponse CreateTranscriptionResponse() - { - return new AudioTranscriptionResponse - { - Text = _fixture.Create(), - Language = "en-US", - Duration = 30.0 - }; - } - - private TextToSpeechRequest CreateTtsRequest() - { - return new TextToSpeechRequest - { - Input = _fixture.Create(), - Voice = "alloy", - Language = "en-US" - }; - } - - private TextToSpeechResponse CreateTtsResponse() - { - return new TextToSpeechResponse - { - AudioData = _fixture.Create(), - Duration = 10.0, - Format = "mp3" - }; - } - - private void SetupMemoryCacheHit(T value) - { - object outValue = value; - _memoryCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out outValue)) - .Returns(true); - } - - private void SetupMemoryCacheMiss() - { - object outValue = null; - _memoryCacheMock.Setup(x => x.TryGetValue(It.IsAny(), out outValue)) - .Returns(false); - } - - private void SetupDistributedCacheHit(T value) where T : class - { - _distributedCacheMock.Setup(x => x.Get(It.IsAny())) - .Returns(value); - } - - private void SetupDistributedCacheMiss() - { - _distributedCacheMock.Setup(x => x.Get(It.IsAny())) - .Returns((AudioTranscriptionResponse?)null); - _distributedCacheMock.Setup(x => x.Get(It.IsAny())) - .Returns((TextToSpeechResponse?)null); - } - - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/ProviderRegistryTests.cs b/ConduitLLM.Tests/Core/Services/ProviderRegistryTests.cs index 9d9c58959..ebaad74db 100644 --- a/ConduitLLM.Tests/Core/Services/ProviderRegistryTests.cs +++ b/ConduitLLM.Tests/Core/Services/ProviderRegistryTests.cs @@ -170,20 +170,6 @@ public void GetProvidersByFeature_WithImageGenerationFilter_ReturnsCorrectProvid Assert.Contains(imageProviders, p => p.ProviderType == ProviderType.Replicate); } - [Fact] - public void GetProvidersByFeature_WithAudioFilter_ReturnsCorrectProviders() - { - // Act - var audioProviders = _registry.GetProvidersByFeature(f => f.TextToSpeech).ToList(); - - // Assert - Assert.NotEmpty(audioProviders); - Assert.All(audioProviders, p => Assert.True(p.Capabilities.Features.TextToSpeech)); - - // Verify known audio providers - Assert.Contains(audioProviders, p => p.ProviderType == ProviderType.ElevenLabs); - Assert.Contains(audioProviders, p => p.ProviderType == ProviderType.OpenAI); - } [Fact] public void GetProvidersByFeature_WithNullPredicate_ThrowsArgumentNullException() diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Configuration.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.Configuration.cs deleted file mode 100644 index 0bb3efcc4..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Configuration.cs +++ /dev/null @@ -1,83 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class RouterServiceTests - { - #region GetRouterConfigAsync Tests - - [Fact] - public async Task GetRouterConfigAsync_ReturnsConfigFromRepository() - { - // Arrange - var expectedConfig = new RouterConfig - { - DefaultRoutingStrategy = "leastcost", - MaxRetries = 3 - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(expectedConfig); - - // Act - var result = await _service.GetRouterConfigAsync(); - - // Assert - Assert.NotNull(result); - Assert.Equal("leastcost", result.DefaultRoutingStrategy); - Assert.Equal(3, result.MaxRetries); - } - - [Fact] - public async Task GetRouterConfigAsync_WhenNoConfig_ReturnsDefaultConfig() - { - // Arrange - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync((RouterConfig?)null); - - // Act - var result = await _service.GetRouterConfigAsync(); - - // Assert - Assert.NotNull(result); - Assert.Equal("simple", result.DefaultRoutingStrategy); - Assert.Equal(3, result.MaxRetries); - } - - #endregion - - #region UpdateRouterConfigAsync Tests - - [Fact] - public async Task UpdateRouterConfigAsync_SavesConfigAndReinitializesRouter() - { - // Arrange - var newConfig = new RouterConfig - { - DefaultRoutingStrategy = "highestpriority", - MaxRetries = 10 - }; - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.UpdateRouterConfigAsync(newConfig); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync(newConfig, It.IsAny()), Times.Once); - } - - [Fact] - public async Task UpdateRouterConfigAsync_WithNullConfig_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _service.UpdateRouterConfigAsync(null!)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Constructor.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.Constructor.cs deleted file mode 100644 index db286e8cf..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Constructor.cs +++ /dev/null @@ -1,35 +0,0 @@ -using ConduitLLM.Core.Services; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class RouterServiceTests - { - #region Constructor Tests - - [Fact] - public void Constructor_WithNullRouter_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new RouterService(null!, _repositoryMock.Object, _loggerMock.Object)); - } - - [Fact] - public void Constructor_WithNullRepository_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new RouterService(_routerMock.Object, null!, _loggerMock.Object)); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new RouterService(_routerMock.Object, _repositoryMock.Object, null!)); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Fallbacks.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.Fallbacks.cs deleted file mode 100644 index 908e6f1c9..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Fallbacks.cs +++ /dev/null @@ -1,110 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class RouterServiceTests - { - #region SetFallbackModelsAsync Tests - - [Fact] - public async Task SetFallbackModelsAsync_SetsFallbacksForModel() - { - // Arrange - var existingConfig = new RouterConfig - { - Fallbacks = new Dictionary> - { - ["existing-model"] = new List { "fallback1" } - } - }; - - var fallbacks = new List { "fallback-model-1", "fallback-model-2" }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // Note: AddFallbackModels is a method on DefaultLLMRouter, not ILLMRouter - // For the service test, we'll skip this setup as it's an implementation detail - - // Act - await _service.SetFallbackModelsAsync("primary-model", fallbacks); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.Fallbacks.ContainsKey("primary-model") && - config.Fallbacks["primary-model"].Count == 2), - It.IsAny()), Times.Once); - - // Note: AddFallbackModels is a method on DefaultLLMRouter, not ILLMRouter - // The router should be configured correctly through Initialize method - } - - [Fact] - public async Task SetFallbackModelsAsync_WithEmptyPrimaryModel_ThrowsArgumentException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _service.SetFallbackModelsAsync("", new List())); - } - - #endregion - - #region GetFallbackModelsAsync Tests - - [Fact] - public async Task GetFallbackModelsAsync_ReturnsFallbacksForModel() - { - // Arrange - var existingConfig = new RouterConfig - { - Fallbacks = new Dictionary> - { - ["primary-model"] = new List { "fallback1", "fallback2" } - } - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // Act - var result = await _service.GetFallbackModelsAsync("primary-model"); - - // Assert - Assert.NotNull(result); - Assert.Equal(2, result.Count); - Assert.Contains("fallback1", result); - Assert.Contains("fallback2", result); - } - - [Fact] - public async Task GetFallbackModelsAsync_WithNoFallbacks_ReturnsEmptyList() - { - // Arrange - var existingConfig = new RouterConfig - { - Fallbacks = new Dictionary>() - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // Act - var result = await _service.GetFallbackModelsAsync("primary-model"); - - // Assert - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - - #region UpdateModelHealth Tests - - // UpdateModelHealth test removed - provider health monitoring has been removed - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Initialization.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.Initialization.cs deleted file mode 100644 index 8d4f58f3d..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Initialization.cs +++ /dev/null @@ -1,79 +0,0 @@ -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Routing; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class RouterServiceTests - { - #region InitializeRouterAsync Tests - - [Fact] - public async Task InitializeRouterAsync_WithExistingConfig_LoadsAndInitializes() - { - // Arrange - var existingConfig = new RouterConfig - { - DefaultRoutingStrategy = "roundrobin", - MaxRetries = 5, - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "gpt-4", - ModelAlias = "openai/gpt-4", - IsHealthy = true - } - } - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.InitializeRouterAsync(); - - // Assert - _repositoryMock.Verify(r => r.GetRouterConfigAsync(It.IsAny()), Times.Once); - - // Since DefaultLLMRouter doesn't implement IInitializableRouter, we need to check if it's cast properly - if (_routerMock.Object is DefaultLLMRouter defaultRouter) - { - // Verify that Initialize was called with the correct config - _loggerMock.Verify(l => l.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains("Router initialized")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - } - - [Fact] - public async Task InitializeRouterAsync_WithNoExistingConfig_CreatesDefaultConfig() - { - // Arrange - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync((RouterConfig?)null); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.InitializeRouterAsync(); - - // Assert - _repositoryMock.Verify(r => r.GetRouterConfigAsync(It.IsAny()), Times.Once); - _repositoryMock.Verify(r => r.SaveRouterConfigAsync(It.IsAny(), It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.ModelDeployments.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.ModelDeployments.cs deleted file mode 100644 index ed61b5ba4..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.ModelDeployments.cs +++ /dev/null @@ -1,249 +0,0 @@ -using ConduitLLM.Core.Models.Routing; - -using Moq; - -namespace ConduitLLM.Tests.Core.Services -{ - public partial class RouterServiceTests - { - #region AddModelDeploymentAsync Tests - - [Fact] - public async Task AddModelDeploymentAsync_AddsDeploymentToConfig() - { - // Arrange - var existingConfig = new RouterConfig - { - ModelDeployments = new List - { - new ModelDeployment { DeploymentName = "existing-model" } - } - }; - - var newDeployment = new ModelDeployment - { - DeploymentName = "new-model", - ModelAlias = "provider/new-model", - IsHealthy = true - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.AddModelDeploymentAsync(newDeployment); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.ModelDeployments.Count == 2 && - config.ModelDeployments.Any(d => d.DeploymentName == "new-model")), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task AddModelDeploymentAsync_WithNullDeployment_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _service.AddModelDeploymentAsync(null!)); - } - - [Fact] - public async Task AddModelDeploymentAsync_WithNoExistingConfig_CreatesNewConfig() - { - // Arrange - var newDeployment = new ModelDeployment - { - DeploymentName = "new-model", - ModelAlias = "provider/new-model" - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync((RouterConfig?)null); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.AddModelDeploymentAsync(newDeployment); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.ModelDeployments.Count == 1 && - config.ModelDeployments[0].DeploymentName == "new-model"), - It.IsAny()), Times.Once); - } - - #endregion - - #region UpdateModelDeploymentAsync Tests - - [Fact] - public async Task UpdateModelDeploymentAsync_UpdatesExistingDeployment() - { - // Arrange - var existingConfig = new RouterConfig - { - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "model-to-update", - ModelAlias = "provider/old-model", - Priority = 5 - } - } - }; - - var updatedDeployment = new ModelDeployment - { - DeploymentName = "model-to-update", - ModelAlias = "provider/new-model", - Priority = 1 - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.UpdateModelDeploymentAsync(updatedDeployment); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.ModelDeployments.Count == 1 && - config.ModelDeployments[0].ModelAlias == "provider/new-model" && - config.ModelDeployments[0].Priority == 1), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task UpdateModelDeploymentAsync_WithNonExistentDeployment_AddsNewDeployment() - { - // Arrange - var existingConfig = new RouterConfig - { - ModelDeployments = new List() - }; - - var updatedDeployment = new ModelDeployment - { - DeploymentName = "non-existent-model", - ModelAlias = "provider/model" - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // Act - await _service.UpdateModelDeploymentAsync(updatedDeployment); - - // Assert - // Verify that SaveRouterConfigAsync was called with the new deployment added - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.ModelDeployments.Count == 1 && - config.ModelDeployments[0].DeploymentName == "non-existent-model"), - It.IsAny()), Times.Once); - } - - #endregion - - #region RemoveModelDeploymentAsync Tests - - [Fact] - public async Task RemoveModelDeploymentAsync_RemovesDeploymentFromConfig() - { - // Arrange - var existingConfig = new RouterConfig - { - ModelDeployments = new List - { - new ModelDeployment { DeploymentName = "model-to-remove" }, - new ModelDeployment { DeploymentName = "model-to-keep" } - } - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // The RouterService checks if _router is DefaultLLMRouter at runtime - // Since we're using a mock, it won't be, so no initialization will happen - - // Act - await _service.RemoveModelDeploymentAsync("model-to-remove"); - - // Assert - _repositoryMock.Verify(r => r.SaveRouterConfigAsync( - It.Is(config => - config.ModelDeployments.Count == 1 && - config.ModelDeployments[0].DeploymentName == "model-to-keep"), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task RemoveModelDeploymentAsync_WithEmptyDeploymentName_ThrowsArgumentException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _service.RemoveModelDeploymentAsync("")); - } - - #endregion - - #region GetModelDeploymentsAsync Tests - - [Fact] - public async Task GetModelDeploymentsAsync_ReturnsAllDeployments() - { - // Arrange - var existingConfig = new RouterConfig - { - ModelDeployments = new List - { - new ModelDeployment { DeploymentName = "model1" }, - new ModelDeployment { DeploymentName = "model2" }, - new ModelDeployment { DeploymentName = "model3" } - } - }; - - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync(existingConfig); - - // Act - var result = await _service.GetModelDeploymentsAsync(); - - // Assert - Assert.NotNull(result); - Assert.Equal(3, result.Count); - Assert.Contains(result, d => d.DeploymentName == "model1"); - Assert.Contains(result, d => d.DeploymentName == "model2"); - Assert.Contains(result, d => d.DeploymentName == "model3"); - } - - [Fact] - public async Task GetModelDeploymentsAsync_WithNoConfig_ReturnsEmptyList() - { - // Arrange - _repositoryMock.Setup(r => r.GetRouterConfigAsync(It.IsAny())) - .ReturnsAsync((RouterConfig?)null); - - // Act - var result = await _service.GetModelDeploymentsAsync(); - - // Assert - Assert.NotNull(result); - Assert.Empty(result); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Setup.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.Setup.cs deleted file mode 100644 index 3798ae974..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.Setup.cs +++ /dev/null @@ -1,34 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Unit tests for the RouterService class. - /// - public partial class RouterServiceTests : TestBase - { - private readonly Mock _routerMock; - private readonly Mock _repositoryMock; - private readonly Mock> _loggerMock; - private readonly RouterService _service; - - public RouterServiceTests(ITestOutputHelper output) : base(output) - { - _routerMock = new Mock(); - _repositoryMock = new Mock(); - _loggerMock = CreateLogger(); - - _service = new RouterService( - _routerMock.Object, - _repositoryMock.Object, - _loggerMock.Object); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Services/RouterServiceTests.cs b/ConduitLLM.Tests/Core/Services/RouterServiceTests.cs deleted file mode 100644 index bce72db39..000000000 --- a/ConduitLLM.Tests/Core/Services/RouterServiceTests.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace ConduitLLM.Tests.Core.Services -{ - /// - /// Unit tests for the RouterService class. - /// Split across multiple partial class files: - /// - RouterServiceTests.Setup.cs - Setup and initialization - /// - RouterServiceTests.Constructor.cs - Constructor tests - /// - RouterServiceTests.Initialization.cs - InitializeRouterAsync tests - /// - RouterServiceTests.Configuration.cs - Configuration management tests - /// - RouterServiceTests.ModelDeployments.cs - Model deployment tests - /// - RouterServiceTests.Fallbacks.cs - Fallback model tests - /// - public partial class RouterServiceTests - { - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Core/Validation/UsageValidatorTests.cs b/ConduitLLM.Tests/Core/Validation/UsageValidatorTests.cs index baaaee2f6..fbed08b16 100644 --- a/ConduitLLM.Tests/Core/Validation/UsageValidatorTests.cs +++ b/ConduitLLM.Tests/Core/Validation/UsageValidatorTests.cs @@ -196,7 +196,6 @@ public void Validate_InvalidMediaDurations_ShouldReturnErrors() { ImageCount = 0, VideoDurationSeconds = -5.5, - AudioDurationSeconds = 0 }; // Act @@ -204,10 +203,9 @@ public void Validate_InvalidMediaDurations_ShouldReturnErrors() // Assert result.IsValid.Should().BeFalse(); - result.Errors.Should().HaveCount(3); + result.Errors.Should().HaveCount(2); result.Errors.Should().Contain("Image count must be positive"); result.Errors.Should().Contain("Video duration must be positive"); - result.Errors.Should().Contain("Audio duration must be positive"); } [Fact] @@ -271,7 +269,6 @@ public void Validate_ComplexValidUsage_ShouldReturnNoErrors() ImageCount = 2, ImageQuality = "hd", InferenceSteps = 50, - AudioDurationSeconds = 120.5m, Metadata = new Dictionary { ["cache_ttl"] = 3600, diff --git a/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs b/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs index 6d6a7d2ec..8efc5fa52 100644 --- a/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs +++ b/ConduitLLM.Tests/Http/Builders/ModelProviderMappingBuilder.cs @@ -33,9 +33,6 @@ public ModelProviderMappingBuilder() SupportsStreaming = false, SupportsVision = false, SupportsFunctionCalling = false, - SupportsAudioTranscription = false, - SupportsTextToSpeech = false, - SupportsRealtimeAudio = false, SupportsVideoGeneration = false, SupportsImageGeneration = false, SupportsEmbeddings = false @@ -178,9 +175,6 @@ public ModelProviderMappingBuilder WithFullCapabilities() _capabilities.SupportsStreaming = true; _capabilities.SupportsVision = true; _capabilities.SupportsFunctionCalling = true; - _capabilities.SupportsAudioTranscription = true; - _capabilities.SupportsTextToSpeech = true; - _capabilities.SupportsRealtimeAudio = true; _capabilities.SupportsVideoGeneration = true; _capabilities.SupportsImageGeneration = true; _capabilities.SupportsEmbeddings = true; diff --git a/ConduitLLM.Tests/Http/Controllers/AudioControllerTests.cs b/ConduitLLM.Tests/Http/Controllers/AudioControllerTests.cs deleted file mode 100644 index f11f1d664..000000000 --- a/ConduitLLM.Tests/Http/Controllers/AudioControllerTests.cs +++ /dev/null @@ -1,423 +0,0 @@ -using System.Security.Claims; -using System.Text; - -using ConduitLLM.Configuration.DTOs.Audio; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Http.Controllers; - -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Http.Controllers -{ - [Trait("Category", "Unit")] - [Trait("Component", "Http")] - public class AudioControllerTests : ControllerTestBase - { - private readonly Mock _audioRouterMock; - private readonly Mock _virtualKeyServiceMock; - private readonly Mock> _loggerMock; - private readonly AudioController _controller; - - public AudioControllerTests(ITestOutputHelper output) : base(output) - { - _audioRouterMock = new Mock(); - _virtualKeyServiceMock = new Mock(); - _loggerMock = CreateLogger(); - - var mockModelMappingService = new Mock(); - _controller = new AudioController( - _audioRouterMock.Object, - _virtualKeyServiceMock.Object, - _loggerMock.Object, - mockModelMappingService.Object); - } - - #region Constructor Tests - - [Fact] - public void Constructor_WithNullAudioRouter_ThrowsArgumentNullException() - { - // Act & Assert - var mockModelMappingService = new Mock(); - var act = () => new AudioController(null!, _virtualKeyServiceMock.Object, _loggerMock.Object, mockModelMappingService.Object); - Assert.Throws(act); - } - - [Fact] - public void Constructor_WithNullVirtualKeyService_ThrowsArgumentNullException() - { - // Act & Assert - var mockModelMappingService = new Mock(); - var act = () => new AudioController(_audioRouterMock.Object, (ConduitLLM.Configuration.Interfaces.IVirtualKeyService)null!, _loggerMock.Object, mockModelMappingService.Object); - Assert.Throws(act); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var mockModelMappingService = new Mock(); - var act = () => new AudioController(_audioRouterMock.Object, _virtualKeyServiceMock.Object, null!, mockModelMappingService.Object); - Assert.Throws(act); - } - - #endregion - - #region TranscribeAudio Tests - - [Fact] - public async Task TranscribeAudio_WithValidRequest_ReturnsTranscription() - { - // Arrange - var virtualKey = "test-virtual-key"; - var fileContent = "test audio content"; - var fileName = "test.mp3"; - var model = "whisper-1"; - - var formFile = CreateFormFile(fileContent, fileName); - var expectedResponse = new AudioTranscriptionResponse - { - Text = "This is the transcribed text", - Language = "en", - Duration = 10.5 - }; - - var mockClient = new Mock(); - mockClient.Setup(x => x.TranscribeAudioAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(expectedResponse); - - _audioRouterMock.Setup(x => x.GetTranscriptionClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny())) - .ReturnsAsync(mockClient.Object); - - // Setup controller context with authenticated user - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.TranscribeAudio(formFile, model); - - // Assert - AssertOkObjectResult(result, response => - { - Assert.Equal(expectedResponse.Text, response.Text); - Assert.Equal(expectedResponse.Language, response.Language); - Assert.Equal(expectedResponse.Duration, response.Duration); - }); - - _audioRouterMock.Verify(x => x.GetTranscriptionClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny()), Times.Once); - mockClient.Verify(x => x.TranscribeAudioAsync( - It.IsAny(), - It.IsAny(), - It.IsAny()), Times.Once); - } - - [Fact] - public async Task TranscribeAudio_WithMissingVirtualKey_ReturnsUnauthorized() - { - // Arrange - var formFile = CreateFormFile("content", "test.mp3"); - _controller.ControllerContext = CreateControllerContext(); // No authentication - - // Act - var result = await _controller.TranscribeAudio(formFile); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var problemDetails = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Unauthorized", problemDetails.Title); - Assert.Equal("Invalid or missing API key", problemDetails.Detail); - } - - [Fact] - public async Task TranscribeAudio_WithEmptyFile_ReturnsBadRequest() - { - // Arrange - var virtualKey = "test-virtual-key"; - var formFile = CreateFormFile("", "test.mp3"); // Empty content - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.TranscribeAudio(formFile); - - // Assert - var badRequestResult = Assert.IsType(result); - var problemDetails = Assert.IsType(badRequestResult.Value); - Assert.Equal("Invalid Request", problemDetails.Title); - Assert.Equal("Audio file is empty", problemDetails.Detail); - } - - [Fact] - public async Task TranscribeAudio_WithOversizedFile_ReturnsBadRequest() - { - // Arrange - var virtualKey = "test-virtual-key"; - var largeContent = new string('x', 26 * 1024 * 1024); // 26MB - var formFile = CreateFormFile(largeContent, "test.mp3"); - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.TranscribeAudio(formFile); - - // Assert - var badRequestResult = Assert.IsType(result); - var problemDetails = Assert.IsType(badRequestResult.Value); - Assert.Equal("Invalid Request", problemDetails.Title); - Assert.Contains("exceeds maximum size", problemDetails.Detail); - } - - [Fact] - public async Task TranscribeAudio_WhenRouterThrowsException_ReturnsInternalServerError() - { - // Arrange - var virtualKey = "test-virtual-key"; - var formFile = CreateFormFile("content", "test.mp3"); - - _audioRouterMock.Setup(x => x.GetTranscriptionClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny())) - .ThrowsAsync(new Exception("Router error")); - - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.TranscribeAudio(formFile); - - // Assert - var objectResult = Assert.IsType(result); - Assert.Equal(500, objectResult.StatusCode); - var problemDetails = Assert.IsType(objectResult.Value); - Assert.Equal("Internal Server Error", problemDetails.Title); - } - - #endregion - - #region TranslateAudio Tests - - [Fact] - public async Task TranslateAudio_WithValidRequest_ReturnsTranslation() - { - // Arrange - var virtualKey = "test-virtual-key"; - var fileContent = "test audio content"; - var fileName = "test.mp3"; - var model = "whisper-1"; - - var formFile = CreateFormFile(fileContent, fileName); - var expectedResponse = new AudioTranscriptionResponse // TranslateAudio returns AudioTranscriptionResponse - { - Text = "This is the translated text" - }; - - var mockClient = new Mock(); - mockClient.Setup(x => x.TranscribeAudioAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(expectedResponse); - - _audioRouterMock.Setup(x => x.GetTranscriptionClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny())) - .ReturnsAsync(mockClient.Object); - - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.TranslateAudio(formFile, model); - - // Assert - AssertOkObjectResult(result, response => - { - Assert.Equal(expectedResponse.Text, response.Text); - }); - } - - [Fact] - public async Task TranslateAudio_WithMissingVirtualKey_ReturnsUnauthorized() - { - // Arrange - var formFile = CreateFormFile("content", "test.mp3"); - _controller.ControllerContext = CreateControllerContext(); - - // Act - var result = await _controller.TranslateAudio(formFile); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var problemDetails = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Unauthorized", problemDetails.Title); - } - - #endregion - - #region GenerateSpeech Tests - - [Fact] - public async Task GenerateSpeech_WithValidRequest_ReturnsAudioStream() - { - // Arrange - var virtualKey = "test-virtual-key"; - var request = new TextToSpeechRequestDto - { - Model = "tts-1", - Input = "Hello, world!", - Voice = "alloy" - }; - - var audioContent = Encoding.UTF8.GetBytes("fake audio data"); - var ttsResponse = new TextToSpeechResponse - { - AudioData = audioContent - }; - - var mockClient = new Mock(); - mockClient.Setup(x => x.CreateSpeechAsync( - It.IsAny(), - It.IsAny(), - It.IsAny())) - .ReturnsAsync(ttsResponse); - - _audioRouterMock.Setup(x => x.GetTextToSpeechClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny())) - .ReturnsAsync(mockClient.Object); - - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.GenerateSpeech(request); - - // Assert - var fileResult = Assert.IsType(result); - Assert.Equal("audio/mpeg", fileResult.ContentType); - Assert.Equal(audioContent, fileResult.FileContents); - } - - [Fact] - public async Task GenerateSpeech_WithMissingVirtualKey_ReturnsUnauthorized() - { - // Arrange - var request = new TextToSpeechRequestDto - { - Model = "tts-1", - Input = "Hello, world!", - Voice = "alloy" - }; - _controller.ControllerContext = CreateControllerContext(); - - // Act - var result = await _controller.GenerateSpeech(request); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var problemDetails = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Unauthorized", problemDetails.Title); - } - - [Fact] - public async Task GenerateSpeech_WithEmptyInput_ReturnsBadRequest() - { - // Arrange - var virtualKey = "test-virtual-key"; - var request = new TextToSpeechRequestDto - { - Model = "tts-1", - Input = "", // Empty input - Voice = "alloy" - }; - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.GenerateSpeech(request); - - // Assert - var badRequestResult = Assert.IsType(result); - var problemDetails = Assert.IsType(badRequestResult.Value); - Assert.Equal("Input text is required", problemDetails.Detail); - } - - [Fact] - public async Task GenerateSpeech_WhenRouterThrowsException_ReturnsInternalServerError() - { - // Arrange - var virtualKey = "test-virtual-key"; - var request = new TextToSpeechRequestDto - { - Model = "tts-1", - Input = "Hello", - Voice = "alloy" - }; - - _audioRouterMock.Setup(x => x.GetTextToSpeechClientAsync( - It.IsAny(), - It.Is(k => k == virtualKey), - It.IsAny())) - .ThrowsAsync(new Exception("Router error")); - - _controller.ControllerContext = CreateAuthenticatedContext(virtualKey); - - // Act - var result = await _controller.GenerateSpeech(request); - - // Assert - var objectResult = Assert.IsType(result); - Assert.Equal(500, objectResult.StatusCode); - var problemDetails = Assert.IsType(objectResult.Value); - Assert.Equal("An error occurred while generating speech", problemDetails.Detail); - } - - #endregion - - #region Helper Methods - - private ControllerContext CreateAuthenticatedContext(string virtualKey) - { - var context = CreateControllerContext(); - - var claims = new[] - { - new Claim("VirtualKey", virtualKey) - }; - - var identity = new ClaimsIdentity(claims, "Test"); - var principal = new ClaimsPrincipal(identity); - - context.HttpContext.User = principal; - return context; - } - - private IFormFile CreateFormFile(string content, string fileName) - { - var bytes = Encoding.UTF8.GetBytes(content); - var stream = new MemoryStream(bytes); - - var formFile = new FormFile(stream, 0, bytes.Length, "file", fileName) - { - Headers = new HeaderDictionary(), - ContentType = "audio/mpeg" - }; - - return formFile; - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/Discovery/DiscoveryControllerGetCapabilitiesTests.cs b/ConduitLLM.Tests/Http/Controllers/Discovery/DiscoveryControllerGetCapabilitiesTests.cs index 5bd5f8290..4b8f8d495 100644 --- a/ConduitLLM.Tests/Http/Controllers/Discovery/DiscoveryControllerGetCapabilitiesTests.cs +++ b/ConduitLLM.Tests/Http/Controllers/Discovery/DiscoveryControllerGetCapabilitiesTests.cs @@ -26,9 +26,6 @@ public async Task GetCapabilities_ReturnsStaticListOfAllCapabilities() Assert.Contains("chat", capabilities); Assert.Contains("chat_stream", capabilities); Assert.Contains("vision", capabilities); - Assert.Contains("audio_transcription", capabilities); - Assert.Contains("text_to_speech", capabilities); - Assert.Contains("realtime_audio", capabilities); Assert.Contains("video_generation", capabilities); Assert.Contains("image_generation", capabilities); Assert.Contains("embeddings", capabilities); @@ -47,7 +44,7 @@ public async Task GetCapabilities_ReturnsCorrectNumberOfCapabilities() var okResult = Assert.IsType(result); dynamic response = okResult.Value!; var capabilities = (string[])response.capabilities; - Assert.Equal(12, capabilities.Length); + Assert.Equal(9, capabilities.Length); } // NOTE: GetCapabilities_WhenExceptionOccurs_Returns500Error test was removed diff --git a/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs b/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs index dfbf06ea6..86cab9f0a 100644 --- a/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs +++ b/ConduitLLM.Tests/Http/Controllers/Discovery/GetModels/GetModelsResponseStructureTests.cs @@ -42,9 +42,6 @@ public async Task GetModels_ReturnsFlatStructureWithBooleanCapabilityFlags() Assert.True(model.supports_streaming); Assert.True(model.supports_vision); Assert.True(model.supports_function_calling); - Assert.True(model.supports_audio_transcription); - Assert.True(model.supports_text_to_speech); - Assert.True(model.supports_realtime_audio); Assert.True(model.supports_video_generation); Assert.True(model.supports_image_generation); Assert.True(model.supports_embeddings); diff --git a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.ProcessAudio.cs b/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.ProcessAudio.cs deleted file mode 100644 index 62931d521..000000000 --- a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.ProcessAudio.cs +++ /dev/null @@ -1,292 +0,0 @@ -using System.Text; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Models.Audio; -using Microsoft.AspNetCore.Mvc; -using Moq; -using ConduitLLM.Configuration.DTOs; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class HybridAudioControllerTests - { - #region ProcessAudio Tests - - [Fact] - public async Task ProcessAudio_WithValidRequest_ShouldReturnAudioFile() - { - // Arrange - var audioContent = Encoding.UTF8.GetBytes("test audio content"); - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - - var response = new HybridAudioResponse - { - AudioData = Encoding.UTF8.GetBytes("response audio"), - AudioFormat = "mp3", - TranscribedText = "Hello", - ResponseText = "Hi there!", - DurationSeconds = 2.5, - Metrics = new ProcessingMetrics - { - InputDurationSeconds = 1.5, - OutputDurationSeconds = 2.5 - } - }; - - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.Is(r => - r.AudioData.Length == audioContent.Length && - r.AudioFormat == "mp3" && - r.Temperature == 0.7 && - r.MaxTokens == 150), - It.IsAny())) - .ReturnsAsync(response); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var fileResult = Assert.IsType(result); - Assert.Equal("audio/mpeg", fileResult.ContentType); - Assert.Equal("response.mp3", fileResult.FileDownloadName); - Assert.Equal(response.AudioData, fileResult.FileContents); - } - - [Fact] - public async Task ProcessAudio_WithAllParameters_ShouldPassCorrectRequest() - { - // Arrange - var audioContent = Encoding.UTF8.GetBytes("test audio"); - var formFile = CreateFormFile("test.wav", audioContent, "audio/wav"); - var sessionId = "session-123"; - var language = "es"; - var systemPrompt = "Be helpful"; - var voiceId = "voice-1"; - var outputFormat = "wav"; - var temperature = 1.2; - var maxTokens = 300; - - var response = new HybridAudioResponse - { - AudioData = new byte[] { 1, 2, 3 }, - AudioFormat = outputFormat - }; - - HybridAudioRequest capturedRequest = null; - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.IsAny(), - It.IsAny())) - .Callback((req, _) => capturedRequest = req) - .ReturnsAsync(response); - - // Act - var result = await _controller.ProcessAudio( - formFile, - sessionId, - language, - systemPrompt, - voiceId, - outputFormat, - temperature, - maxTokens); - - // Assert - Assert.IsType(result); - Assert.NotNull(capturedRequest); - Assert.Equal(sessionId, capturedRequest.SessionId); - Assert.Equal("wav", capturedRequest.AudioFormat); - Assert.Equal(language, capturedRequest.Language); - Assert.Equal(systemPrompt, capturedRequest.SystemPrompt); - Assert.Equal(voiceId, capturedRequest.VoiceId); - Assert.Equal(outputFormat, capturedRequest.OutputFormat); - Assert.Equal(temperature, capturedRequest.Temperature); - Assert.Equal(maxTokens, capturedRequest.MaxTokens); - Assert.False(capturedRequest.EnableStreaming); - } - - [Fact] - public async Task ProcessAudio_WithoutFile_ShouldReturnBadRequest() - { - // Arrange & Act - var result = await _controller.ProcessAudio(null); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal("No audio file provided", errorResponse.error.ToString()); - } - - [Fact] - public async Task ProcessAudio_WithEmptyFile_ShouldReturnBadRequest() - { - // Arrange - var formFile = CreateFormFile("empty.mp3", Array.Empty(), "audio/mpeg"); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal("No audio file provided", errorResponse.error.ToString()); - } - - [Fact] - public async Task ProcessAudio_WithVirtualKey_ShouldCheckPermissions() - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - var virtualKey = "vk-test-key"; - - _controller.HttpContext.Items["ApiKey"] = virtualKey; - - var keyEntity = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyHash = "test-hash" - }; - - _mockVirtualKeyService.Setup(x => x.GetVirtualKeyByKeyValueAsync(virtualKey)) - .ReturnsAsync(keyEntity); - - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.IsAny(), - It.IsAny())) - .ReturnsAsync(new HybridAudioResponse { AudioData = new byte[] { 4, 5, 6 }, AudioFormat = "mp3" }); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - Assert.IsType(result); - _mockVirtualKeyService.Verify(x => x.GetVirtualKeyByKeyValueAsync(virtualKey), Times.Once); - } - - [Fact] - public async Task ProcessAudio_WithInvalidVirtualKey_ShouldReturnForbidden() - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - var virtualKey = "vk-invalid-key"; - - _controller.HttpContext.Items["ApiKey"] = virtualKey; - - _mockVirtualKeyService.Setup(x => x.GetVirtualKeyByKeyValueAsync(virtualKey)) - .ReturnsAsync((VirtualKey)null); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var forbidResult = Assert.IsType(result); - Assert.Equal("Virtual key is not valid or enabled", forbidResult.AuthenticationSchemes[0]); - } - - [Fact] - public async Task ProcessAudio_WithDisabledVirtualKey_ShouldReturnForbidden() - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - var virtualKey = "vk-disabled-key"; - - _controller.HttpContext.Items["ApiKey"] = virtualKey; - - var keyEntity = new VirtualKey - { - Id = 1, - IsEnabled = false, - KeyHash = "test-hash" - }; - - _mockVirtualKeyService.Setup(x => x.GetVirtualKeyByKeyValueAsync(virtualKey)) - .ReturnsAsync(keyEntity); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var forbidResult = Assert.IsType(result); - Assert.Equal("Virtual key is not valid or enabled", forbidResult.AuthenticationSchemes[0]); - } - - [Fact] - public async Task ProcessAudio_WithArgumentException_ShouldReturnBadRequest() - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - var errorMessage = "Invalid audio format"; - - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new ArgumentException(errorMessage)); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal(errorMessage, errorResponse.error.ToString()); - } - - [Fact] - public async Task ProcessAudio_WithGeneralException_ShouldReturn500() - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile("test.mp3", audioContent, "audio/mpeg"); - - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new Exception("Processing failed")); - - // Act - var result = await _controller.ProcessAudio(formFile); - - // Assert - var statusCodeResult = Assert.IsType(result); - Assert.Equal(500, statusCodeResult.StatusCode); - var errorResponse = Assert.IsType(statusCodeResult.Value); - Assert.Equal("An error occurred processing the audio", errorResponse.error.ToString()); - } - - [Theory] - [InlineData("audio/mpeg", "test.mp3", "mp3")] - [InlineData("audio/wav", "test.wav", "wav")] - [InlineData("audio/webm", "test.webm", "webm")] - [InlineData("audio/flac", "test.flac", "flac")] - [InlineData("audio/ogg", "test.ogg", "ogg")] - [InlineData("application/octet-stream", "test.mp3", "mp3")] - [InlineData(null, "test.wav", "wav")] - [InlineData("unknown/type", "test.mp3", "mp3")] - [InlineData("unknown/type", "test", "mp3")] // Test edge case - file with no extension defaults to mp3 - public async Task ProcessAudio_ShouldDetectCorrectAudioFormat(string contentType, string fileName, string expectedFormat) - { - // Arrange - var audioContent = new byte[] { 1, 2, 3 }; - var formFile = CreateFormFile(fileName, audioContent, contentType); - - HybridAudioRequest capturedRequest = null; - _mockHybridAudioService.Setup(x => x.ProcessAudioAsync( - It.IsAny(), - It.IsAny())) - .Callback((req, _) => capturedRequest = req) - .ReturnsAsync(new HybridAudioResponse { AudioData = new byte[] { 4, 5, 6 }, AudioFormat = "mp3" }); - - // Act - await _controller.ProcessAudio(formFile); - - // Assert - Assert.NotNull(capturedRequest); - Assert.Equal(expectedFormat, capturedRequest.AudioFormat); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.Session.cs b/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.Session.cs deleted file mode 100644 index 108463395..000000000 --- a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.Session.cs +++ /dev/null @@ -1,195 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Http.Controllers; -using Microsoft.AspNetCore.Mvc; -using Moq; -using ConduitLLM.Configuration.DTOs; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class HybridAudioControllerTests - { - #region CreateSession Tests - - [Fact] - public async Task CreateSession_WithValidConfig_ShouldReturnSessionId() - { - // Arrange - var config = new HybridSessionConfig - { - SttProvider = "whisper", - LlmModel = "gpt-4", - TtsProvider = "elevenlabs", - SystemPrompt = "Be helpful", - DefaultVoice = "voice-1" - }; - - var sessionId = "session-" + Guid.NewGuid(); - _mockHybridAudioService.Setup(x => x.CreateSessionAsync(config, It.IsAny())) - .ReturnsAsync(sessionId); - - // Act - var result = await _controller.CreateSession(config); - - // Assert - var okResult = Assert.IsType(result); - var response = Assert.IsType(okResult.Value); - Assert.Equal(sessionId, response.SessionId); - } - - [Fact] - public async Task CreateSession_WithVirtualKey_ShouldCheckPermissions() - { - // Arrange - var config = new HybridSessionConfig(); - var virtualKey = "vk-test-key"; - - _controller.HttpContext.Items["ApiKey"] = virtualKey; - - var keyEntity = new VirtualKey - { - Id = 1, - IsEnabled = true, - KeyHash = "test-hash" - }; - - _mockVirtualKeyService.Setup(x => x.GetVirtualKeyByKeyValueAsync(virtualKey)) - .ReturnsAsync(keyEntity); - - _mockHybridAudioService.Setup(x => x.CreateSessionAsync( - It.IsAny(), - It.IsAny())) - .ReturnsAsync("session-123"); - - // Act - var result = await _controller.CreateSession(config); - - // Assert - Assert.IsType(result); - _mockVirtualKeyService.Verify(x => x.GetVirtualKeyByKeyValueAsync(virtualKey), Times.Once); - } - - [Fact] - public async Task CreateSession_WithInvalidVirtualKey_ShouldReturnForbidden() - { - // Arrange - var config = new HybridSessionConfig(); - var virtualKey = "vk-invalid-key"; - - _controller.HttpContext.Items["ApiKey"] = virtualKey; - - _mockVirtualKeyService.Setup(x => x.GetVirtualKeyByKeyValueAsync(virtualKey)) - .ReturnsAsync((VirtualKey)null); - - // Act - var result = await _controller.CreateSession(config); - - // Assert - var forbidResult = Assert.IsType(result); - Assert.Equal("Virtual key is not valid or enabled", forbidResult.AuthenticationSchemes[0]); - } - - [Fact] - public async Task CreateSession_WithArgumentException_ShouldReturnBadRequest() - { - // Arrange - var config = new HybridSessionConfig(); - var errorMessage = "Invalid configuration"; - - _mockHybridAudioService.Setup(x => x.CreateSessionAsync( - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new ArgumentException(errorMessage)); - - // Act - var result = await _controller.CreateSession(config); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal(errorMessage, errorResponse.error.ToString()); - } - - [Fact] - public async Task CreateSession_WithGeneralException_ShouldReturn500() - { - // Arrange - var config = new HybridSessionConfig(); - - _mockHybridAudioService.Setup(x => x.CreateSessionAsync( - It.IsAny(), - It.IsAny())) - .ThrowsAsync(new Exception("Service error")); - - // Act - var result = await _controller.CreateSession(config); - - // Assert - var statusCodeResult = Assert.IsType(result); - Assert.Equal(500, statusCodeResult.StatusCode); - var errorResponse = Assert.IsType(statusCodeResult.Value); - Assert.Equal("An error occurred creating the session", errorResponse.error.ToString()); - } - - #endregion - - #region CloseSession Tests - - [Fact] - public async Task CloseSession_WithValidSessionId_ShouldReturnNoContent() - { - // Arrange - var sessionId = "session-123"; - - _mockHybridAudioService.Setup(x => x.CloseSessionAsync(sessionId, It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - var result = await _controller.CloseSession(sessionId); - - // Assert - Assert.IsType(result); - _mockHybridAudioService.Verify(x => x.CloseSessionAsync(sessionId, It.IsAny()), Times.Once); - } - - [Fact] - public async Task CloseSession_WithArgumentException_ShouldReturnBadRequest() - { - // Arrange - var sessionId = "invalid-session"; - var errorMessage = "Session not found"; - - _mockHybridAudioService.Setup(x => x.CloseSessionAsync(sessionId, It.IsAny())) - .ThrowsAsync(new ArgumentException(errorMessage)); - - // Act - var result = await _controller.CloseSession(sessionId); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal(errorMessage, errorResponse.error.ToString()); - } - - [Fact] - public async Task CloseSession_WithGeneralException_ShouldReturn500() - { - // Arrange - var sessionId = "session-123"; - - _mockHybridAudioService.Setup(x => x.CloseSessionAsync(sessionId, It.IsAny())) - .ThrowsAsync(new Exception("Service error")); - - // Act - var result = await _controller.CloseSession(sessionId); - - // Assert - var statusCodeResult = Assert.IsType(result); - Assert.Equal(500, statusCodeResult.StatusCode); - var errorResponse = Assert.IsType(statusCodeResult.Value); - Assert.Equal("An error occurred closing the session", errorResponse.error.ToString()); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.StatusAndConstructor.cs b/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.StatusAndConstructor.cs deleted file mode 100644 index 3983dc519..000000000 --- a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.StatusAndConstructor.cs +++ /dev/null @@ -1,136 +0,0 @@ -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Http.Controllers; - -using Microsoft.AspNetCore.Mvc; - -using Moq; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class HybridAudioControllerTests - { - #region GetStatus Tests - - [Fact] - public async Task GetStatus_WhenServiceAvailable_ShouldReturnStatusWithMetrics() - { - // Arrange - var metrics = new HybridLatencyMetrics - { - AverageSttLatencyMs = 100, - AverageLlmLatencyMs = 200, - AverageTtsLatencyMs = 150, - AverageTotalLatencyMs = 450, - P95LatencyMs = 600, - P99LatencyMs = 800, - SampleCount = 100 - }; - - _mockHybridAudioService.Setup(x => x.IsAvailableAsync(It.IsAny())) - .ReturnsAsync(true); - _mockHybridAudioService.Setup(x => x.GetLatencyMetricsAsync(It.IsAny())) - .ReturnsAsync(metrics); - - // Act - var result = await _controller.GetStatus(); - - // Assert - var okResult = Assert.IsType(result); - var status = Assert.IsType(okResult.Value); - Assert.True(status.Available); - Assert.NotNull(status.LatencyMetrics); - Assert.Equal(metrics.AverageTotalLatencyMs, status.LatencyMetrics.AverageTotalLatencyMs); - } - - [Fact] - public async Task GetStatus_WhenServiceUnavailable_ShouldReturnUnavailableStatus() - { - // Arrange - _mockHybridAudioService.Setup(x => x.IsAvailableAsync(It.IsAny())) - .ReturnsAsync(false); - _mockHybridAudioService.Setup(x => x.GetLatencyMetricsAsync(It.IsAny())) - .ReturnsAsync(new HybridLatencyMetrics()); - - // Act - var result = await _controller.GetStatus(); - - // Assert - var okResult = Assert.IsType(result); - var status = Assert.IsType(okResult.Value); - Assert.False(status.Available); - } - - [Fact] - public async Task GetStatus_WhenExceptionOccurs_ShouldReturnUnavailableStatus() - { - // Arrange - _mockHybridAudioService.Setup(x => x.IsAvailableAsync(It.IsAny())) - .ThrowsAsync(new Exception("Service check failed")); - - // Act - var result = await _controller.GetStatus(); - - // Assert - var okResult = Assert.IsType(result); - var status = Assert.IsType(okResult.Value); - Assert.False(status.Available); - Assert.Null(status.LatencyMetrics); - } - - #endregion - - #region Constructor Tests - - [Fact] - public void Constructor_WithNullHybridAudioService_ShouldThrowArgumentNullException() - { - // Arrange & Act & Assert - var ex = Assert.Throws(() => new HybridAudioController( - null, - _mockVirtualKeyService.Object, - _mockLogger.Object)); - Assert.Equal("hybridAudioService", ex.ParamName); - } - - [Fact] - public void Constructor_WithNullVirtualKeyService_ShouldThrowArgumentNullException() - { - // Arrange & Act & Assert - var ex = Assert.Throws(() => new HybridAudioController( - _mockHybridAudioService.Object, - null, - _mockLogger.Object)); - Assert.Equal("virtualKeyService", ex.ParamName); - } - - [Fact] - public void Constructor_WithNullLogger_ShouldThrowArgumentNullException() - { - // Arrange & Act & Assert - var ex = Assert.Throws(() => new HybridAudioController( - _mockHybridAudioService.Object, - _mockVirtualKeyService.Object, - null)); - Assert.Equal("logger", ex.ParamName); - } - - #endregion - - #region Authorization Tests - - [Fact] - public void Controller_ShouldRequireAuthorization() - { - // Arrange & Act - var controllerType = typeof(HybridAudioController); - var authorizeAttribute = Attribute.GetCustomAttribute(controllerType, typeof(Microsoft.AspNetCore.Authorization.AuthorizeAttribute)); - - // Assert - Assert.NotNull(authorizeAttribute); - var authAttribute = (Microsoft.AspNetCore.Authorization.AuthorizeAttribute)authorizeAttribute; - Assert.Equal("VirtualKey", authAttribute.AuthenticationSchemes); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.cs b/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.cs deleted file mode 100644 index 560004b31..000000000 --- a/ConduitLLM.Tests/Http/Controllers/HybridAudioControllerTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Http.Controllers; - -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Http.Controllers -{ - [Trait("Category", "Unit")] - [Trait("Component", "Http")] - [Trait("Phase", "2")] - public partial class HybridAudioControllerTests : ControllerTestBase - { - private readonly Mock _mockHybridAudioService; - private readonly Mock _mockVirtualKeyService; - private readonly Mock> _mockLogger; - private readonly HybridAudioController _controller; - - public HybridAudioControllerTests(ITestOutputHelper output) : base(output) - { - _mockHybridAudioService = new Mock(); - _mockVirtualKeyService = new Mock(); - _mockLogger = CreateLogger(); - - _controller = new HybridAudioController( - _mockHybridAudioService.Object, - _mockVirtualKeyService.Object, - _mockLogger.Object); - - _controller.ControllerContext = CreateControllerContext(); - } - - - - - #region Helper Methods - - private IFormFile CreateFormFile(string fileName, byte[] content, string contentType) - { - var stream = new MemoryStream(content); - var formFile = new FormFile(stream, 0, content.Length, "file", fileName) - { - Headers = new HeaderDictionary(), - ContentType = contentType - }; - return formFile; - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/ModelsControllerTests.cs b/ConduitLLM.Tests/Http/Controllers/ModelsControllerTests.cs deleted file mode 100644 index 97be859e4..000000000 --- a/ConduitLLM.Tests/Http/Controllers/ModelsControllerTests.cs +++ /dev/null @@ -1,281 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Http.Controllers; -using ConduitLLM.Http.Services; - -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public class ModelsControllerTests : ControllerTestBase - { - private readonly Mock _mockRouter; - private readonly Mock> _mockLogger; - private readonly Mock _mockMetadataService; - private readonly ModelsController _controller; - - public ModelsControllerTests(ITestOutputHelper output) : base(output) - { - _mockRouter = new Mock(); - _mockLogger = CreateLogger(); - _mockMetadataService = new Mock(); - - _controller = new ModelsController( - _mockRouter.Object, - _mockLogger.Object, - _mockMetadataService.Object); - - _controller.ControllerContext = CreateControllerContext(); - } - - #region ListModels Tests - - [Fact] - public void ListModels_ReturnsAvailableModels() - { - // Arrange - var models = new List { "gpt-4", "gpt-3.5-turbo", "dall-e-3" }; - _mockRouter.Setup(x => x.GetAvailableModels()).Returns(models); - - // Act - var result = _controller.ListModels(); - - // Assert - var okResult = Assert.IsType(result); - dynamic response = okResult.Value!; - - Assert.Equal("list", response.@object); - Assert.NotNull(response.data); - - // Count the items in the data array - int count = 0; - foreach (var item in response.data) - { - count++; - Assert.NotNull(item.id); - Assert.Equal("model", item.@object); - } - Assert.Equal(3, count); - } - - [Fact] - public void ListModels_WhenExceptionThrown_Returns500() - { - // Arrange - _mockRouter.Setup(x => x.GetAvailableModels()) - .Throws(new Exception("Test exception")); - - // Act - var result = _controller.ListModels(); - - // Assert - var objectResult = Assert.IsType(result); - Assert.Equal(500, objectResult.StatusCode); - - var errorResponse = Assert.IsType(objectResult.Value); - Assert.Equal("Test exception", errorResponse.Error.Message); - Assert.Equal("server_error", errorResponse.Error.Type); - Assert.Equal("internal_error", errorResponse.Error.Code); - } - - #endregion - - #region GetModelMetadata Tests - - [Fact] - public async Task GetModelMetadata_WhenMetadataExists_ReturnsMetadata() - { - // Arrange - var modelId = "dall-e-3"; - var metadata = new - { - image = new - { - sizes = new[] { "1024x1024", "1792x1024" }, - maxImages = 1, - qualityOptions = new[] { "standard", "hd" } - } - }; - - _mockMetadataService.Setup(x => x.GetModelMetadataAsync(modelId)) - .ReturnsAsync(metadata); - - // Act - var result = await _controller.GetModelMetadata(modelId); - - // Assert - var okResult = Assert.IsType(result); - dynamic response = okResult.Value!; - - Assert.Equal(modelId, response.modelId); - Assert.NotNull(response.metadata); - } - - [Fact] - public async Task GetModelMetadata_WhenMetadataNotFound_Returns404() - { - // Arrange - var modelId = "nonexistent-model"; - _mockMetadataService.Setup(x => x.GetModelMetadataAsync(modelId)) - .ReturnsAsync((object?)null); - - // Act - var result = await _controller.GetModelMetadata(modelId); - - // Assert - var notFoundResult = Assert.IsType(result); - var errorResponse = Assert.IsType(notFoundResult.Value); - - Assert.Equal($"No metadata found for model '{modelId}'", errorResponse.Error.Message); - Assert.Equal("invalid_request_error", errorResponse.Error.Type); - Assert.Equal("model_not_found", errorResponse.Error.Code); - } - - [Fact] - public async Task GetModelMetadata_WhenExceptionThrown_Returns500() - { - // Arrange - var modelId = "dall-e-3"; - _mockMetadataService.Setup(x => x.GetModelMetadataAsync(modelId)) - .ThrowsAsync(new Exception("Test exception")); - - // Act - var result = await _controller.GetModelMetadata(modelId); - - // Assert - var objectResult = Assert.IsType(result); - Assert.Equal(500, objectResult.StatusCode); - - var errorResponse = Assert.IsType(objectResult.Value); - Assert.Equal("Test exception", errorResponse.Error.Message); - Assert.Equal("server_error", errorResponse.Error.Type); - Assert.Equal("internal_error", errorResponse.Error.Code); - } - - [Fact] - public async Task GetModelMetadata_LogsInformation() - { - // Arrange - var modelId = "dall-e-3"; - var metadata = new { test = "data" }; - - _mockMetadataService.Setup(x => x.GetModelMetadataAsync(modelId)) - .ReturnsAsync(metadata); - - // Act - await _controller.GetModelMetadata(modelId); - - // Assert - _mockLogger.Verify( - x => x.Log( - LogLevel.Information, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains($"Getting metadata for model {modelId}")), - It.IsAny(), - It.IsAny>()), - Times.Once); - } - - [Fact] - public async Task GetModelMetadata_WhenError_LogsError() - { - // Arrange - var modelId = "dall-e-3"; - var exception = new Exception("Test error"); - - _mockMetadataService.Setup(x => x.GetModelMetadataAsync(modelId)) - .ThrowsAsync(exception); - - // Act - await _controller.GetModelMetadata(modelId); - - // Assert - _mockLogger.Verify( - x => x.Log( - LogLevel.Error, - It.IsAny(), - It.Is((v, t) => v.ToString()!.Contains($"Error retrieving metadata for model {modelId}")), - It.Is(e => e == exception), - It.IsAny>()), - Times.Once); - } - - #endregion - - #region Constructor Tests - - [Fact] - public void Constructor_WithNullRouter_ThrowsArgumentNullException() - { - // Act & Assert - var ex = Assert.Throws(() => new ModelsController( - null!, - _mockLogger.Object, - _mockMetadataService.Object)); - - Assert.Equal("router", ex.ParamName); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var ex = Assert.Throws(() => new ModelsController( - _mockRouter.Object, - null!, - _mockMetadataService.Object)); - - Assert.Equal("logger", ex.ParamName); - } - - [Fact] - public void Constructor_WithNullMetadataService_ThrowsArgumentNullException() - { - // Act & Assert - var ex = Assert.Throws(() => new ModelsController( - _mockRouter.Object, - _mockLogger.Object, - null!)); - - Assert.Equal("metadataService", ex.ParamName); - } - - #endregion - - #region Attribute Tests - - [Fact] - public void Controller_HasCorrectAttributes() - { - // Arrange - var controllerType = typeof(ModelsController); - - // Assert - Controller attributes - Assert.NotNull(controllerType.GetCustomAttributes(typeof(ApiControllerAttribute), false)); - Assert.NotNull(controllerType.GetCustomAttributes(typeof(RouteAttribute), false)); - - var routeAttr = (RouteAttribute)controllerType.GetCustomAttributes(typeof(RouteAttribute), false)[0]; - Assert.Equal("v1", routeAttr.Template); - } - - [Fact] - public void GetModelMetadata_HasCorrectRoute() - { - // Arrange - var methodInfo = typeof(ModelsController).GetMethod(nameof(ModelsController.GetModelMetadata)); - - // Assert - Assert.NotNull(methodInfo); - var routeAttr = methodInfo.GetCustomAttributes(typeof(HttpGetAttribute), false)[0] as HttpGetAttribute; - Assert.NotNull(routeAttr); - Assert.Equal("models/{modelId}/metadata", routeAttr.Template); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Connect.cs b/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Connect.cs deleted file mode 100644 index ed35a4ddc..000000000 --- a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Connect.cs +++ /dev/null @@ -1,208 +0,0 @@ -using System.Net.WebSockets; -using ConduitLLM.Configuration.Entities; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Mvc; -using Moq; -using ConduitLLM.Configuration.DTOs; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class RealtimeControllerTests - { - #region Connect Tests - - [Fact] - public async Task Connect_WithoutWebSocketRequest_ShouldReturnBadRequest() - { - // Arrange - var model = "gpt-4o-realtime-preview"; - - // Setup non-websocket request - var mockWebSocketManager = new Mock(); - mockWebSocketManager.Setup(x => x.IsWebSocketRequest).Returns(false); - - var httpContext = CreateHttpContextWithWebSockets(mockWebSocketManager.Object); - _controller.ControllerContext = new ControllerContext - { - HttpContext = httpContext - }; - - // Act - var result = await _controller.Connect(model); - - // Assert - var badRequestResult = Assert.IsType(result); - var errorResponse = Assert.IsType(badRequestResult.Value); - Assert.Equal("WebSocket connection required", errorResponse.error.ToString()); - } - - [Fact] - public async Task Connect_WithoutVirtualKey_ShouldReturnUnauthorized() - { - // Arrange - var model = "gpt-4o-realtime-preview"; - - // Setup websocket context without auth headers - var mockWebSocketManager = new Mock(); - mockWebSocketManager.Setup(x => x.IsWebSocketRequest).Returns(true); - - var httpContext = CreateHttpContextWithWebSockets(mockWebSocketManager.Object); - _controller.ControllerContext = new ControllerContext - { - HttpContext = httpContext - }; - - // Act - var result = await _controller.Connect(model); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var errorResponse = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Virtual key required", errorResponse.error.ToString()); - } - - [Fact] - public async Task Connect_WithInvalidVirtualKey_ShouldReturnUnauthorized() - { - // Arrange - var model = "gpt-4o-realtime-preview"; - var virtualKey = "condt_invalid_key"; - - // Setup websocket context with auth header - var mockWebSocketManager = new Mock(); - mockWebSocketManager.Setup(x => x.IsWebSocketRequest).Returns(true); - - var httpContext = CreateHttpContextWithWebSockets(mockWebSocketManager.Object); - httpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - - _controller.ControllerContext = new ControllerContext - { - HttpContext = httpContext - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, model)) - .ReturnsAsync((VirtualKey)null); - - // Act - var result = await _controller.Connect(model); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var errorResponse = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Invalid virtual key", errorResponse.error.ToString()); - } - - [Fact] - public async Task Connect_WithoutRealtimePermissions_ShouldReturnForbidden() - { - // Arrange - var model = "gpt-4o-realtime-preview"; - var virtualKey = "condt_test_key"; - var keyEntity = new VirtualKey - { - Id = 1, - KeyHash = "hash", - AllowedModels = "gpt-4,gpt-3.5-turbo" // No realtime models - }; - - // Setup websocket context - var mockWebSocketManager = new Mock(); - mockWebSocketManager.Setup(x => x.IsWebSocketRequest).Returns(true); - - var httpContext = CreateHttpContextWithWebSockets(mockWebSocketManager.Object); - httpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - - _controller.ControllerContext = new ControllerContext - { - HttpContext = httpContext - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, model)) - .ReturnsAsync(keyEntity); - - // Act - var result = await _controller.Connect(model); - - // Assert - var forbiddenResult = Assert.IsType(result); - Assert.Equal(403, forbiddenResult.StatusCode); - var errorResponse = Assert.IsType(forbiddenResult.Value); - Assert.Equal("Virtual key does not have real-time audio permissions", errorResponse.error.ToString()); - } - - [Fact] - public async Task Connect_WithValidCredentials_ShouldEstablishConnection() - { - // Arrange - var model = "gpt-4o-realtime-preview"; - var virtualKey = "condt_valid_key"; - var keyEntity = new VirtualKey - { - Id = 1, - KeyHash = "hash", - AllowedModels = "gpt-4o-realtime-preview,gpt-4" - }; - - // Setup websocket context - var mockWebSocketManager = new Mock(); - mockWebSocketManager.Setup(x => x.IsWebSocketRequest).Returns(true); - - var mockWebSocket = new Mock(); - mockWebSocketManager.Setup(x => x.AcceptWebSocketAsync()) - .ReturnsAsync(mockWebSocket.Object); - - var httpContext = CreateHttpContextWithWebSockets(mockWebSocketManager.Object); - httpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - - _controller.ControllerContext = new ControllerContext - { - HttpContext = httpContext - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, model)) - .ReturnsAsync(keyEntity); - - _mockConnectionManager.Setup(x => x.RegisterConnectionAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) - .Returns(Task.CompletedTask); - - _mockProxyService.Setup(x => x.HandleConnectionAsync( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny())) - .Returns(Task.CompletedTask); - - // Act - var result = await _controller.Connect(model); - - // Assert - Assert.IsType(result); - } - - #endregion - - #region Helper Methods - - private HttpContext CreateHttpContextWithWebSockets(WebSocketManager webSocketManager) - { - var httpContext = new DefaultHttpContext(); - var mockFeature = new Mock(); - mockFeature.Setup(x => x.IsWebSocketRequest).Returns(webSocketManager.IsWebSocketRequest); - httpContext.Features.Set(mockFeature.Object); - - // Set up the WebSocketManager through reflection or mocking - var webSocketManagerProperty = typeof(HttpContext).GetProperty("WebSockets"); - if (webSocketManagerProperty != null && webSocketManagerProperty.CanWrite) - { - webSocketManagerProperty.SetValue(httpContext, webSocketManager); - } - - return httpContext; - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Core.cs b/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Core.cs deleted file mode 100644 index c12b88f32..000000000 --- a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.Core.cs +++ /dev/null @@ -1,75 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Http.Controllers; - -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Http.Controllers -{ - [Trait("Category", "Unit")] - [Trait("Component", "Http")] - [Trait("Phase", "2")] - public partial class RealtimeControllerTests : ControllerTestBase - { - private readonly Mock> _mockLogger; - private readonly Mock _mockProxyService; - private readonly Mock _mockVirtualKeyService; - private readonly Mock _mockConnectionManager; - private readonly RealtimeController _controller; - - public RealtimeControllerTests(ITestOutputHelper output) : base(output) - { - _mockLogger = CreateLogger(); - _mockProxyService = new Mock(); - _mockVirtualKeyService = new Mock(); - _mockConnectionManager = new Mock(); - - _controller = new RealtimeController( - _mockLogger.Object, - _mockProxyService.Object, - _mockVirtualKeyService.Object, - _mockConnectionManager.Object); - - _controller.ControllerContext = CreateControllerContext(); - } - - #region Constructor Tests - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new RealtimeController(null!, _mockProxyService.Object, _mockVirtualKeyService.Object, _mockConnectionManager.Object); - Assert.Throws(act); - } - - [Fact] - public void Constructor_WithNullProxyService_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new RealtimeController(_mockLogger.Object, null!, _mockVirtualKeyService.Object, _mockConnectionManager.Object); - Assert.Throws(act); - } - - [Fact] - public void Constructor_WithNullVirtualKeyService_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new RealtimeController(_mockLogger.Object, _mockProxyService.Object, null!, _mockConnectionManager.Object); - Assert.Throws(act); - } - - [Fact] - public void Constructor_WithNullConnectionManager_ThrowsArgumentNullException() - { - // Act & Assert - var act = () => new RealtimeController(_mockLogger.Object, _mockProxyService.Object, _mockVirtualKeyService.Object, null!); - Assert.Throws(act); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.GetConnections.cs b/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.GetConnections.cs deleted file mode 100644 index a2a46bce8..000000000 --- a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.GetConnections.cs +++ /dev/null @@ -1,96 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using Microsoft.AspNetCore.Mvc; -using Moq; -using ConduitLLM.Configuration.DTOs; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class RealtimeControllerTests - { - #region GetConnections Tests - - [Fact] - public async Task GetConnections_WithValidKey_ShouldReturnConnections() - { - // Arrange - var virtualKey = "condt_valid_key"; - var keyEntity = new VirtualKey - { - Id = 1, - KeyHash = "hash", - AllowedModels = "gpt-4o-realtime-preview" - }; - - var expectedConnections = new List - { - new ConduitLLM.Core.Models.Realtime.ConnectionInfo - { - ConnectionId = "conn-1", - Model = "gpt-4o-realtime-preview", - ConnectedAt = DateTime.UtcNow, - VirtualKey = virtualKey - }, - new ConduitLLM.Core.Models.Realtime.ConnectionInfo - { - ConnectionId = "conn-2", - Model = "gpt-4o-realtime-preview", - ConnectedAt = DateTime.UtcNow, - VirtualKey = virtualKey - } - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, null)) - .ReturnsAsync(keyEntity); - - _mockConnectionManager.Setup(x => x.GetActiveConnectionsAsync(keyEntity.Id)) - .ReturnsAsync(expectedConnections); - - // Act - _controller.ControllerContext = CreateControllerContext(); - _controller.ControllerContext.HttpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - var result = await _controller.GetConnections(); - - // Assert - var okResult = Assert.IsType(result); - var response = okResult.Value as ConduitLLM.Http.Controllers.ConnectionStatusResponse; - Assert.NotNull(response); - Assert.Equal(keyEntity.Id, response.VirtualKeyId); - Assert.Equal(expectedConnections, response.ActiveConnections); - } - - [Fact] - public async Task GetConnections_WithInvalidKey_ShouldReturnUnauthorized() - { - // Arrange - var virtualKey = "condt_invalid_key"; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, null)) - .ReturnsAsync((VirtualKey)null); - - // Act - _controller.ControllerContext = CreateControllerContext(); - _controller.ControllerContext.HttpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - var result = await _controller.GetConnections(); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var errorResponse = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Invalid virtual key", errorResponse.error.ToString()); - } - - [Fact] - public async Task GetConnections_WithMissingKey_ShouldReturnUnauthorized() - { - // Act - _controller.ControllerContext = CreateControllerContext(); - var result = await _controller.GetConnections(); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var errorResponse = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Virtual key required", errorResponse.error.ToString()); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.TerminateConnection.cs b/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.TerminateConnection.cs deleted file mode 100644 index ec39ed0f5..000000000 --- a/ConduitLLM.Tests/Http/Controllers/RealtimeControllerTests.TerminateConnection.cs +++ /dev/null @@ -1,93 +0,0 @@ -using ConduitLLM.Configuration.Entities; -using Microsoft.AspNetCore.Mvc; -using Moq; -using ConduitLLM.Configuration.DTOs; - -namespace ConduitLLM.Tests.Http.Controllers -{ - public partial class RealtimeControllerTests - { - #region TerminateConnection Tests - - [Fact] - public async Task TerminateConnection_WithValidConnection_ShouldTerminate() - { - // Arrange - var connectionId = "conn-123"; - var virtualKey = "condt_valid_key"; - var keyEntity = new VirtualKey - { - Id = 1, - KeyHash = "hash", - AllowedModels = "gpt-4o-realtime-preview" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, null)) - .ReturnsAsync(keyEntity); - - _mockConnectionManager.Setup(x => x.TerminateConnectionAsync(connectionId, keyEntity.Id)) - .ReturnsAsync(true); - - // Act - _controller.ControllerContext = CreateControllerContext(); - _controller.ControllerContext.HttpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - var result = await _controller.TerminateConnection(connectionId); - - // Assert - Assert.IsType(result); - } - - [Fact] - public async Task TerminateConnection_WithInvalidKey_ShouldReturnUnauthorized() - { - // Arrange - var connectionId = "conn-123"; - var virtualKey = "condt_invalid_key"; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, null)) - .ReturnsAsync((VirtualKey)null); - - // Act - _controller.ControllerContext = CreateControllerContext(); - _controller.ControllerContext.HttpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - var result = await _controller.TerminateConnection(connectionId); - - // Assert - var unauthorizedResult = Assert.IsType(result); - var errorResponse = Assert.IsType(unauthorizedResult.Value); - Assert.Equal("Invalid virtual key", errorResponse.error.ToString()); - } - - [Fact] - public async Task TerminateConnection_WithNonExistentConnection_ShouldReturnNotFound() - { - // Arrange - var connectionId = "conn-nonexistent"; - var virtualKey = "condt_valid_key"; - var keyEntity = new VirtualKey - { - Id = 1, - KeyHash = "hash", - AllowedModels = "gpt-4o-realtime-preview" - }; - - _mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(virtualKey, null)) - .ReturnsAsync(keyEntity); - - _mockConnectionManager.Setup(x => x.TerminateConnectionAsync(connectionId, keyEntity.Id)) - .ReturnsAsync(false); - - // Act - _controller.ControllerContext = CreateControllerContext(); - _controller.ControllerContext.HttpContext.Request.Headers["Authorization"] = $"Bearer {virtualKey}"; - var result = await _controller.TerminateConnection(connectionId); - - // Assert - var notFoundResult = Assert.IsType(result); - var errorResponse = Assert.IsType(notFoundResult.Value); - Assert.Equal("Connection not found or not owned by this key", errorResponse.error.ToString()); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Integration/ModelMappingIntegrationTests.cs b/ConduitLLM.Tests/Integration/ModelMappingIntegrationTests.cs deleted file mode 100644 index a9030c483..000000000 --- a/ConduitLLM.Tests/Integration/ModelMappingIntegrationTests.cs +++ /dev/null @@ -1,253 +0,0 @@ -using ConduitLLM.Configuration; -using ConduitLLM.Configuration.Entities; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Core.Routing; -using ConduitLLM.Core.Services; - -using Microsoft.Extensions.Logging; - -using Moq; - -namespace ConduitLLM.Tests.Integration -{ - /// - /// Integration tests to verify model mapping behavior is consistent across all services - /// - public class ModelMappingIntegrationTests - { - private readonly Mock _mockClientFactory; - private readonly Mock _mockModelMappingService; - private readonly Mock> _mockAudioRouterLogger; - private readonly Mock> _mockVideoServiceLogger; - private readonly string _testVirtualKey = "test-virtual-key"; - - public ModelMappingIntegrationTests() - { - _mockClientFactory = new Mock(); - _mockModelMappingService = new Mock(); - _mockAudioRouterLogger = new Mock>(); - _mockVideoServiceLogger = new Mock>(); - } - - [Fact] - public async Task AudioTranscription_UsesModelMapping_CorrectProviderModelId() - { - // Arrange - var modelAlias = "whisper-large"; - var providerModelId = "whisper-1"; - var providerId = 123; - - var mapping = new ModelProviderMapping - { - ModelAlias = modelAlias, - ModelId = 1, - ProviderModelId = providerModelId, - ProviderId = providerId, - Provider = new Provider { Id = providerId, ProviderType = ProviderType.OpenAI } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(modelAlias)) - .ReturnsAsync(mapping); - - var mockAudioClient = new Mock(); - var mockLLMClient = mockAudioClient.As(); - _mockClientFactory.Setup(x => x.GetClient(modelAlias)) - .Returns(mockLLMClient.Object); - - var audioRouter = new AudioRouter( - _mockClientFactory.Object, - _mockAudioRouterLogger.Object, - _mockModelMappingService.Object); - - var request = new AudioTranscriptionRequest - { - Model = modelAlias, - AudioData = new byte[] { 1, 2, 3 }, - FileName = "test.mp3" - }; - - // Act - var client = await audioRouter.GetTranscriptionClientAsync(request, _testVirtualKey); - - // Assert - Assert.NotNull(client); - Assert.Equal(providerModelId, request.Model); // Model should be updated to provider model ID - _mockModelMappingService.Verify(x => x.GetMappingByModelAliasAsync(modelAlias), Times.Once); - } - - [Fact] - public async Task TextToSpeech_UsesModelMapping_CorrectProviderModelId() - { - // Arrange - var modelAlias = "tts-hd"; - var providerModelId = "tts-1-hd"; - var providerId = 456; - - var mapping = new ModelProviderMapping - { - ModelAlias = modelAlias, - ModelId = 1, - ProviderModelId = providerModelId, - ProviderId = providerId, - Provider = new Provider { Id = providerId, ProviderType = ProviderType.OpenAI } - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(modelAlias)) - .ReturnsAsync(mapping); - - var mockTtsClient = new Mock(); - var mockLLMClient = mockTtsClient.As(); - _mockClientFactory.Setup(x => x.GetClient(modelAlias)) - .Returns(mockLLMClient.Object); - - var audioRouter = new AudioRouter( - _mockClientFactory.Object, - _mockAudioRouterLogger.Object, - _mockModelMappingService.Object); - - var request = new TextToSpeechRequest - { - Model = modelAlias, - Input = "Hello world", - Voice = "alloy" - }; - - // Act - var client = await audioRouter.GetTextToSpeechClientAsync(request, _testVirtualKey); - - // Assert - Assert.NotNull(client); - Assert.Equal(providerModelId, request.Model); // Model should be updated to provider model ID - _mockModelMappingService.Verify(x => x.GetMappingByModelAliasAsync(modelAlias), Times.Once); - } - - [Fact] - public async Task VideoGeneration_UsesModelMapping_CorrectProviderModelId() - { - // Arrange - var modelAlias = "video-gen-v2"; - var providerModelId = "minimax-video-01"; - var providerId = 789; - - var mapping = new ModelProviderMapping - { - ModelAlias = modelAlias, - ModelId = 1, - ProviderModelId = providerModelId, - ProviderId = providerId, - Provider = new Provider { Id = providerId, ProviderType = ProviderType.MiniMax }, - // SupportsVideoGeneration = true - }; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(modelAlias)) - .ReturnsAsync(mapping); - - var mockVideoClient = new Mock(); - _mockClientFactory.Setup(x => x.GetClient(modelAlias)) - .Returns(mockVideoClient.Object); - - // Mock other dependencies - var mockCapabilityService = new Mock(); - mockCapabilityService.Setup(x => x.SupportsVideoGenerationAsync(modelAlias)) - .ReturnsAsync(true); - - var mockCostService = new Mock(); - var mockVirtualKeyService = new Mock(); - mockVirtualKeyService.Setup(x => x.ValidateVirtualKeyAsync(_testVirtualKey, modelAlias)) - .ReturnsAsync(new ConduitLLM.Configuration.Entities.VirtualKey - { - Id = 1, // Changed from Guid to int - IsEnabled = true - }); - - var mockMediaStorage = new Mock(); - var mockTaskService = new Mock(); - - var videoService = new VideoGenerationService( - _mockClientFactory.Object, - mockCapabilityService.Object, - mockCostService.Object, - mockVirtualKeyService.Object, - mockMediaStorage.Object, - mockTaskService.Object, - _mockVideoServiceLogger.Object, - _mockModelMappingService.Object); - - var request = new VideoGenerationRequest - { - Model = modelAlias, - Prompt = "A beautiful sunset over the ocean" - }; - - // Act & Assert - // The service will throw because we haven't mocked the reflection-based video generation - // But we can verify that model mapping was called - await Assert.ThrowsAsync(async () => - await videoService.GenerateVideoAsync(request, _testVirtualKey)); - - // Verify model mapping was retrieved - _mockModelMappingService.Verify(x => x.GetMappingByModelAliasAsync(modelAlias), Times.Once); - } - - [Fact] - public async Task AllServices_ReturnNull_WhenModelMappingNotFound() - { - // Arrange - var unknownModel = "unknown-model"; - - _mockModelMappingService.Setup(x => x.GetMappingByModelAliasAsync(unknownModel)) - .ReturnsAsync((ModelProviderMapping?)null); - - var audioRouter = new AudioRouter( - _mockClientFactory.Object, - _mockAudioRouterLogger.Object, - _mockModelMappingService.Object); - - // Test Audio Transcription - var audioRequest = new AudioTranscriptionRequest - { - Model = unknownModel, - AudioData = new byte[] { 1, 2, 3 } - }; - var audioClient = await audioRouter.GetTranscriptionClientAsync(audioRequest, _testVirtualKey); - Assert.Null(audioClient); - - // Test TTS - var ttsRequest = new TextToSpeechRequest - { - Model = unknownModel, - Input = "Test" - }; - var ttsClient = await audioRouter.GetTextToSpeechClientAsync(ttsRequest, _testVirtualKey); - Assert.Null(ttsClient); - - // Verify all services checked for mapping - _mockModelMappingService.Verify(x => x.GetMappingByModelAliasAsync(unknownModel), Times.Exactly(2)); - } - - [Fact] - public void ModelMapping_PreservesOriginalAlias_InResponse() - { - // This test verifies that responses maintain the original model alias - // even though internally the provider model ID is used - - var modelAlias = "custom-whisper"; - var providerModelId = "whisper-1"; - - var mapping = new ModelProviderMapping - { - ModelAlias = modelAlias, - ModelId = 1, - ProviderModelId = providerModelId, - ProviderId = 1, - Provider = new Provider { Id = 1, ProviderType = ProviderType.OpenAI } - }; - - // The response should contain the original alias, not the provider model ID - // This is handled in the service/controller layer - Assert.NotEqual(modelAlias, providerModelId); - } - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Providers/OpenAIClientTests.Audio.cs b/ConduitLLM.Tests/Providers/OpenAIClientTests.Audio.cs deleted file mode 100644 index 6ea84279d..000000000 --- a/ConduitLLM.Tests/Providers/OpenAIClientTests.Audio.cs +++ /dev/null @@ -1,367 +0,0 @@ -using System.Net; -using System.Text; -using System.Text.Json; - -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Models.Audio; -using ConduitLLM.Providers.OpenAI; - -using Moq; -using Moq.Protected; - -namespace ConduitLLM.Tests.Providers -{ - public partial class OpenAIClientTests - { - #region Audio Transcription Tests - - [Fact] - public async Task TranscribeAudioAsync_WithValidRequest_ReturnsTranscription() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioData = Encoding.UTF8.GetBytes("fake audio data"), - FileName = "test.mp3", - Model = "whisper-1" - }; - - var expectedResponse = new TranscriptionResponse - { - Text = "This is the transcribed text", - Language = "en", - Duration = 10.5 - }; - - SetupHttpResponse(HttpStatusCode.OK, expectedResponse); - - // Act - var result = await client.TranscribeAudioAsync(request); - - // Assert - Assert.NotNull(result); - Assert.Equal(expectedResponse.Text, result.Text); - Assert.Equal(expectedResponse.Language, result.Language); - Assert.Equal(expectedResponse.Duration, result.Duration); - } - - [Fact] - public async Task TranscribeAudioAsync_WithLanguageAndPrompt_IncludesOptionalParameters() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioData = Encoding.UTF8.GetBytes("fake audio data"), - FileName = "test.mp3", - Model = "whisper-1", - Language = "es", - Prompt = "This is a conversation about technology", - Temperature = 0.5 - }; - - string? capturedContent = null; - _httpMessageHandlerMock.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(async (HttpRequestMessage request, CancellationToken ct) => - { - // Capture the content before returning - if (request.Content != null) - { - capturedContent = await request.Content.ReadAsStringAsync(); - } - - return new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(new TranscriptionResponse - { - Text = "Transcribed text" - })) - }; - }); - - // Act - await client.TranscribeAudioAsync(request); - - // Assert - Assert.NotNull(capturedContent); - Assert.Contains("language", capturedContent); - Assert.Contains("es", capturedContent); - Assert.Contains("prompt", capturedContent); - Assert.Contains("temperature", capturedContent); - } - - [Fact] - public async Task TranscribeAudioAsync_WithUrlInsteadOfData_ThrowsNotSupportedException() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioUrl = "https://example.com/audio.mp3", - Model = "whisper-1" - }; - - // Act & Assert - await Assert.ThrowsAsync(() => - client.TranscribeAudioAsync(request)); - } - - [Fact] - public async Task TranscribeAudioAsync_WithDifferentResponseFormats_HandlesCorrectly() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioData = Encoding.UTF8.GetBytes("fake audio data"), - FileName = "test.mp3", - Model = "whisper-1", - ResponseFormat = TranscriptionFormat.Text - }; - - SetupHttpResponse(HttpStatusCode.OK, "This is plain text response"); - - // Act - var result = await client.TranscribeAudioAsync(request); - - // Assert - Assert.NotNull(result); - Assert.Equal("This is plain text response", result.Text); - } - - [Fact] - public async Task TranscribeAudioAsync_WithApiError_ThrowsLLMCommunicationException() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioData = Encoding.UTF8.GetBytes("fake audio data"), - FileName = "test.mp3", - Model = "whisper-1" - }; - - SetupHttpResponse(HttpStatusCode.BadRequest, new { error = new { message = "Invalid audio format" } }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client.TranscribeAudioAsync(request)); - - Assert.Contains("Audio transcription failed", exception.Message); - Assert.Equal(HttpStatusCode.BadRequest, exception.StatusCode); - } - - [Fact] - public async Task TranscribeAudioAsync_WithNullRequest_ThrowsArgumentNullException() - { - // Arrange - var client = CreateOpenAIClient(); - - // Act & Assert - await Assert.ThrowsAsync(() => - client.TranscribeAudioAsync(null!)); - } - - [Fact] - public async Task TranscribeAudioAsync_ForAzure_UsesCorrectEndpoint() - { - // Arrange - var client = CreateAzureOpenAIClient(); - var request = new AudioTranscriptionRequest - { - AudioData = Encoding.UTF8.GetBytes("fake audio data"), - FileName = "test.mp3", - Model = "whisper-deployment" - }; - - string? capturedUrl = null; - _httpMessageHandlerMock.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Callback((request, ct) => - { - capturedUrl = request.RequestUri?.ToString(); - }) - .ReturnsAsync(new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new StringContent(JsonSerializer.Serialize(new TranscriptionResponse - { - Text = "Transcribed text" - })) - }); - - // Act - await client.TranscribeAudioAsync(request); - - // Assert - Assert.NotNull(capturedUrl); - Assert.Contains("/openai/deployments/", capturedUrl); - Assert.Contains("/audio/transcriptions", capturedUrl); - Assert.Contains("api-version=", capturedUrl); - } - - #endregion - - #region Text-to-Speech Tests - - [Fact] - public async Task CreateSpeechAsync_WithValidRequest_ReturnsAudioData() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new ConduitLLM.Core.Models.Audio.TextToSpeechRequest - { - Input = "Hello, this is a test", - Voice = "alloy", - Model = "tts-1" - }; - - var audioData = Encoding.UTF8.GetBytes("fake audio data"); - SetupHttpResponse(HttpStatusCode.OK, audioData, "audio/mpeg"); - - // Act - var result = await client.CreateSpeechAsync(request); - - // Assert - Assert.NotNull(result); - Assert.Equal(audioData, result.AudioData); - Assert.Equal("alloy", result.VoiceUsed); - Assert.Equal("tts-1", result.ModelUsed); - Assert.Equal(request.Input.Length, result.CharacterCount); - } - - [Fact] - public async Task CreateSpeechAsync_WithDifferentFormats_HandlesCorrectly() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new ConduitLLM.Core.Models.Audio.TextToSpeechRequest - { - Input = "Test audio", - Voice = "nova", - Model = "tts-1", - ResponseFormat = AudioFormat.Opus, - Speed = 1.5 - }; - - string? capturedContent = null; - _httpMessageHandlerMock.Protected() - .Setup>( - "SendAsync", - ItExpr.IsAny(), - ItExpr.IsAny()) - .Returns(async (HttpRequestMessage request, CancellationToken ct) => - { - // Capture the content before returning - if (request.Content != null) - { - capturedContent = await request.Content.ReadAsStringAsync(); - } - - return new HttpResponseMessage - { - StatusCode = HttpStatusCode.OK, - Content = new ByteArrayContent(new byte[] { 1, 2, 3 }) - }; - }); - - // Act - await client.CreateSpeechAsync(request); - - // Assert - Assert.NotNull(capturedContent); - var json = JsonDocument.Parse(capturedContent); - Assert.Equal("opus", json.RootElement.GetProperty("response_format").GetString()); - Assert.Equal(1.5, json.RootElement.GetProperty("speed").GetDouble()); - } - - [Fact] - public async Task CreateSpeechAsync_WithApiError_ThrowsLLMCommunicationException() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new ConduitLLM.Core.Models.Audio.TextToSpeechRequest - { - Input = "Test", - Voice = "invalid-voice", - Model = "tts-1" - }; - - SetupHttpResponse(HttpStatusCode.BadRequest, new { error = new { message = "Invalid voice" } }); - - // Act & Assert - var exception = await Assert.ThrowsAsync(() => - client.CreateSpeechAsync(request)); - - Assert.Contains("Text-to-speech failed", exception.Message); - } - - [Fact] - public async Task StreamSpeechAsync_ReturnsChunkedAudio() - { - // Arrange - var client = CreateOpenAIClient(); - var request = new ConduitLLM.Core.Models.Audio.TextToSpeechRequest - { - Input = "Test streaming", - Voice = "echo", - Model = "tts-1" - }; - - var audioData = new byte[10000]; // Large enough to require multiple chunks - Array.Fill(audioData, (byte)42); - SetupHttpResponse(HttpStatusCode.OK, audioData, "audio/mpeg"); - - // Act - var chunks = new List(); - await foreach (var chunk in client.StreamSpeechAsync(request)) - { - chunks.Add(chunk); - } - - // Assert - Assert.NotEmpty(chunks); - Assert.True(chunks.Count > 1); // Should be chunked - Assert.True(chunks.Last().IsFinal); - - // Verify data integrity - var reconstructed = chunks.SelectMany(c => c.Data).ToArray(); - Assert.Equal(audioData.Length, reconstructed.Length); - } - - #endregion - - #region Voice Listing Tests - - [Fact] - public async Task ListVoicesAsync_ReturnsOpenAIVoices() - { - // Arrange - var client = CreateOpenAIClient(); - - // Act - var voices = await client.ListVoicesAsync(); - - // Assert - Assert.NotNull(voices); - Assert.Equal(6, voices.Count); // OpenAI has 6 voices - Assert.Contains(voices, v => v.VoiceId == "alloy"); - Assert.Contains(voices, v => v.VoiceId == "echo"); - Assert.Contains(voices, v => v.VoiceId == "fable"); - Assert.Contains(voices, v => v.VoiceId == "onyx"); - Assert.Contains(voices, v => v.VoiceId == "nova"); - Assert.Contains(voices, v => v.VoiceId == "shimmer"); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Providers/OpenAIClientTests.Capabilities.cs b/ConduitLLM.Tests/Providers/OpenAIClientTests.Capabilities.cs deleted file mode 100644 index 4d2e49e9b..000000000 --- a/ConduitLLM.Tests/Providers/OpenAIClientTests.Capabilities.cs +++ /dev/null @@ -1,226 +0,0 @@ -using Moq; - -namespace ConduitLLM.Tests.Providers -{ - public partial class OpenAIClientTests - { - #region Capability Tests - - [Fact] - public async Task SupportsTranscriptionAsync_WithCapabilityService_UsesService() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.SupportsAudioTranscriptionAsync("gpt-4")) - .ReturnsAsync(true); - - // Act - var result = await client.SupportsTranscriptionAsync(); - - // Assert - Assert.True(result); - _capabilityServiceMock.Verify(x => x.SupportsAudioTranscriptionAsync("gpt-4"), Times.Once); - } - - [Fact] - public async Task SupportsTranscriptionAsync_WithCapabilityServiceError_FallsBackToDefault() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.SupportsAudioTranscriptionAsync(It.IsAny())) - .ThrowsAsync(new Exception("Service error")); - - // Act - var result = await client.SupportsTranscriptionAsync(); - - // Assert - Assert.True(result); // Falls back to true for OpenAI - } - - [Fact] - public async Task GetSupportedFormatsAsync_ReturnsWhisperFormats() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.GetSupportedFormatsAsync("gpt-4")) - .ReturnsAsync(new List { "mp3", "wav" }); - - // Act - var formats = await client.GetSupportedFormatsAsync(); - - // Assert - Assert.NotNull(formats); - Assert.Contains("mp3", formats); - Assert.Contains("wav", formats); - } - - [Fact] - public async Task GetSupportedLanguagesAsync_ReturnsWhisperLanguages() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.GetSupportedLanguagesAsync("gpt-4")) - .ThrowsAsync(new Exception("Service error")); // Force fallback to default - - // Act - var languages = await client.GetSupportedLanguagesAsync(); - - // Assert - Assert.NotNull(languages); - Assert.Contains("en", languages); - Assert.Contains("es", languages); - Assert.Contains("fr", languages); - Assert.Contains("de", languages); - Assert.Contains("zh", languages); - Assert.Contains("ja", languages); - // And many more... - } - - [Fact] - public async Task SupportsTextToSpeechAsync_WithCapabilityService_UsesService() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.SupportsTextToSpeechAsync("gpt-4")) - .ReturnsAsync(true); - - // Act - var result = await client.SupportsTextToSpeechAsync(); - - // Assert - Assert.True(result); - _capabilityServiceMock.Verify(x => x.SupportsTextToSpeechAsync("gpt-4"), Times.Once); - } - - #endregion - - #region Realtime Audio Tests - - [Fact] - public async Task SupportsRealtimeAsync_WithSupportedModel_ReturnsTrue() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.SupportsRealtimeAudioAsync("gpt-4o-realtime-preview")) - .ReturnsAsync(true); - - // Act - var result = await client.SupportsRealtimeAsync("gpt-4o-realtime-preview"); - - // Assert - Assert.True(result); - } - - [Fact] - public async Task SupportsRealtimeAsync_WithUnsupportedModel_ReturnsFalse() - { - // Arrange - var client = CreateOpenAIClient(); - _capabilityServiceMock.Setup(x => x.SupportsRealtimeAudioAsync("gpt-4")) - .ReturnsAsync(false); - - // Act - var result = await client.SupportsRealtimeAsync("gpt-4"); - - // Assert - Assert.False(result); - } - - [Fact] - public async Task GetRealtimeCapabilitiesAsync_ReturnsExpectedCapabilities() - { - // Arrange - var client = CreateOpenAIClient(); - - // Act - var capabilities = await client.GetRealtimeCapabilitiesAsync(); - - // Assert - Assert.NotNull(capabilities); - Assert.NotEmpty(capabilities.SupportedInputFormats); - Assert.NotEmpty(capabilities.SupportedOutputFormats); - Assert.NotEmpty(capabilities.AvailableVoices); - Assert.NotEmpty(capabilities.SupportedLanguages); - Assert.True(capabilities.SupportsFunctionCalling); - Assert.True(capabilities.SupportsInterruptions); - } - - #endregion - - #region Provider Capabilities Tests - - [Fact] - public async Task GetCapabilitiesAsync_ForChatModel_ReturnsCorrectCapabilities() - { - // Arrange - var client = CreateOpenAIClient("gpt-4"); - - // Act - var capabilities = await client.GetCapabilitiesAsync(); - - // Assert - Assert.NotNull(capabilities); - Assert.Equal("OpenAI", capabilities.Provider); - Assert.Equal("gpt-4", capabilities.ModelId); - - // Chat parameters - Assert.True(capabilities.ChatParameters.Temperature); - Assert.True(capabilities.ChatParameters.MaxTokens); - Assert.True(capabilities.ChatParameters.TopP); - Assert.False(capabilities.ChatParameters.TopK); // OpenAI doesn't support top-k - Assert.True(capabilities.ChatParameters.Stop); - Assert.True(capabilities.ChatParameters.Tools); - - // Features - Assert.True(capabilities.Features.Streaming); - Assert.False(capabilities.Features.Embeddings); - Assert.False(capabilities.Features.ImageGeneration); - Assert.True(capabilities.Features.FunctionCalling); - } - - [Fact] - public async Task GetCapabilitiesAsync_ForVisionModel_EnablesVisionInput() - { - // Arrange - var client = CreateOpenAIClient("gpt-4o"); - - // Act - var capabilities = await client.GetCapabilitiesAsync(); - - // Assert - Assert.True(capabilities.Features.VisionInput); - } - - [Fact] - public async Task GetCapabilitiesAsync_ForDalleModel_EnablesImageGeneration() - { - // Arrange - var client = CreateOpenAIClient("dall-e-3"); - - // Act - var capabilities = await client.GetCapabilitiesAsync(); - - // Assert - Assert.True(capabilities.Features.ImageGeneration); - Assert.False(capabilities.Features.Streaming); - Assert.False(capabilities.ChatParameters.Tools); - } - - [Fact] - public async Task GetCapabilitiesAsync_ForEmbeddingModel_EnablesEmbeddings() - { - // Arrange - var client = CreateOpenAIClient("text-embedding-ada-002"); - - // Act - var capabilities = await client.GetCapabilitiesAsync(); - - // Assert - Assert.True(capabilities.Features.Embeddings); - Assert.False(capabilities.Features.Streaming); - Assert.False(capabilities.ChatParameters.Tools); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.ChatCompletion.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.ChatCompletion.cs deleted file mode 100644 index fc442ffa5..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.ChatCompletion.cs +++ /dev/null @@ -1,209 +0,0 @@ -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Moq; - -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region CreateChatCompletionAsync Tests - - [Fact] - public async Task CreateChatCompletionAsync_WithNullRequest_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _router.CreateChatCompletionAsync(null!)); - } - - [Fact] - public async Task CreateChatCompletionAsync_PassthroughMode_DirectlyCallsClient() - { - // Arrange - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - var expectedResponse = new ChatCompletionResponse - { - Id = "test-response", - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Model = "gpt-4", - Object = "chat.completion", - Choices = new List { new() { Index = 0, Message = new Message { Role = "assistant", Content = "Hello!" }, FinishReason = "stop" } } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.CreateChatCompletionAsync(request, null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient("gpt-4")) - .Returns(mockClient.Object); - - // Act - var response = await _router.CreateChatCompletionAsync(request, "passthrough"); - - // Assert - Assert.Equal(expectedResponse.Id, response.Id); - mockClient.Verify(c => c.CreateChatCompletionAsync(request, null, It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateChatCompletionAsync_WithRoutingStrategy_SelectsAppropriateModel() - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - var expectedResponse = new ChatCompletionResponse - { - Id = "test-response", - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Model = "gpt-4", - Object = "chat.completion", - Choices = new List { new() { Index = 0, Message = new Message { Role = "assistant", Content = "Hello!" }, FinishReason = "stop" } } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient(It.IsAny())) - .Returns(mockClient.Object); - - // Act - var response = await _router.CreateChatCompletionAsync(request, "simple"); - - // Assert - Assert.Equal(expectedResponse.Id, response.Id); - mockClient.Verify(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateChatCompletionAsync_WithFailedRequest_RetriesAndUsesFallback() - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - var expectedResponse = new ChatCompletionResponse - { - Id = "fallback-response", - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Model = "claude-3", - Object = "chat.completion", - Choices = new List { new() { Index = 0, Message = new Message { Role = "assistant", Content = "Fallback!" }, FinishReason = "stop" } } - }; - - var failingClient = new Mock(); - failingClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ThrowsAsync(new LLMCommunicationException("Connection failed")); - - var successClient = new Mock(); - successClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient("openai/gpt-4")) - .Returns(failingClient.Object); - _clientFactoryMock.Setup(f => f.GetClient("anthropic/claude-3")) - .Returns(successClient.Object); - - // Act - var response = await _router.CreateChatCompletionAsync(request, "simple"); - - // Assert - Assert.Equal(expectedResponse.Id, response.Id); - failingClient.Verify(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny()), Times.Once); - successClient.Verify(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateChatCompletionAsync_AllModelsUnavailable_ThrowsLLMCommunicationException() - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - - var failingClient = new Mock(); - failingClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ThrowsAsync(new LLMCommunicationException("Connection failed")); - - _clientFactoryMock.Setup(f => f.GetClient(It.IsAny())) - .Returns(failingClient.Object); - - // Act & Assert - await Assert.ThrowsAsync(() => - _router.CreateChatCompletionAsync(request, "simple")); - } - - [Fact] - public async Task CreateChatCompletionAsync_WithVisionRequest_SelectsVisionCapableModel() - { - // Arrange - InitializeRouterWithVisionModels(); - - // Don't request a specific model, let the router choose based on vision capability - var request = new ChatCompletionRequest - { - Model = "", // Empty model to trigger routing logic - Messages = new List - { - new() - { - Role = "user", - Content = new List - { - new { type = "text", text = "What's in this image?" }, - new { type = "image_url", image_url = new { url = "data:image/png;base64,..." } } - } - } - } - }; - var expectedResponse = new ChatCompletionResponse - { - Id = "vision-response", - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Model = "gpt-4-vision", - Object = "chat.completion", - Choices = new List { new() { Index = 0, Message = new Message { Role = "assistant", Content = "I see..." }, FinishReason = "stop" } } - }; - - _capabilityDetectorMock.Setup(d => d.ContainsImageContent(It.IsAny())) - .Returns(true); - // Set up vision capability checks for both deployment names - _capabilityDetectorMock.Setup(d => d.HasVisionCapability("openai/gpt-4-vision")) - .Returns(true); - _capabilityDetectorMock.Setup(d => d.HasVisionCapability("openai/gpt-4")) - .Returns(false); - - var visionClient = new Mock(); - visionClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient("openai/gpt-4-vision")) - .Returns(visionClient.Object); - - // Act - var response = await _router.CreateChatCompletionAsync(request, "simple"); - - // Assert - Assert.Equal(expectedResponse.Id, response.Id); - visionClient.Verify(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Embedding.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Embedding.cs deleted file mode 100644 index e17907010..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Embedding.cs +++ /dev/null @@ -1,89 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Moq; - -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region CreateEmbeddingAsync Tests - - [Fact] - public async Task CreateEmbeddingAsync_WithNullRequest_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync(() => - _router.CreateEmbeddingAsync(null!)); - } - - [Fact] - public async Task CreateEmbeddingAsync_WithEmbeddingCapableModel_Succeeds() - { - // Arrange - InitializeRouterWithEmbeddingModels(); - var request = new EmbeddingRequest - { - Input = "Test embedding", - Model = "text-embedding-ada-002", - EncodingFormat = "float" - }; - var expectedResponse = new EmbeddingResponse - { - Data = new List { new() { Index = 0, Embedding = new List { 0.1f, 0.2f, 0.3f }, Object = "embedding" } }, - Model = "text-embedding-ada-002", - Object = "list", - Usage = new Usage { PromptTokens = 5, CompletionTokens = 0, TotalTokens = 5 } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.CreateEmbeddingAsync(It.IsAny(), null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient("openai/text-embedding-ada-002")) - .Returns(mockClient.Object); - - // Act - var response = await _router.CreateEmbeddingAsync(request, "simple"); - - // Assert - Assert.Equal(expectedResponse.Data.Count, response.Data.Count); - mockClient.Verify(c => c.CreateEmbeddingAsync(It.IsAny(), null, It.IsAny()), Times.Once); - } - - [Fact] - public async Task CreateEmbeddingAsync_WithCachedResponse_ReturnsCachedResult() - { - // Arrange - InitializeRouterWithEmbeddingModels(); - var request = new EmbeddingRequest - { - Input = "Test embedding", - Model = "text-embedding-ada-002", - EncodingFormat = "float" - }; - var cachedResponse = new EmbeddingResponse - { - Data = new List { new() { Index = 0, Embedding = new List { 0.1f, 0.2f, 0.3f }, Object = "embedding" } }, - Model = "text-embedding-ada-002", - Object = "list", - Usage = new Usage { PromptTokens = 5, CompletionTokens = 0, TotalTokens = 5 } - }; - - _embeddingCacheMock.Setup(c => c.IsAvailable).Returns(true); - _embeddingCacheMock.Setup(c => c.GenerateCacheKey(It.IsAny())) - .Returns("cache-key"); - _embeddingCacheMock.Setup(c => c.GetEmbeddingAsync("cache-key")) - .ReturnsAsync(cachedResponse); - - // Act - var response = await _router.CreateEmbeddingAsync(request, "simple"); - - // Assert - Assert.Equal(cachedResponse.Data.Count, response.Data.Count); - _embeddingCacheMock.Verify(c => c.GetEmbeddingAsync("cache-key"), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Initialization.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Initialization.cs deleted file mode 100644 index 4d44666fa..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Initialization.cs +++ /dev/null @@ -1,85 +0,0 @@ -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Routing; - -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region Initialization Tests - - [Fact] - public void Constructor_WithNullClientFactory_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new DefaultLLMRouter(null!, _loggerMock.Object)); - } - - [Fact] - public void Constructor_WithNullLogger_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => - new DefaultLLMRouter(_clientFactoryMock.Object, null!)); - } - - [Fact] - public void Initialize_WithNullConfig_ThrowsArgumentNullException() - { - // Act & Assert - Assert.Throws(() => _router.Initialize(null!)); - } - - [Fact] - public void Initialize_WithValidConfig_SetsUpModelDeployments() - { - // Arrange - var config = new RouterConfig - { - DefaultRoutingStrategy = "roundrobin", - MaxRetries = 5, - RetryBaseDelayMs = 1000, - RetryMaxDelayMs = 20000, - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "gpt-4", - ModelAlias = "openai/gpt-4", - IsHealthy = true, - Priority = 1, - InputTokenCostPer1K = 0.03m, - OutputTokenCostPer1K = 0.06m - }, - new ModelDeployment - { - DeploymentName = "claude-3", - ModelAlias = "anthropic/claude-3", - IsHealthy = false, - Priority = 2 - } - }, - Fallbacks = new Dictionary> - { - ["gpt-4"] = new List { "claude-3", "gpt-3.5-turbo" } - } - }; - - // Act - _router.Initialize(config); - - // Assert - var availableModels = _router.GetAvailableModels(); - Assert.Equal(2, availableModels.Count); - Assert.Contains("gpt-4", availableModels); - Assert.Contains("claude-3", availableModels); - - var fallbacks = _router.GetFallbackModels("gpt-4"); - Assert.Equal(2, fallbacks.Count); - Assert.Equal("claude-3", fallbacks[0]); - Assert.Equal("gpt-3.5-turbo", fallbacks[1]); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.OtherTests.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.OtherTests.cs deleted file mode 100644 index ae7e3668d..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.OtherTests.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region Health Management Tests - - // UpdateModelHealth tests removed - provider health monitoring has been removed - - #endregion - - #region GetAvailableModelDetailsAsync Tests - - [Fact] - public async Task GetAvailableModelDetailsAsync_ReturnsModelInfo() - { - // Arrange - InitializeRouterWithModels(); - - // Act - var models = await _router.GetAvailableModelDetailsAsync(); - - // Assert - Assert.Equal(2, models.Count); - var gpt4 = models.FirstOrDefault(m => m.Id == "gpt-4"); - Assert.NotNull(gpt4); - Assert.Equal("openai/gpt-4", gpt4.OwnedBy); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.RoutingStrategies.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.RoutingStrategies.cs deleted file mode 100644 index 07b4a2ef6..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.RoutingStrategies.cs +++ /dev/null @@ -1,53 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Moq; - -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region Routing Strategy Tests - - [Theory] - [InlineData("simple")] - [InlineData("roundrobin")] - [InlineData("leastcost")] - [InlineData("leastlatency")] - [InlineData("highestpriority")] - public async Task CreateChatCompletionAsync_WithDifferentStrategies_SelectsModels(string strategy) - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - var expectedResponse = new ChatCompletionResponse - { - Id = "test-response", - Created = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), - Model = "gpt-4", - Object = "chat.completion", - Choices = new List { new() { Index = 0, Message = new Message { Role = "assistant", Content = "Hello!" }, FinishReason = "stop" } } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .ReturnsAsync(expectedResponse); - - _clientFactoryMock.Setup(f => f.GetClient(It.IsAny())) - .Returns(mockClient.Object); - - // Act - var response = await _router.CreateChatCompletionAsync(request, strategy); - - // Assert - Assert.Equal(expectedResponse.Id, response.Id); - mockClient.Verify(c => c.CreateChatCompletionAsync(It.IsAny(), null, It.IsAny()), Times.Once); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Streaming.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Streaming.cs deleted file mode 100644 index cfd410fee..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.Streaming.cs +++ /dev/null @@ -1,93 +0,0 @@ -using ConduitLLM.Core.Exceptions; -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models; - -using Moq; - -namespace ConduitLLM.Tests.Routing -{ - public partial class DefaultLLMRouterTests - { - #region StreamChatCompletionAsync Tests - - [Fact] - public async Task StreamChatCompletionAsync_WithNullRequest_ThrowsArgumentNullException() - { - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var _ in _router.StreamChatCompletionAsync(null!)) - { - // Should not reach here - } - }); - } - - [Fact] - public async Task StreamChatCompletionAsync_SuccessfulStream_ReturnsChunks() - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - - var chunks = new List - { - new() { Id = "chunk1", Choices = new List { new() { Index = 0, Delta = new DeltaContent { Role = "assistant", Content = "Hello" }, FinishReason = null } } }, - new() { Id = "chunk2", Choices = new List { new() { Index = 0, Delta = new DeltaContent { Content = " world" }, FinishReason = "stop" } } } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.StreamChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .Returns(chunks.ToAsyncEnumerable()); - - _clientFactoryMock.Setup(f => f.GetClient(It.IsAny())) - .Returns(mockClient.Object); - - // Act - var receivedChunks = new List(); - await foreach (var chunk in _router.StreamChatCompletionAsync(request, "simple")) - { - receivedChunks.Add(chunk); - } - - // Assert - Assert.Equal(2, receivedChunks.Count); - Assert.Equal("chunk1", receivedChunks[0].Id); - Assert.Equal("chunk2", receivedChunks[1].Id); - } - - [Fact] - public async Task StreamChatCompletionAsync_NoChunksReceived_ThrowsLLMCommunicationException() - { - // Arrange - InitializeRouterWithModels(); - var request = new ChatCompletionRequest - { - Model = "gpt-4", - Messages = new List { new() { Role = "user", Content = "Hello" } } - }; - - var mockClient = new Mock(); - mockClient.Setup(c => c.StreamChatCompletionAsync(It.IsAny(), null, It.IsAny())) - .Returns(new List().ToAsyncEnumerable()); - - _clientFactoryMock.Setup(f => f.GetClient(It.IsAny())) - .Returns(mockClient.Object); - - // Act & Assert - await Assert.ThrowsAsync(async () => - { - await foreach (var _ in _router.StreamChatCompletionAsync(request, "simple")) - { - // Processing chunks - } - }); - } - - #endregion - } -} \ No newline at end of file diff --git a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.cs b/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.cs deleted file mode 100644 index 9150b1048..000000000 --- a/ConduitLLM.Tests/Routing/DefaultLLMRouterTests.cs +++ /dev/null @@ -1,161 +0,0 @@ -using ConduitLLM.Core.Interfaces; -using ConduitLLM.Core.Models.Routing; -using ConduitLLM.Core.Routing; - -using Microsoft.Extensions.Logging; - -using Moq; - -using Xunit.Abstractions; - -namespace ConduitLLM.Tests.Routing -{ - /// - /// Unit tests for the DefaultLLMRouter class. - /// - public partial class DefaultLLMRouterTests : TestBase - { - private readonly Mock _clientFactoryMock; - private readonly Mock> _loggerMock; - private readonly Mock _capabilityDetectorMock; - private readonly Mock _embeddingCacheMock; - private readonly DefaultLLMRouter _router; - - public DefaultLLMRouterTests(ITestOutputHelper output) : base(output) - { - _clientFactoryMock = new Mock(); - _loggerMock = CreateLogger(); - _capabilityDetectorMock = new Mock(); - _embeddingCacheMock = new Mock(); - - _router = new DefaultLLMRouter( - _clientFactoryMock.Object, - _loggerMock.Object, - _capabilityDetectorMock.Object, - _embeddingCacheMock.Object); - } - - - - - - #region Helper Methods - - private void InitializeRouterWithModels() - { - var config = new RouterConfig - { - DefaultRoutingStrategy = "simple", - MaxRetries = 3, - RetryBaseDelayMs = 100, - RetryMaxDelayMs = 1000, - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "gpt-4", - ModelAlias = "openai/gpt-4", - IsHealthy = true, - Priority = 1, - InputTokenCostPer1K = 0.03m, - OutputTokenCostPer1K = 0.06m - }, - new ModelDeployment - { - DeploymentName = "claude-3", - ModelAlias = "anthropic/claude-3", - IsHealthy = true, - Priority = 2, - InputTokenCostPer1K = 0.025m, - OutputTokenCostPer1K = 0.05m - } - }, - Fallbacks = new Dictionary> - { - ["gpt-4"] = new List { "claude-3" } - } - }; - - _router.Initialize(config); - } - - private void InitializeRouterWithVisionModels() - { - var config = new RouterConfig - { - DefaultRoutingStrategy = "simple", - MaxRetries = 3, - RetryBaseDelayMs = 100, - RetryMaxDelayMs = 1000, - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "gpt-4", - ModelAlias = "openai/gpt-4", - IsHealthy = true, - Priority = 2 - }, - new ModelDeployment - { - DeploymentName = "gpt-4-vision", - ModelAlias = "openai/gpt-4-vision", - IsHealthy = true, - Priority = 1 - } - } - }; - - _router.Initialize(config); - } - - private void InitializeRouterWithEmbeddingModels() - { - var config = new RouterConfig - { - DefaultRoutingStrategy = "simple", - MaxRetries = 3, - RetryBaseDelayMs = 100, - RetryMaxDelayMs = 1000, - ModelDeployments = new List - { - new ModelDeployment - { - DeploymentName = "text-embedding-ada-002", - ModelAlias = "openai/text-embedding-ada-002", - IsHealthy = true, - Priority = 1, - SupportsEmbeddings = true - }, - new ModelDeployment - { - DeploymentName = "gpt-4", - ModelAlias = "openai/gpt-4", - IsHealthy = true, - Priority = 2, - SupportsEmbeddings = false - } - } - }; - - _router.Initialize(config); - } - - #endregion - } - - /// - /// Extension to convert IEnumerable to IAsyncEnumerable for testing - /// - internal static class AsyncEnumerableExtensions - { - public static async IAsyncEnumerable ToAsyncEnumerable(this IEnumerable source) - { - foreach (var item in source) - { - yield return item; - await Task.Yield(); - } - } - } -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/model-costs/add/PricingConfigSections.tsx b/ConduitLLM.WebUI/src/app/model-costs/add/PricingConfigSections.tsx index 98dd2620f..15657bed6 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/add/PricingConfigSections.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/add/PricingConfigSections.tsx @@ -145,59 +145,6 @@ export function PricingConfigSections({ form, modelType }: PricingConfigSections )} - {modelType === ModelType.Audio && ( - - }> - Audio Pricing - - - - - - - - - - - - - - - )} {modelType === ModelType.Video && ( diff --git a/ConduitLLM.WebUI/src/app/model-costs/add/page.tsx b/ConduitLLM.WebUI/src/app/model-costs/add/page.tsx index 3c7551415..52ea0a983 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/add/page.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/add/page.tsx @@ -69,10 +69,6 @@ export default function AddModelCostPage() { costPerInferenceStep: values.inferenceStepCost > 0 ? values.inferenceStepCost : undefined, defaultInferenceSteps: values.defaultInferenceSteps > 0 ? values.defaultInferenceSteps : undefined, imageCostPerImage: values.imageCostPerImage > 0 ? values.imageCostPerImage : undefined, - audioCostPerMinute: values.audioCostPerMinute > 0 ? values.audioCostPerMinute : undefined, - audioCostPerKCharacters: values.audioCostPerKCharacters > 0 ? values.audioCostPerKCharacters : undefined, - audioInputCostPerMinute: values.audioInputCostPerMinute > 0 ? values.audioInputCostPerMinute : undefined, - audioOutputCostPerMinute: values.audioOutputCostPerMinute > 0 ? values.audioOutputCostPerMinute : undefined, videoCostPerSecond: values.videoCostPerSecond > 0 ? values.videoCostPerSecond : undefined, videoResolutionMultipliers: values.videoResolutionMultipliers || undefined, supportsBatchProcessing: values.supportsBatchProcessing, diff --git a/ConduitLLM.WebUI/src/app/model-costs/add/types.ts b/ConduitLLM.WebUI/src/app/model-costs/add/types.ts index 842d75a8f..6efb4cd3c 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/add/types.ts +++ b/ConduitLLM.WebUI/src/app/model-costs/add/types.ts @@ -15,10 +15,6 @@ export interface FormValues { inferenceStepCost: number; defaultInferenceSteps: number; imageCostPerImage: number; - audioCostPerMinute: number; - audioCostPerKCharacters: number; - audioInputCostPerMinute: number; - audioOutputCostPerMinute: number; videoCostPerSecond: number; videoResolutionMultipliers: string; // Batch processing diff --git a/ConduitLLM.WebUI/src/app/model-costs/add/validation.ts b/ConduitLLM.WebUI/src/app/model-costs/add/validation.ts index 8243f90f9..66cc66bdf 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/add/validation.ts +++ b/ConduitLLM.WebUI/src/app/model-costs/add/validation.ts @@ -14,10 +14,6 @@ export const getInitialValues = (): FormValues => ({ inferenceStepCost: 0, defaultInferenceSteps: 0, imageCostPerImage: 0, - audioCostPerMinute: 0, - audioCostPerKCharacters: 0, - audioInputCostPerMinute: 0, - audioOutputCostPerMinute: 0, videoCostPerSecond: 0, videoResolutionMultipliers: '', supportsBatchProcessing: false, @@ -41,7 +37,6 @@ export const getFormValidation = () => ({ inferenceStepCost: (value: number) => value < 0 ? 'Cost must be non-negative' : null, defaultInferenceSteps: (value: number) => value < 0 ? 'Steps must be non-negative' : null, imageCostPerImage: (value: number) => value < 0 ? 'Cost must be non-negative' : null, - audioCostPerMinute: (value: number) => value < 0 ? 'Cost must be non-negative' : null, videoCostPerSecond: (value: number) => value < 0 ? 'Cost must be non-negative' : null, batchProcessingMultiplier: (value: number, values: FormValues) => { if (values.supportsBatchProcessing && value) { diff --git a/ConduitLLM.WebUI/src/app/model-costs/components/EditModelCostModal.tsx b/ConduitLLM.WebUI/src/app/model-costs/components/EditModelCostModal.tsx index f4d275b7d..4081e160b 100755 --- a/ConduitLLM.WebUI/src/app/model-costs/components/EditModelCostModal.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/components/EditModelCostModal.tsx @@ -70,10 +70,6 @@ export function EditModelCostModal({ isOpen, modelCost, onClose, onSuccess }: Ed inferenceStepCost: (modelCost.costPerInferenceStep as number) ?? 0, defaultInferenceSteps: (modelCost.defaultInferenceSteps as number) ?? 0, imageCostPerImage: (modelCost.imageCostPerImage as number) ?? 0, - audioCostPerMinute: (modelCost.audioCostPerMinute as number) ?? 0, - audioCostPerKCharacters: (modelCost.audioCostPerKCharacters as number) ?? 0, - audioInputCostPerMinute: (modelCost.audioInputCostPerMinute as number) ?? 0, - audioOutputCostPerMinute: (modelCost.audioOutputCostPerMinute as number) ?? 0, videoCostPerSecond: (modelCost.videoCostPerSecond as number) ?? 0, videoResolutionMultipliers: (modelCost.videoResolutionMultipliers as string) ?? '', supportsBatchProcessing: (modelCost.supportsBatchProcessing) ?? false, @@ -140,18 +136,6 @@ export function EditModelCostModal({ isOpen, modelCost, onClose, onSuccess }: Ed updates.imageCostPerImage = values.imageCostPerImage || undefined; } - if (values.audioCostPerMinute > 0) { - updates.audioCostPerMinute = values.audioCostPerMinute; - } - if (values.audioCostPerKCharacters > 0) { - updates.audioCostPerKCharacters = values.audioCostPerKCharacters; - } - if (values.audioInputCostPerMinute > 0) { - updates.audioInputCostPerMinute = values.audioInputCostPerMinute; - } - if (values.audioOutputCostPerMinute > 0) { - updates.audioOutputCostPerMinute = values.audioOutputCostPerMinute; - } if (values.videoCostPerSecond !== modelCost.videoCostPerSecond) { updates.videoCostPerSecond = values.videoCostPerSecond || undefined; @@ -258,16 +242,6 @@ export function EditModelCostModal({ isOpen, modelCost, onClose, onSuccess }: Ed )} - {modelType === ModelType.Audio && ( - - }> - Audio Pricing - - - - - - )} {modelType === ModelType.Video && ( diff --git a/ConduitLLM.WebUI/src/app/model-costs/components/ImportModelCostsModal.tsx b/ConduitLLM.WebUI/src/app/model-costs/components/ImportModelCostsModal.tsx index ae133a6b8..0365faba8 100755 --- a/ConduitLLM.WebUI/src/app/model-costs/components/ImportModelCostsModal.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/components/ImportModelCostsModal.tsx @@ -87,10 +87,6 @@ export function ImportModelCostsModal({ isOpen, onClose, onSuccess }: ImportMode cachedInputWriteCostPerMillionTokens: cost.cachedInputWriteCostPerMillion, embeddingCostPerMillionTokens: cost.embeddingCostPerMillion, imageCostPerImage: cost.imageCostPerImage, - audioCostPerMinute: cost.audioCostPerMinute, - audioCostPerKCharacters: cost.audioCostPerKCharacters, - audioInputCostPerMinute: cost.audioInputCostPerMinute, - audioOutputCostPerMinute: cost.audioOutputCostPerMinute, videoCostPerSecond: cost.videoCostPerSecond, videoResolutionMultipliers: cost.videoResolutionMultipliers, supportsBatchProcessing: cost.supportsBatchProcessing, diff --git a/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormSections.tsx b/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormSections.tsx index 58925056c..722117496 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormSections.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormSections.tsx @@ -122,53 +122,6 @@ export function ModelCostFormSections({ form, modelType }: ModelCostFormSections )} - {/* Audio Pricing */} - {modelType === ModelType.Audio && ( - - - - - - - - - - - )} {/* Video Generation Pricing */} {modelType === ModelType.Video && ( diff --git a/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormTypes.ts b/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormTypes.ts index c72286558..abaca8849 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormTypes.ts +++ b/ConduitLLM.WebUI/src/app/model-costs/components/ModelCostFormTypes.ts @@ -15,10 +15,6 @@ export interface FormValues { inferenceStepCost: number; defaultInferenceSteps: number; imageCostPerImage: number; - audioCostPerMinute: number; - audioCostPerKCharacters: number; - audioInputCostPerMinute: number; - audioOutputCostPerMinute: number; videoCostPerSecond: number; videoResolutionMultipliers: string; // Batch processing @@ -45,7 +41,6 @@ export const getFormValidation = () => ({ inferenceStepCost: (value: number) => value < 0 ? 'Cost must be non-negative' : null, defaultInferenceSteps: (value: number) => value < 0 ? 'Steps must be non-negative' : null, imageCostPerImage: (value: number) => value < 0 ? 'Cost must be non-negative' : null, - audioCostPerMinute: (value: number) => value < 0 ? 'Cost must be non-negative' : null, videoCostPerSecond: (value: number) => value < 0 ? 'Cost must be non-negative' : null, batchProcessingMultiplier: (value: number, values: FormValues) => { if (values.supportsBatchProcessing && value) { diff --git a/ConduitLLM.WebUI/src/app/model-costs/components/PricingModelSelector.tsx b/ConduitLLM.WebUI/src/app/model-costs/components/PricingModelSelector.tsx index 83698e9db..ade6962d1 100644 --- a/ConduitLLM.WebUI/src/app/model-costs/components/PricingModelSelector.tsx +++ b/ConduitLLM.WebUI/src/app/model-costs/components/PricingModelSelector.tsx @@ -18,8 +18,6 @@ const PRICING_MODEL_OPTIONS = [ { value: String(PricingModel.InferenceSteps), label: 'Inference Steps' }, { value: String(PricingModel.TieredTokens), label: 'Tiered Tokens' }, { value: String(PricingModel.PerImage), label: 'Per Image' }, - { value: String(PricingModel.PerMinuteAudio), label: 'Per Minute Audio' }, - { value: String(PricingModel.PerThousandCharacters), label: 'Per Thousand Characters' }, ]; const getDefaultConfiguration = (model: PricingModel): string => { @@ -93,10 +91,6 @@ const getPricingModelDescription = (model: PricingModel): string => { return 'Different token rates based on context length tiers (e.g., MiniMax M1).'; case PricingModel.PerImage: return 'Per-image pricing with quality and resolution multipliers.'; - case PricingModel.PerMinuteAudio: - return 'Audio pricing based on minutes of audio processed.'; - case PricingModel.PerThousandCharacters: - return 'Text-to-speech pricing based on character count.'; default: return ''; } diff --git a/ConduitLLM.WebUI/src/app/model-costs/utils/costFormatters.ts b/ConduitLLM.WebUI/src/app/model-costs/utils/costFormatters.ts index 9d5b5ecde..ed988a21d 100755 --- a/ConduitLLM.WebUI/src/app/model-costs/utils/costFormatters.ts +++ b/ConduitLLM.WebUI/src/app/model-costs/utils/costFormatters.ts @@ -39,8 +39,6 @@ export const formatModelType = (type: ModelType): string => { return 'Embedding'; case ModelType.Image: return 'Image'; - case ModelType.Audio: - return 'Audio'; case ModelType.Video: return 'Video'; default: @@ -80,8 +78,6 @@ export const getCostDisplayForModelType = (cost: ModelCost): string => { return '-'; case ModelType.Image: return formatCostPerImage(cost.imageCostPerImage); - case ModelType.Audio: - return formatCostPerSecond(cost.audioCostPerMinute ? cost.audioCostPerMinute / 60 : undefined); case ModelType.Video: return formatCostPerSecond(cost.videoCostPerSecond); default: @@ -97,8 +93,6 @@ export const getCostTypeLabel = (modelType: ModelType): string => { return 'Per million tokens'; case ModelType.Image: return 'Per image'; - case ModelType.Audio: - return 'Per second'; case ModelType.Video: return 'Per second'; default: diff --git a/ConduitLLM.WebUI/src/app/model-costs/utils/csvHelpers.ts b/ConduitLLM.WebUI/src/app/model-costs/utils/csvHelpers.ts index d4d4b0262..52c2842d6 100755 --- a/ConduitLLM.WebUI/src/app/model-costs/utils/csvHelpers.ts +++ b/ConduitLLM.WebUI/src/app/model-costs/utils/csvHelpers.ts @@ -10,10 +10,6 @@ export interface ParsedModelCost { cachedInputWriteCostPerMillion?: number; embeddingCostPerMillion?: number; imageCostPerImage?: number; - audioCostPerMinute?: number; - audioCostPerKCharacters?: number; - audioInputCostPerMinute?: number; - audioOutputCostPerMinute?: number; videoCostPerSecond?: number; videoResolutionMultipliers?: string; batchProcessingMultiplier?: number; @@ -127,10 +123,6 @@ export const parseCSVContent = (text: string): ParsedModelCost[] => { cachedInputWriteCostPerMillion: parseNumericValue(row['cache write cost (per million tokens)']), embeddingCostPerMillion: parseNumericValue(row['embedding cost (per million tokens)']), imageCostPerImage: parseNumericValue(row['image cost (per image)']), - audioCostPerMinute: parseNumericValue(row['audio cost (per minute)']), - audioCostPerKCharacters: parseNumericValue(row['audio cost (per 1k characters)']), - audioInputCostPerMinute: parseNumericValue(row['audio input cost (per minute)']), - audioOutputCostPerMinute: parseNumericValue(row['audio output cost (per minute)']), videoCostPerSecond: parseNumericValue(row['video cost (per second)']), videoResolutionMultipliers: row['video resolution multipliers']?.trim(), batchProcessingMultiplier: parseNumericValue(row['batch processing multiplier']), @@ -163,10 +155,6 @@ export const parseCSVContent = (text: string): ParsedModelCost[] => { if (cost.cachedInputWriteCostPerMillion !== undefined && cost.cachedInputWriteCostPerMillion < 0) errors.push('Cache write cost cannot be negative'); if (cost.embeddingCostPerMillion !== undefined && cost.embeddingCostPerMillion < 0) errors.push('Embedding cost cannot be negative'); if (cost.imageCostPerImage !== undefined && cost.imageCostPerImage < 0) errors.push('Image cost cannot be negative'); - if (cost.audioCostPerMinute !== undefined && cost.audioCostPerMinute < 0) errors.push('Audio cost per minute cannot be negative'); - if (cost.audioCostPerKCharacters !== undefined && cost.audioCostPerKCharacters < 0) errors.push('Audio cost per 1k characters cannot be negative'); - if (cost.audioInputCostPerMinute !== undefined && cost.audioInputCostPerMinute < 0) errors.push('Audio input cost per minute cannot be negative'); - if (cost.audioOutputCostPerMinute !== undefined && cost.audioOutputCostPerMinute < 0) errors.push('Audio output cost per minute cannot be negative'); if (cost.videoCostPerSecond !== undefined && cost.videoCostPerSecond < 0) errors.push('Video cost cannot be negative'); if (cost.batchProcessingMultiplier !== undefined && cost.batchProcessingMultiplier < 0) errors.push('Batch processing multiplier cannot be negative'); if (cost.batchProcessingMultiplier !== undefined && cost.batchProcessingMultiplier > 1) errors.push('Batch processing multiplier cannot be greater than 1 (>100% cost)'); @@ -255,7 +243,6 @@ export const convertParsedToDto = (parsedData: ParsedModelCost[]): CreateModelCo cachedInputWriteCostPerMillionTokens: cost.cachedInputWriteCostPerMillion, embeddingCostPerMillionTokens: cost.embeddingCostPerMillion, imageCostPerImage: cost.imageCostPerImage, - audioCostPerMinute: cost.audioCostPerMinute, videoCostPerSecond: cost.videoCostPerSecond, batchProcessingMultiplier: cost.batchProcessingMultiplier, supportsBatchProcessing: cost.supportsBatchProcessing, diff --git a/ConduitLLM.WebUI/src/app/models/[id]/providers/page.tsx b/ConduitLLM.WebUI/src/app/models/[id]/providers/page.tsx new file mode 100644 index 000000000..46ba93300 --- /dev/null +++ b/ConduitLLM.WebUI/src/app/models/[id]/providers/page.tsx @@ -0,0 +1,14 @@ +import { notFound } from 'next/navigation'; + +interface PageProps { + params: Promise<{ + id: string; + }>; +} + +export default async function ModelProvidersPage({ params }: PageProps) { + const { id } = await params; + + // This page is not implemented yet + notFound(); +} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/BulkActions.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/BulkActions.tsx deleted file mode 100755 index dd231e2f0..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/BulkActions.tsx +++ /dev/null @@ -1,194 +0,0 @@ -'use client'; - -import { - Group, - Button, - Menu, - ActionIcon, - Tooltip, -} from '@mantine/core'; -import { - IconToggleLeft, - IconToggleRight, - IconRestore, - IconDots, - IconDownload, - IconUpload, -} from '@tabler/icons-react'; -import { notifications } from '@mantine/notifications'; - -interface ProviderDisplay { - providerId: string; - providerName: string; - priority: number; - weight?: number; - isEnabled: boolean; - statistics: { - usagePercentage: number; - successRate: number; - avgResponseTime: number; - }; - type: 'primary' | 'backup' | 'special'; -} - -interface BulkActionsProps { - providers: ProviderDisplay[]; - onAction: (action: 'enable-all' | 'disable-all' | 'reset') => void; - disabled: boolean; -} - -export function BulkActions({ providers, onAction, disabled }: BulkActionsProps) { - const enabledCount = providers.filter(p => p.isEnabled).length; - const disabledCount = providers.length - enabledCount; - - const handleExportConfiguration = () => { - const config = { - timestamp: new Date().toISOString(), - providers: providers.map(p => ({ - providerId: p.providerId, - providerName: p.providerName, - priority: p.priority, - weight: p.weight, - isEnabled: p.isEnabled, - type: p.type, - })) - }; - - const blob = new Blob([JSON.stringify(config, null, 2)], { - type: 'application/json' - }); - const url = URL.createObjectURL(blob); - const a = document.createElement('a'); - a.href = url; - a.download = `provider-priorities-${new Date().toISOString().split('T')[0]}.json`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - - notifications.show({ - title: 'Configuration Exported', - message: 'Provider configuration has been downloaded', - color: 'green', - }); - }; - - const handleImportConfiguration = () => { - const input = document.createElement('input'); - input.type = 'file'; - input.accept = '.json'; - input.onchange = (e) => { - const file = (e.target as HTMLInputElement).files?.[0]; - if (file) { - const reader = new FileReader(); - reader.onload = (e) => { - try { - JSON.parse(e.target?.result as string); - // Here you would validate and apply the imported configuration - notifications.show({ - title: 'Import Feature', - message: 'Import functionality would be implemented here', - color: 'blue', - }); - } catch { - notifications.show({ - title: 'Import Error', - message: 'Invalid configuration file format', - color: 'red', - }); - } - }; - reader.readAsText(file); - } - }; - input.click(); - }; - - return ( - - {/* Enable All */} - - - - - {/* Disable All */} - - - - - {/* Reset to Default */} - - - - - {/* More Actions Menu */} - - - - - - - - - Configuration - } - onClick={handleExportConfiguration} - > - Export Configuration - - } - onClick={handleImportConfiguration} - > - Import Configuration - - - - - Statistics - - {enabledCount} / {providers.length} enabled - - - Avg Success Rate: {providers.length > 0 ? - (providers.reduce((sum, p) => sum + p.statistics.successRate, 0) / providers.length).toFixed(1) - : 0}% - - - - - ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderList.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderList.tsx deleted file mode 100755 index 4cd1f8060..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderList.tsx +++ /dev/null @@ -1,97 +0,0 @@ -'use client'; - -import { - Card, - Stack, - Group, - Text, - Table, - Badge, -} from '@mantine/core'; -import { ProviderRow } from './ProviderRow'; - -interface ProviderDisplay { - providerId: string; - providerName: string; - priority: number; - weight?: number; - isEnabled: boolean; - statistics: { - usagePercentage: number; - successRate: number; - avgResponseTime: number; - }; - type: 'primary' | 'backup' | 'special'; -} - -interface ProviderListProps { - providers: ProviderDisplay[]; - originalProviders: ProviderDisplay[]; - onProviderUpdate: (index: number, updates: Partial) => void; - isLoading: boolean; -} - -export function ProviderList({ - providers, - originalProviders, - onProviderUpdate, - isLoading -}: ProviderListProps) { - const getProviderOriginalIndex = (provider: ProviderDisplay) => { - return originalProviders.findIndex(p => p.providerId === provider.providerId); - }; - - return ( - - - {/* Header */} - - Provider Priority List - - {providers.length} providers - - - - {/* Table Header */} - - - - - Priority - Provider - Type - Status - Usage % - Success Rate - Avg Response - Weight - Actions - - - - {providers.map((provider) => { - const originalIndex = getProviderOriginalIndex(provider); - return ( - - ); - })} - -
-
- - {providers.length === 0 && ( - - No providers to display - - )} -
-
- ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderRow.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderRow.tsx deleted file mode 100755 index 551aae735..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderRow.tsx +++ /dev/null @@ -1,279 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - Table, - NumberInput, - Switch, - Badge, - Text, - Progress, - Group, - Tooltip, - ActionIcon, -} from '@mantine/core'; -import { IconAlertTriangle } from '@tabler/icons-react'; -import { notifications } from '@mantine/notifications'; - -interface ProviderDisplay { - providerId: string; - providerName: string; - priority: number; - weight?: number; - isEnabled: boolean; - statistics: { - usagePercentage: number; - successRate: number; - avgResponseTime: number; - }; - type: 'primary' | 'backup' | 'special'; -} - -interface ProviderRowProps { - provider: ProviderDisplay; - index: number; - onUpdate: (index: number, updates: Partial) => void; - isLoading: boolean; - allProviders: ProviderDisplay[]; -} - -export function ProviderRow({ - provider, - index, - onUpdate, - isLoading, - allProviders -}: ProviderRowProps) { - const [priorityError, setPriorityError] = useState(null); - - const getProviderTypeColor = (type: string) => { - switch (type) { - case 'primary': return 'blue'; - case 'backup': return 'orange'; - case 'special': return 'green'; - default: return 'gray'; - } - }; - - const getSuccessRateColor = (rate: number) => { - if (rate >= 95) return 'green'; - if (rate >= 85) return 'yellow'; - return 'red'; - }; - - const getResponseTimeColor = (time: number) => { - if (time <= 300) return 'green'; - if (time <= 500) return 'yellow'; - return 'red'; - }; - - const validatePriority = (newPriority: number) => { - // Check for duplicate priorities - const duplicates = allProviders.filter(p => - p.providerId !== provider.providerId && p.priority === newPriority - ); - - if (duplicates.length > 0) { - setPriorityError(`Priority ${newPriority} is already used by ${duplicates[0].providerName}`); - return false; - } - - // Check priority range - if (newPriority < 1 || newPriority > allProviders.length) { - setPriorityError(`Priority must be between 1 and ${allProviders.length}`); - return false; - } - - setPriorityError(null); - return true; - }; - - const handlePriorityChange = (value: number | string) => { - const newPriority = typeof value === 'string' ? parseInt(value, 10) : value; - - if (isNaN(newPriority)) { - setPriorityError('Priority must be a number'); - return; - } - - if (validatePriority(newPriority)) { - onUpdate(index, { priority: newPriority }); - } - }; - - const handleEnabledChange = (checked: boolean) => { - // Prevent disabling if this is the last enabled provider - if (!checked) { - const enabledCount = allProviders.filter(p => p.isEnabled).length; - if (enabledCount <= 1) { - notifications.show({ - title: 'Action Prevented', - message: 'At least one provider must remain enabled', - color: 'orange', - }); - return; - } - } - - // Warn if disabling a high-usage provider - if (!checked && provider.statistics.usagePercentage > 25) { - notifications.show({ - title: 'High Usage Provider', - message: `Warning: ${provider.providerName} handles ${provider.statistics.usagePercentage}% of traffic`, - color: 'yellow', - }); - } - - onUpdate(index, { isEnabled: checked }); - }; - - const handleWeightChange = (value: number | string) => { - const newWeight = typeof value === 'string' ? parseInt(value, 10) : value; - if (!isNaN(newWeight) && newWeight >= 1 && newWeight <= 100) { - onUpdate(index, { weight: newWeight }); - } - }; - - return ( - - {/* Priority Input */} - -
- - {priorityError && ( - - - - - - )} -
-
- - {/* Provider Name and ID */} - -
- {provider.providerName} - {provider.providerId} -
-
- - {/* Provider Type */} - - - {provider.type} - - - - {/* Status */} - - handleEnabledChange(e.target.checked)} - size="sm" - disabled={isLoading} - onLabel="ON" - offLabel="OFF" - /> - - - {/* Usage Percentage */} - -
- - - {provider.statistics.usagePercentage.toFixed(1)}% - - - -
-
- - {/* Success Rate */} - -
- - - {provider.statistics.successRate.toFixed(1)}% - - - -
-
- - {/* Average Response Time */} - - - {provider.statistics.avgResponseTime.toFixed(0)}ms - - - - {/* Weight */} - - {provider.weight !== undefined ? ( - - ) : ( - N/A - )} - - - {/* Actions */} - - - {provider.statistics.usagePercentage > 50 && ( - - - High - - - )} - {provider.statistics.usagePercentage > 0 && provider.statistics.successRate < 90 && ( - - - Alert - - - )} - - -
- ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderStats.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderStats.tsx deleted file mode 100755 index 6924ba9b3..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/components/ProviderStats.tsx +++ /dev/null @@ -1,232 +0,0 @@ -'use client'; - -import { - Card, - SimpleGrid, - Group, - Text, - Badge, - Progress, - Stack, - RingProgress, - Center, -} from '@mantine/core'; -import { - IconServer, - IconActivity, - IconClock, - IconChartBar, - IconToggleLeft, - IconToggleRight, -} from '@tabler/icons-react'; - -interface ProviderDisplay { - providerId: string; - providerName: string; - priority: number; - weight?: number; - isEnabled: boolean; - statistics: { - usagePercentage: number; - successRate: number; - avgResponseTime: number; - }; - type: 'primary' | 'backup' | 'special'; -} - -interface ProviderStatsProps { - providers: ProviderDisplay[]; -} - -export function ProviderStats({ providers }: ProviderStatsProps) { - const enabledProviders = providers.filter(p => p.isEnabled); - const disabledProviders = providers.filter(p => !p.isEnabled); - const avgSuccessRate = providers.length > 0 - ? providers.reduce((sum, p) => sum + p.statistics.successRate, 0) / providers.length - : 0; - const avgResponseTime = providers.length > 0 - ? providers.reduce((sum, p) => sum + p.statistics.avgResponseTime, 0) / providers.length - : 0; - - const providersByType = providers.reduce((acc, p) => { - acc[p.type] = (acc[p.type] || 0) + 1; - return acc; - }, {} as Record); - - const topProviders = providers - .filter(p => p.isEnabled) - .sort((a, b) => b.statistics.usagePercentage - a.statistics.usagePercentage) - .slice(0, 3); - - const getSuccessRateColor = (rate: number) => { - if (rate >= 95) return 'green'; - if (rate >= 85) return 'yellow'; - return 'red'; - }; - - const getResponseTimeColor = (time: number) => { - if (time <= 300) return 'green'; - if (time <= 500) return 'yellow'; - return 'red'; - }; - - return ( - - {/* Provider Count */} - - -
- - Total Providers - - - {providers.length} - - - - - {enabledProviders.length} enabled - - {disabledProviders.length > 0 && ( - - - {disabledProviders.length} disabled - - )} - -
- -
-
- - {/* Success Rate */} - - -
- - Avg Success Rate - - - {avgSuccessRate.toFixed(1)}% - - -
- -
-
- - {/* Response Time */} - - -
- - Avg Response Time - - - {avgResponseTime.toFixed(0)}ms - - - {(() => { - if (avgResponseTime <= 300) return 'Excellent'; - if (avgResponseTime <= 500) return 'Good'; - return 'Needs Attention'; - })()} - -
- -
-
- - {/* Provider Distribution */} - - -
- - Provider Types - - - {Object.entries(providersByType).map(([type, count]) => ( - - { - if (type === 'primary') return 'blue'; - if (type === 'backup') return 'orange'; - return 'green'; - })()} - size="sm" - > - {type} - - {count} - - ))} - -
- -
-
- - {/* Top Providers by Usage */} - {topProviders.length > 0 && ( - - Top Providers by Usage - - {topProviders.map((provider, index) => ( - - -
- {provider.providerName} - { - if (index === 0) return 'gold'; - if (index === 1) return 'gray'; - return 'orange'; - })()} - size="xs" - > - #{index + 1} - -
- - - {provider.statistics.usagePercentage.toFixed(0)}% - - - } - /> -
- - - Success: {provider.statistics.successRate.toFixed(1)}% - - - {provider.statistics.avgResponseTime.toFixed(0)}ms - - -
- ))} -
-
- )} -
- ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/index.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/index.tsx deleted file mode 100755 index fbb6e2ee5..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/index.tsx +++ /dev/null @@ -1,341 +0,0 @@ -'use client'; - -import { useState, useEffect, useCallback } from 'react'; -import { - Card, - Stack, - Group, - Text, - Title, - TextInput, - Button, - Alert, - Center, - Loader, -} from '@mantine/core'; -import { - IconSearch, - IconRefresh, - IconAlertCircle, - IconDeviceFloppy, - IconX, -} from '@tabler/icons-react'; -import { notifications } from '@mantine/notifications'; -import { ProviderPriority } from '../../types/routing'; -import { ProviderList } from './components/ProviderList'; -import { BulkActions } from './components/BulkActions'; -import { ProviderStats } from './components/ProviderStats'; -import { useProviderPriorities } from '../../hooks/useProviderPriorities'; - -interface ProviderDisplay extends ProviderPriority { - statistics: { - usagePercentage: number; - successRate: number; - avgResponseTime: number; - }; - type: 'primary' | 'backup' | 'special'; -} - -interface ProviderPriorityManagerProps { - onLoadingChange: (loading: boolean) => void; -} - -export function ProviderPriorityManager({ onLoadingChange }: ProviderPriorityManagerProps) { - const [providers, setProviders] = useState([]); - const [originalProviders, setOriginalProviders] = useState([]); - const [filteredProviders, setFilteredProviders] = useState([]); - const [hasChanges, setHasChanges] = useState(false); - const [filter, setFilter] = useState(''); - const [refreshKey, setRefreshKey] = useState(0); - - const { - isLoading, - error, - } = useProviderPriorities(); - - useEffect(() => { - onLoadingChange(isLoading); - }, [isLoading, onLoadingChange]); - - const loadData = useCallback(async () => { - try { - const providerHealthData = await fetch('/api/health/providers') - .then(res => res.json() as Promise>) - .catch(() => []); - - // Transform provider data to include statistics and type - const providersWithStats: ProviderDisplay[] = providerHealthData.map(provider => { - return { - providerId: provider.id, - providerName: provider.name, - priority: 1, - isEnabled: provider.status === 'healthy', - statistics: { - usagePercentage: 0, - successRate: typeof provider.uptime === 'number' ? provider.uptime : 0, - avgResponseTime: typeof provider.responseTime === 'number' ? provider.responseTime : 0, - }, - type: determineProviderType(provider.name), - }; - }); - - setProviders(providersWithStats); - setOriginalProviders(JSON.parse(JSON.stringify(providersWithStats)) as ProviderDisplay[]); - setHasChanges(false); - } catch { - // Error is handled by the hook - } - }, []); - - useEffect(() => { - void loadData(); - }, [refreshKey, loadData]); - - useEffect(() => { - // Filter providers based on search term - if (!filter.trim()) { - setFilteredProviders(providers); - } else { - const filtered = providers.filter(provider => - provider.providerName.toLowerCase().includes(filter.toLowerCase()) || - provider.providerId.toLowerCase().includes(filter.toLowerCase()) || - provider.type.toLowerCase().includes(filter.toLowerCase()) - ); - setFilteredProviders(filtered); - } - }, [providers, filter]); - - - const determineProviderType = (providerName: string): 'primary' | 'backup' | 'special' => { - const name = providerName.toLowerCase(); - if (name.includes('primary') || name.includes('openai') || name.includes('anthropic')) { - return 'primary'; - } - if (name.includes('backup') || name.includes('azure') || name.includes('fallback')) { - return 'backup'; - } - return 'special'; - }; - - const handleProviderUpdate = useCallback((index: number, updates: Partial) => { - setProviders(prev => { - const updated = [...prev]; - updated[index] = { ...updated[index], ...updates }; - return updated; - }); - setHasChanges(true); - }, []); - - const handleBulkAction = useCallback((action: 'enable-all' | 'disable-all' | 'reset') => { - switch (action) { - case 'enable-all': - setProviders(prev => prev.map(p => ({ ...p, isEnabled: true }))); - setHasChanges(true); - break; - case 'disable-all': { - const enabledCount = providers.filter(p => p.isEnabled).length; - if (enabledCount <= 1) { - notifications.show({ - title: 'Action Prevented', - message: 'At least one provider must remain enabled', - color: 'orange', - }); - return; - } - setProviders(prev => prev.map(p => ({ ...p, isEnabled: false }))); - setHasChanges(true); - break; - } - case 'reset': - setProviders(JSON.parse(JSON.stringify(originalProviders)) as ProviderDisplay[]); - setHasChanges(false); - break; - } - }, [providers, originalProviders]); - - const handleSave = async () => { - try { - // Validate priorities are unique - const priorities = providers.map(p => p.priority); - const uniquePriorities = new Set(priorities); - if (priorities.length !== uniquePriorities.size) { - notifications.show({ - title: 'Validation Error', - message: 'All provider priorities must be unique', - color: 'red', - }); - return; - } - - // Ensure at least one provider is enabled - const enabledProviders = providers.filter(p => p.isEnabled); - if (enabledProviders.length === 0) { - notifications.show({ - title: 'Validation Error', - message: 'At least one provider must be enabled', - color: 'red', - }); - return; - } - - // Save functionality removed - no backend implementation - notifications.show({ - title: 'Info', - message: 'Provider priority management is not currently implemented', - color: 'blue', - }); - } catch { - // Error is handled by the hook - } - }; - - const handleCancel = () => { - setProviders(JSON.parse(JSON.stringify(originalProviders)) as ProviderDisplay[]); - setHasChanges(false); - }; - - const handleRefresh = () => { - setRefreshKey(prev => prev + 1); - }; - - if (error) { - return ( - } title="Error" color="red"> - {error} - - ); - } - - return ( - - {/* Header */} - - -
- Provider Priority Management - - Configure provider priorities, enable/disable providers, and monitor routing statistics - -
- -
-
- - {/* Statistics Overview */} - - - {/* Search and Bulk Actions */} - - - } - value={filter} - onChange={(e) => setFilter(e.target.value)} - rightSection={ - filter && ( - - ) - } - style={{ flex: 1, maxWidth: 300 }} - /> - - - - - {/* Provider List */} - {(() => { - if (isLoading && providers.length === 0) { - return ( -
- -
- ); - } - - if (filteredProviders.length === 0) { - return ( - -
- - - {filter ? 'No providers match your search' : 'No providers configured'} - - - {filter - ? 'Try adjusting your search terms' - : 'Provider priorities will appear here once you configure LLM providers' - } - - -
-
- ); - } - - return ( - - ); - })()} - - {/* Save/Cancel Actions */} - {hasChanges && ( - - -
- Unsaved Changes - - You have modified provider priorities. Save your changes to apply them. - -
- - - - -
-
- )} -
- ); -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/utils/priorityHelpers.ts b/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/utils/priorityHelpers.ts deleted file mode 100755 index 1f0efdfae..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProviderPriorityManager/utils/priorityHelpers.ts +++ /dev/null @@ -1,242 +0,0 @@ -interface ProviderPriority { - providerId: string; - providerName: string; - priority: number; - isEnabled: boolean; - weight?: number; -} - -/** - * Validates that all provider priorities are unique - */ -export function validateUniquePriorities(providers: ProviderPriority[]): { - isValid: boolean; - errors: string[]; - duplicates: { priority: number; providers: string[] }[]; -} { - const errors: string[] = []; - const duplicates: { priority: number; providers: string[] }[] = []; - const priorityMap = new Map(); - - // Group providers by priority - providers.forEach(provider => { - if (!priorityMap.has(provider.priority)) { - priorityMap.set(provider.priority, []); - } - const providerList = priorityMap.get(provider.priority); - if (providerList) { - providerList.push(provider.providerName); - } - }); - - // Find duplicates - priorityMap.forEach((providerNames, priority) => { - if (providerNames.length > 1) { - duplicates.push({ priority, providers: providerNames }); - errors.push(`Priority ${priority} is used by multiple providers: ${providerNames.join(', ')}`); - } - }); - - return { - isValid: errors.length === 0, - errors, - duplicates, - }; -} - -/** - * Auto-adjusts priorities to ensure uniqueness when a provider's priority changes - */ -export function autoAdjustPriorities( - providers: ProviderPriority[], - changedProviderId: string, - newPriority: number -): ProviderPriority[] { - const result = [...providers]; - const changedProviderIndex = result.findIndex(p => p.providerId === changedProviderId); - - if (changedProviderIndex === -1) return result; - - const oldPriority = result[changedProviderIndex].priority; - - // If the new priority is the same as the old one, no change needed - if (oldPriority === newPriority) return result; - - // Update the changed provider's priority - result[changedProviderIndex].priority = newPriority; - - // Find provider with the conflicting priority - const conflictingProvider = result.find( - (p, index) => index !== changedProviderIndex && p.priority === newPriority - ); - - if (conflictingProvider) { - // Shift the conflicting provider to the old priority - conflictingProvider.priority = oldPriority; - } - - return result; -} - -/** - * Normalizes priorities to be sequential starting from 1 - */ -export function normalizePriorities(providers: ProviderPriority[]): ProviderPriority[] { - const sorted = [...providers].sort((a, b) => a.priority - b.priority); - - return sorted.map((provider, index) => ({ - ...provider, - priority: index + 1, - })); -} - -/** - * Validates priority constraints - */ -export function validatePriorityConstraints(providers: ProviderPriority[]): { - isValid: boolean; - errors: string[]; -} { - const errors: string[] = []; - - // Check if at least one provider is enabled - const enabledProviders = providers.filter(p => p.isEnabled); - if (enabledProviders.length === 0) { - errors.push('At least one provider must be enabled'); - } - - // Check priority ranges - providers.forEach(provider => { - if (provider.priority < 1) { - errors.push(`${provider.providerName}: Priority must be at least 1`); - } - if (provider.priority > providers.length) { - errors.push(`${provider.providerName}: Priority cannot exceed ${providers.length}`); - } - }); - - // Check for gaps in priorities (optional validation) - const priorities = providers.map(p => p.priority).sort((a, b) => a - b); - for (let i = 1; i <= providers.length; i++) { - if (!priorities.includes(i)) { - errors.push(`Priority ${i} is missing - priorities should be sequential`); - break; - } - } - - return { - isValid: errors.length === 0, - errors, - }; -} - -/** - * Calculates optimal priority distribution based on usage statistics - */ -export function suggestOptimalPriorities(providers: Array): ProviderPriority[] { - // Score providers based on multiple factors - const scoredProviders = providers.map(provider => { - const successWeight = 0.4; - const responseTimeWeight = 0.3; - const usageWeight = 0.3; - - // Normalize response time (lower is better) - const normalizedResponseTime = Math.max(0, 100 - (provider.statistics.avgResponseTime / 10)); - - const score = - (provider.statistics.successRate * successWeight) + - (normalizedResponseTime * responseTimeWeight) + - (provider.statistics.usagePercentage * usageWeight); - - return { - ...provider, - score, - }; - }); - - // Sort by score (descending) and assign priorities - const sorted = scoredProviders.sort((a, b) => b.score - a.score); - - return sorted.map((provider, index) => ({ - providerId: provider.providerId, - providerName: provider.providerName, - priority: index + 1, - isEnabled: provider.isEnabled, - weight: provider.weight, - })); -} - -/** - * Gets default configuration for new providers - */ -export function getDefaultProviderConfig( - existingProviders: ProviderPriority[], - providerName: string, - providerType: 'primary' | 'backup' | 'special' -): Partial { - const highestPriority = existingProviders.length > 0 - ? Math.max(...existingProviders.map(p => p.priority)) - : 0; - - const defaultPriority = providerType === 'primary' ? 1 : highestPriority + 1; - const defaultWeight = providerType === 'primary' ? 100 : 50; - - return { - priority: defaultPriority, - isEnabled: true, - weight: defaultWeight, - }; -} - -/** - * Validates provider configuration changes - */ -export function validateProviderChanges( - originalProviders: ProviderPriority[], - updatedProviders: ProviderPriority[] -): { - isValid: boolean; - warnings: string[]; - errors: string[]; -} { - const warnings: string[] = []; - const errors: string[] = []; - - // Check for major changes that might affect traffic - updatedProviders.forEach(updated => { - const original = originalProviders.find(p => p.providerId === updated.providerId); - if (!original) return; - - // Warn about disabling high-usage providers - if (original.isEnabled && !updated.isEnabled) { - // This would need real usage data - warnings.push(`Disabling ${updated.providerName} - this may affect traffic distribution`); - } - - // Warn about major priority changes - const priorityChange = Math.abs(updated.priority - original.priority); - if (priorityChange > 3) { - warnings.push(`Large priority change for ${updated.providerName} (${original.priority} → ${updated.priority})`); - } - }); - - // Validate unique priorities - const uniqueValidation = validateUniquePriorities(updatedProviders); - errors.push(...uniqueValidation.errors); - - // Validate constraints - const constraintsValidation = validatePriorityConstraints(updatedProviders); - errors.push(...constraintsValidation.errors); - - return { - isValid: errors.length === 0, - warnings, - errors, - }; -} \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab.tsx deleted file mode 100755 index aa72fa793..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab.tsx +++ /dev/null @@ -1 +0,0 @@ -export { ProvidersTab } from './ProvidersTab/index'; \ No newline at end of file diff --git a/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab/ProviderPriorityList.tsx b/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab/ProviderPriorityList.tsx deleted file mode 100755 index 18d1a16ec..000000000 --- a/ConduitLLM.WebUI/src/app/routing-settings/components/ProvidersTab/ProviderPriorityList.tsx +++ /dev/null @@ -1,340 +0,0 @@ -'use client'; - -import { useState } from 'react'; -import { - Card, - Stack, - Group, - Text, - Badge, - NumberInput, - Switch, - Button, - Select, - Title, - Progress, -} from '@mantine/core'; -import { - IconGripVertical, - IconDeviceFloppy, - IconActivity, -} from '@tabler/icons-react'; -import { DragDropContext, Droppable, Draggable } from '@hello-pangea/dnd'; -import { notifications } from '@mantine/notifications'; -import { ProviderPriority, RoutingConfiguration, LoadBalancerHealth } from '../../types/routing'; - -interface ProviderPriorityListProps { - providers: ProviderPriority[]; - config: RoutingConfiguration | null; - health: LoadBalancerHealth | null; - onUpdateProviders: (providers: ProviderPriority[]) => void; - onUpdateConfig: (config: Partial) => void; -} - -export function ProviderPriorityList({ - providers, - config, - health, - onUpdateProviders, - onUpdateConfig, -}: ProviderPriorityListProps) { - const [localProviders, setLocalProviders] = useState(providers); - const [localConfig, setLocalConfig] = useState>(config ?? {}); - const [hasChanges, setHasChanges] = useState(false); - - const handleProviderChange = (index: number, field: keyof ProviderPriority, value: number | boolean | string) => { - const updated = localProviders.map((provider, i) => - i === index ? { ...provider, [field]: value } : provider - ); - setLocalProviders(updated); - setHasChanges(true); - }; - - const handleConfigChange = (field: keyof RoutingConfiguration, value: number | boolean | string) => { - const updated = { ...localConfig, [field]: value }; - setLocalConfig(updated); - setHasChanges(true); - }; - - const handleDragEnd = (result: { destination?: { index: number } | null; source: { index: number } }) => { - if (!result.destination) return; - - const items = Array.from(localProviders); - const [reorderedItem] = items.splice(result.source.index, 1); - items.splice(result.destination.index, 0, reorderedItem); - - // Update priorities based on new order - const updated = items.map((provider, index) => ({ - ...provider, - priority: items.length - index, - })); - - setLocalProviders(updated); - setHasChanges(true); - }; - - const handleSave = async () => { - try { - onUpdateProviders(localProviders); - if (Object.keys(localConfig).length > 0) { - onUpdateConfig(localConfig); - } - setHasChanges(false); - notifications.show({ - title: 'Success', - message: 'Provider settings saved successfully', - color: 'green', - }); - } catch { - // Error handling is done in the hook - } - }; - - const handleReset = () => { - setLocalProviders(providers); - setLocalConfig(config ?? {}); - setHasChanges(false); - }; - - const getHealthStatus = (providerId: string) => { - if (!health) return null; - const node = health.nodes.find(n => n.id === providerId); - return node?.status ?? null; - }; - - const getHealthColor = (status: string | null) => { - switch (status) { - case 'healthy': return 'green'; - case 'unhealthy': return 'red'; - case 'draining': return 'yellow'; - default: return 'gray'; - } - }; - - return ( - - {/* Configuration Settings */} - - Routing Configuration - - onRequestChange({ ...request, model: value ?? '' })} - searchable - disabled={modelsLoading} - required - error={!request.model.trim() ? 'Model is required' : null} - allowDeselect={false} - /> - - - - -